Merge remote-tracking branch 'origin/development' into development

# Conflicts:
#	.github/workflows/build.yml
#	.github/workflows/build_and_docs_to_dev.yml
#	.github/workflows/manual_docs.yml
#	.github/workflows/release.yml
#	platformio.ini
This commit is contained in:
chriz committed 2026-10-27 22:31:36 +01:00
commit a11f3b9990
430 files changed
+27120 -38785

No files matched your search

+3 -6
View File
@@ -67,16 +67,13 @@ jobs:
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
- name: Set version tag in sources
env:
TAG_NAME: ${{ steps.prep.outputs.tag_name }}
run: |
sed -i "s/version_tag/${TAG_NAME}/g" main/User_config.h scripts/latest_version.json
- name: Build OpenMQTTGateway Firmware (${{ steps.prep.outputs.pio_env }})
env:
PIO_ENV: ${{ steps.prep.outputs.pio_env }}
TAG_NAME: ${{ steps.prep.outputs.tag_name }}
run: |
# Inject the firmware version (same mechanism as upstream scripts/ci_build_firmware.sh)
export PLATFORMIO_BUILD_FLAGS="-DOMG_VERSION=\\\"${TAG_NAME}\\\""
mkdir -p ".pio/build/${PIO_ENV}"
pio run -e "${PIO_ENV}"
+37
View File
@@ -1,13 +1,50 @@
.pio
.vscode
*_env.ini
*.pem
**/.DS_Store
node_modules
*.ps1
main/certs/private*
lib/*
!lib/esp32-bt-lib
!lib/TheengsUtils
!lib/LEDManager
CMakeLists.txt
dependencies.lock
sdkconfig.*
!sdkconfig.defaults
__pycache__
*.pem
managed_components
.github/chatmodes
.github/prompts
.github/*instructions.md
.github/workflows/_docs
# CI/CD Generated outputs
docs/.vitepress/dist/
docs/.vitepress/cache/
docs/.vitepress/public/boards-info.json
docs/.vitepress/public/commonConfig.js
docs/.vitepress/public/firmware_build
# Generated site/firmware/reports files
generated/
# Generated ssl for testing
.ssl
# Generated JSON files from templates
scripts/latest_version.json
scripts/latest_version_dev.json
# Generated configuration files
docs/.vitepress/meta.json
# Generated full documentation pages
docs/prerequisites/board-full.md
docs/upload/web-install-full.md
.superpowers/
docs/superpowers/
+1
View File
@@ -0,0 +1 @@
disable=SC1091
+319
View File
@@ -0,0 +1,319 @@
# OpenMQTTGateway - Claude Code Context
This document provides essential context about the OpenMQTTGateway project for AI assistants working on the codebase.
## Project Overview
OpenMQTTGateway is a unified firmware that bridges various wireless technologies (RF 433MHz, IR, BLE, LoRa, etc.) to MQTT protocol, enabling home automation integration with platforms like Home Assistant, OpenHAB, and Node-RED.
**Key Features:**
- Multi-protocol gateway (BLE, RF 433MHz, IR, LoRa, RTL_433, Pilight, etc.)
- ESP8266/ESP32 and Arduino support
- MQTT-based communication
- Web UI for configuration
- Home Assistant MQTT Discovery support
- Over 100 supported BLE sensors via Theengs Decoder
**Main Branch:** `development` (not master/main)
## Architecture
### Modular Design
The firmware uses a modular architecture where each protocol/sensor/actuator is implemented as a separate "gateway" or "sensor" module:
- **Gateways**: Receive and transmit signals (e.g., `gatewayBT.cpp`, `gatewayRF.cpp`, `gatewayIR.cpp`)
- **Sensors**: Read physical sensor data (e.g., `sensorBME280.cpp`, `sensorDHT.cpp`)
- **Actuators**: Control outputs (e.g., `actuatorONOFF.cpp`, `actuatorPWM.cpp`)
Each module has:
- A config file (e.g., `config_BT.h`, `config_RF.h`)
- An implementation file (e.g., `gatewayBT.cpp`, `gatewayRF.cpp`)
### Configuration System
Configuration is managed through:
1. **User_config.h** - Main configuration file with defaults
2. **config_*.h** - Module-specific configurations
3. **platformio.ini & environments.ini** - Build-time configurations
4. **prod_env.ini** - User-specific environments (gitignored)
Build flags in `environments.ini` can override User_config.h parameters.
## Directory Structure
```
OpenMQTTGateway/
├── main/ # Main source code (Arduino convention)
│ ├── main.cpp # Main application entry point
│ ├── User_config.h # Global configuration
│ ├── config_*.h # Module configurations
│ ├── gateway*.cpp # Gateway implementations
│ ├── sensor*.cpp # Sensor implementations
│ ├── actuator*.cpp # Actuator implementations
│ ├── webUI.cpp # Web interface
│ ├── mqttDiscovery.cpp # Home Assistant discovery
│ └── rf/ # RF-specific modules
├── lib/ # Custom libraries
├── scripts/ # Build and automation scripts
├── docs/ # VuePress documentation
├── tests/ # Test configurations
├── environments.ini # PlatformIO environment definitions
├── platformio.ini # PlatformIO configuration
└── prod_env.ini.example # Example production environment
Note: src_dir is set to "main" in platformio.ini
```
## Key Concepts
### Environment-Based Builds
The project supports 80+ hardware configurations defined in `environments.ini`. Examples:
- `esp32dev-ble` - ESP32 with BLE gateway
- `esp32dev-pilight` - ESP32 with Pilight RF
- `esp32dev-rtl_433` - ESP32 with RTL_433 decoder
- `nodemcuv2-ir` - NodeMCU with IR
- `rfbridge` - Sonoff RF Bridge
### MQTT Topics
Standard topic structure:
- Base: `home/` (configurable via Base_Topic)
- Gateway name: `OpenMQTTGateway` or `OMG` (configurable)
- Example: `home/OpenMQTTGateway/BTtoMQTT`
### Module Activation
Modules are activated via build flags in environments.ini:
```ini
build_flags =
'-DZgatewayBT="BT"' # Enable BLE gateway
'-DZgatewayRF="RF"' # Enable RF gateway
'-DZsensorBME280="BME280"' # Enable BME280 sensor
```
### Web UI & WiFi Manager
- ESP32/ESP8266 boards include a WiFiManager for initial setup
- Access point: SSID = `WifiManager_ssid`, Password = `WifiManager_password`
- Web UI provides runtime configuration and control
- Can disable WiFiManager with `ESPWifiManualSetup` flag
## Important Files
### Core Files
- **main/main.cpp** - Main application loop, MQTT handling, module coordination
- **main/User_config.h** - Global configuration defaults
- **main/webUI.cpp** - Web interface implementation
- **main/mqttDiscovery.cpp** - Home Assistant MQTT discovery
### Gateway Files
- **main/gatewayBT.cpp** - BLE gateway (uses Theengs Decoder)
- **main/gatewayRF.cpp** - RF 433MHz gateway (RCSwitch library)
- **main/gatewayRF2.cpp** - Alternative RF implementation
- **main/gatewayPilight.cpp** - Pilight protocol gateway
- **main/gatewayRTL_433.cpp** - RTL_433 decoder integration
- **main/gatewayIR.cpp** - Infrared gateway
- **main/gatewayLORA.cpp** - LoRa gateway
### Configuration Files
All in `main/` directory:
- **config_RF.h** - RF gateway configuration
- **config_BT.h** - BLE gateway configuration
- **config_IR.h** - IR gateway configuration
- etc.
### Build System
- **platformio.ini** - Main PlatformIO config
- **environments.ini** - All environment definitions (~3000+ lines)
- **scripts/** - Python scripts for build automation
## Common Patterns
### Adding a New Gateway Module
1. Create `gateway[NAME].cpp` in `main/`
2. Create `config_[NAME].h` in `main/`
3. Add build flag in `environments.ini`: `-DZgateway[NAME]="[NAME]"`
4. Include conditional compilation: `#ifdef Zgateway[NAME]`
5. Implement setup function: `setup[NAME]()`
6. Implement loop function: `[NAME]toMQTT()`
7. Optionally implement MQTT callback: `MQTTto[NAME]()`
### Adding a New Sensor Module
Similar to gateways, but prefix with `sensor` instead of `gateway` and `Z` becomes `Zsensor`.
### Configuration Override Pattern
```cpp
#ifndef PARAM_NAME
# define PARAM_NAME default_value
#endif
```
This allows platformio.ini build flags to override defaults.
### MQTT Publishing Pattern
```cpp
pub(subjectToMQTT, data); // Basic publish
pub_custom_topic(topic, data, retain); // Custom topic with retain
```
## Development Workflow
### Building
```bash
# List all environments
pio run --list-targets
# Build specific environment
pio run -e esp32dev-ble
# Upload to device
pio run -e esp32dev-ble -t upload
# Monitor serial
pio device monitor
```
### Code Formatting
- Uses clang-format with config in `.clang-format`
- CI checks formatting automatically
- Format before committing
### Testing
- Unit tests in `tests/` directory
- CI runs builds for all environments
- Nightly builds test comprehensive environment matrix
### Documentation
- Documentation uses VuePress, located in `docs/`
- Hosted at https://docs.openmqttgateway.com
## CI/CD
### GitHub Actions Workflows
Located in `.github/workflows/`:
- **build.yml** - Build validation on PRs
- **nightly.yml** - Nightly builds of all environments
- **format-check.yml** - Code formatting validation
### Build Matrix
The nightly build tests configurations:
- ESP32 variants (ESP32, ESP32-S3, ESP32-C3)
- ESP8266 variants (NodeMCU, Sonoff)
- Different gateway combinations
- Board-specific builds (M5Stack, Heltec, TTGO, etc.)
## Common Issues & Solutions
### Memory Constraints
- ESP8266 has limited RAM (~40KB free)
- Use `PROGMEM` for constants
- Minimize module combinations on ESP8266
- ESP32 has more headroom (~200KB+ free)
### RF Module Conflicts
- Some RF implementations (RF, RF2, Pilight, RTL_433) may conflict
- Use separate environments for different RF approaches
- Check GPIO pin assignments in config files
### BLE on ESP32
- BLE stack uses significant RAM
- Limit simultaneous BLE connections
## Related Projects
- **Theengs Decoder** - BLE device decoder library
- **Theengs Gateway** - Python version for Raspberry Pi/PC
- **RTL_433_ESP** - ESP32 port of RTL_433
## Resources
- **Documentation**: https://docs.openmqttgateway.com
- **Community Forum**: https://community.openmqttgateway.com
- **GitHub**: https://github.com/1technophile/OpenMQTTGateway
- **Web Installer**: https://docs.openmqttgateway.com/upload/web-install.html
## Products
- **Theengs Bridge** - ESP32 BLE gateway with Ethernet
- **Theengs Plug** - Smart plug with BLE gateway
## Conventions
### Naming
- Gateways: `gateway[NAME].cpp` with `Zgateway[NAME]` flag
- Sensors: `sensor[NAME].cpp` with `Zsensor[NAME]` flag
- Actuators: `actuator[NAME].cpp` with `Zactuator[NAME]` flag
- Config files: `config_[NAME].h`
### Code Style
- 2-space indentation
- Follow existing patterns in similar modules
- Use preprocessor directives for module isolation
- Comment complex RF protocols or BLE parsing
### Git Workflow
- Main branch: `development`
- Feature branches from `development`
- PRs target `development`
- Squash commits when appropriate
- Descriptive commit messages
## Quick Start for Contributors
1. Fork the repository
2. Create `prod_env.ini` for your local environments
3. Choose an environment from `environments.ini`
4. Build: `pio run -e [environment-name]`
5. Make changes following existing patterns
6. Test build with your environment
7. Run format check
8. Submit PR to `development` branch
## Platform-Specific Notes
### ESP32
- Preferred platform for new development
- More RAM and Flash
- Supports BLE natively
- Can use Ethernet via add-on boards
### ESP8266
- Legacy support
- Memory constrained
- No native BLE
- Good for simple RF/IR gateways
## Tips for AI Assistants
1. **Check environments.ini** when modifying build configurations
2. **Follow existing module patterns** when adding new features
3. **Test on relevant environments** - changes may affect multiple boards
4. **Memory is precious** especially on ESP8266
5. **MQTT topic structure** is critical for Home Assistant integration
6. **Config headers** allow user customization without code changes
7. **Use conditional compilation** to keep builds modular
8. **WebUI changes** need both backend (webUI.cpp) and frontend (config_WebUI.h)
9. **RF protocols** often need precise timing - be careful with changes
10. **The main branch is `development`** - always target PRs there
+10 -24
View File
@@ -1,8 +1,5 @@
[![Community forum](https://img.shields.io/badge/community-forum-brightgreen.svg)](https://community.openmqttgateway.com)
![Build](https://github.com/1technophile/OpenMQTTGateway/workflows/Build/badge.svg?branch=development)
![Check Code Format](https://github.com/1technophile/OpenMQTTGateway/workflows/Check%20Code%20Format/badge.svg?branch=development)
[![OpenMQTTGateway capabilities](https://github.com/1technophile/OpenMQTTGateway/blob/development/docs/img/OpenMQTTGateway.png)](https://community.openmqttgateway.com)
OpenMQTTGateway aims to unify various technologies and protocols into a single firmware. This reduces the need for multiple physical bridges and streamlines diverse technologies under the widely-used [MQTT](http://mqtt.org/) protocol.
@@ -15,49 +12,39 @@ OpenMQTTGateway aims to unify various technologies and protocols into a single f
The documentation is [here](https://docs.openmqttgateway.com)
New to OpenMQTTGateway? Follow the [Getting Started guide](https://docs.openmqttgateway.com/getting-started.html) to flash a board from your browser and see your first sensor data in about 15 minutes.
The reference sheet, with the list of all functions, pinouts is [here](https://docs.google.com/spreadsheets/d/1_5fQjAixzRtepkykmL-3uN3G5bLfQ0zMajM9OBZ1bx0/edit#gid=0)
## Upload
Upload OpenMQTTGateway directly from the [upload page](https://docs.openmqttgateway.com/upload/web-install.html) (no additional software required) or [build your own configuration](https://docs.openmqttgateway.com/upload/builds.html) with [PlatformIO](https://platformio.org/) or [Arduino IDE](https://www.arduino.cc/en/software)
Upload OpenMQTTGateway directly from the [upload page](https://docs.openmqttgateway.com/upload/web-install.html) (no additional software required) or [build your own configuration](https://docs.openmqttgateway.com/upload/builds.html) with [PlatformIO](https://platformio.org/).
## Using OpenMQTTGateway ?
Support open-source development through sponsorship and gain exclusive access to our private forum. Your questions, issues, and feature requests will receive priority attention, plus you'll gain insider access to our roadmap.
Support open-source development through sponsorship, it helps keep the project healthy and evolving.
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/theengs)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/1technophile)
## Products powered by OpenMQTTGateway
### Theengs Bridge, Bluetooth gateway (BLE) with external antenna
[Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) is a powerfull BLE to MQTT gateway for over [90 sensors](https://decoder.theengs.io/devices/devices.html). Equipped with an Ethernet port, and external antenna, ensuring an enhanced range for your BLE sensors. It supports also WiFi connectivity.
[Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) is a powerfull BLE to MQTT gateway for over [100 sensors](https://decoder.theengs.io/devices/devices.html). Equipped with an Ethernet port, and external antenna, ensuring an enhanced range for your BLE sensors. It supports also WiFi connectivity.
[![Theengs bridge view](./docs/img/Theengs-Bridge-ble-gateway.png)](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna)
### Theengs Plug, Bluetooth gateway (BLE) gateway and Smart Plug
[Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption) brings the following features:
* BLE to MQTT gateway, tens of [Bluetooth devices](https://compatible.openmqttgateway.com/index.php/devices/ble-devices/) supported thanks to Theengs Decoder library. The plug uses an ESP32 acting as a BLE to Wifi gateway to scan, decode and forward the data of the nearby sensors,
* Smart plug that can be controlled remotely,
* Energy consumption monitoring,
* Device tracker,
* Presence detection (beta),
* Local connectivity first.
[![Theengs plug view](./docs/img/Theengs-Plug-OpenMQTTGateway.png)](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
Support the project by purchasing the [Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) or the [Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
Support the project by purchasing the [Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna)
## Compatible items
* [List of supported devices](https://compatible.openmqttgateway.com/index.php/devices/), door/window sensors, PIR sensors, smoke detectors, weather stations...
* [List of supported BLE devices](https://decoder.theengs.io/devices/devices.html), thermometers, scales, plant sensors, beacons...
* [List of compatible boards (Off the shelf or DIY) is available](https://compatible.openmqttgateway.com/index.php/boards/), RF Bridge, IR, BLE gateways...
* [Boards guide](https://docs.openmqttgateway.com/prerequisites/board.html), RF Bridge, IR, BLE gateways...
*Running on a computer*
If you want to use the BLE decoding capabilities of OpenMQTTGateway with a Raspberry Pi, Windows or Unix PC you can now leverage [Theengs Gateway](https://theengs.github.io/gateway/).
* [List of compatible components to build your gateway](https://compatible.openmqttgateway.com/index.php/parts/), DHT, RF, IR emitters and receivers...
* [Parts to build your gateway](https://docs.openmqttgateway.com/prerequisites/parts.html), DHT, RF, IR emitters and receivers...
## Compatible controllers, saas or software
@@ -82,7 +69,6 @@ For Questions or Support please don't open an issue, first go to the [docs](http
If you like the project and/or used it please consider supporting it! It can be done in different ways:
* Helping other users in the [community](https://community.openmqttgateway.com)
* [Contribute](development) to the [code](https://github.com/1technophile/OpenMQTTGateway) or the [documentation](https://docs.openmqttgateway.com)
* Buy devices, boards or parts from the [compatible web site](https://compatible.openmqttgateway.com), the devices and parts linked use affiliated links.
* Donate or sponsor the project [developers](https://github.com/1technophile/OpenMQTTGateway/graphs/contributors)
* Make a video or a blog article about what you have done with [OpenMQTTGateway](https://docs.openmqttgateway.com) and share it to the [community](https://community.openmqttgateway.com)
+41
View File
@@ -0,0 +1,41 @@
// docs/.vitepress/commonConfig.mts
// Adapter that loads the shared Theengs commonConfig.js (CommonJS) downloaded
// by CI from https://www.theengs.io/commonConfig.js, and re-exports the nav
// and head arrays in a format VitePress can consume.
//
// This keeps theengs.io as the single source of truth for nav/head until all
// Theengs docs sites are migrated to VitePress.
import { createRequire } from 'module'
import { existsSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
const commonConfigPath = resolve(__dirname, 'public/commonConfig.js')
let sharedConfig: any = null
if (existsSync(commonConfigPath)) {
sharedConfig = require(commonConfigPath)
} else {
console.warn(
'commonConfig.js not found in .vitepress/public/. ' +
'Using fallback nav/head. Run CI or download from https://www.theengs.io/commonConfig.js'
)
}
// Re-export nav from shared config, or use a minimal fallback
export const commonNav = sharedConfig?.themeConfig?.nav ?? [
{ text: 'Home', link: 'https://www.theengs.io' },
{ text: 'Community', link: 'https://community.openmqttgateway.com' },
{ text: 'Shop', link: 'https://shop.theengs.io/' }
]
// Re-export head from shared config, or use a minimal fallback
export const commonHead = sharedConfig?.head ?? [
['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
['meta', { name: 'theme-color', content: '#3eaf7c' }]
]
@@ -0,0 +1,926 @@
<template>
<div class="board-environment-list">
<div v-if="loading" class="custom-block tip">
<p class="custom-block-title">
<span class="loading-spinner" aria-label="Loading"></span>
Loading supported environments...
</p>
</div>
<div v-if="error" class="custom-block danger">
<p class="custom-block-title">Error</p>
<p>{{ error }}</p>
</div>
<div v-if="!loading && !error">
<!-- Guided picker -->
<div class="wizard">
<span class="wizard-title">Help me choose</span>
<div class="wizard-row">
<span class="filter-group__label">I want to bridge:</span>
<div class="filter-chips">
<button
v-for="b in bridgeOptions"
:key="b.key"
:class="['filter-chip', { 'filter-chip--active': wizardBridge === b.key }]"
@click="wizardBridge = wizardBridge === b.key ? null : b.key">
{{ b.label }}
</button>
</div>
</div>
<div class="wizard-row">
<span class="filter-group__label">My hardware:</span>
<div class="filter-chips">
<button
v-for="h in hardwareOptions"
:key="h.key"
:class="['filter-chip', { 'filter-chip--active': wizardHardware === h.key }]"
@click="wizardHardware = wizardHardware === h.key ? null : h.key">
{{ h.label }}
</button>
</div>
</div>
<div v-if="wizardBridge && recommendedBoard" class="wizard-result">
<div class="wizard-result__text">
<span>Recommended: <code>{{ recommendedBoard.environment }}</code></span>
<span v-if="recommendedBoard.description" class="wizard-result__desc" v-html="recommendedBoard.description"></span>
<span v-if="wizardHardware === 'none' && shoppingHint" class="wizard-result__desc">🛒 {{ shoppingHint }}</span>
</div>
<button class="wizard-result__cta" @click="openSelector(recommendedBoard.environment)">Flash it →</button>
</div>
<div v-else-if="wizardBridge && !recommendedBoard" class="wizard-result wizard-result--none">
No pre-built environment matches this combination<template v-if="wizardBridge === 'ble' && wizardHardware === 'esp8266'"> — Bluetooth needs an ESP32</template>. Try another hardware choice, or <a :href="buildsUrl">build from source</a>.
</div>
</div>
<!-- Search and Filter Bar -->
<div class="filter-bar">
<div class="search-box">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input
v-model="searchQuery"
type="text"
placeholder="Search by name, description, module..."
class="search-input"
aria-label="Search boards">
<button
v-if="searchQuery"
class="search-clear"
@click="searchQuery = ''"
aria-label="Clear search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div v-if="hasActiveFilters" class="filter-status">
<span>Showing {{ filteredBoards.length }} of {{ boards.length }} boards</span>
<button class="filter-clear" @click="clearFilters">Clear all filters</button>
</div>
</div>
<!-- No Results -->
<div v-if="filteredBoards.length === 0" class="no-results">
<p>No boards match your filters.</p>
<button class="filter-clear" @click="clearFilters">Clear all filters</button>
</div>
<div class="boards-grid">
<article
v-for="board in filteredBoards"
:key="board.environment"
class="board-card"
@click="openSelector(board.environment)">
<div class="board-card__image">
<span v-if="popularEnvironments.includes(board.environment)" class="popular-badge">Popular</span>
<img
:src="getBoardImageUrl(board)"
:alt="board.environment"
loading="lazy">
</div>
<div class="board-card__content">
<div class="board-card__header">
<h3 class="board-card__title">
<code>{{ board.environment }}</code>
</h3>
<span v-if="board.microcontroller" class="board-card__chip">
{{ board.microcontroller }}
</span>
</div>
<p v-if="board.description" class="board-card__description" v-html="board.description">
</p>
<!-- Modules Section with expand/collapse -->
<div v-if="Array.isArray(board.modules) && board.modules.length" class="board-card__libraries">
<div class="modules-label-row">
<span class="libraries-label">Modules:</span>
<span
v-if="board.modules.length > 2"
class="expand-icon-btn"
@click.stop="toggleModules(board.environment)"
:title="expandedModules[board.environment] ? 'Hide' : 'Show all'">
<svg :class="{rotated: expandedModules[board.environment]}" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
</span>
</div>
<Transition name="expand-fade">
<div class="libraries-badges"
:style="expandedModules[board.environment] ? '' : 'max-height: 2.2em; overflow: hidden;'">
<span
v-for="(mod, index) in board.modules"
:key="index"
class="lib-badge">
{{ mod }}
</span>
</div>
</Transition>
</div>
<!-- Libraries Section with expand/collapse -->
<div v-if="Array.isArray(board.libraries) && board.libraries.length" class="board-card__libraries">
<div class="modules-label-row">
<span class="libraries-label">Libraries:</span>
<span
v-if="board.libraries.length > 2"
class="expand-icon-btn"
@click.stop="toggleLibraries(board.environment)"
:title="expandedLibraries[board.environment] ? 'Hide' : 'Show all'">
<svg :class="{rotated: expandedLibraries[board.environment]}" width="19" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
</span>
</div>
<Transition name="expand-fade">
<div class="libraries-badges"
:style="expandedLibraries[board.environment] ? '' : 'max-height: 2.2em; overflow: hidden;'">
<span
v-for="(lib, index) in board.libraries"
:key="index"
class="lib-badge">
{{ lib }}
</span>
</div>
</Transition>
</div>
<div class="board-card__action">
<span class="action-text">More info →</span>
</div>
</div>
</article>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
interface Board {
environment: string
description?: string
microcontroller?: string
modules?: string[]
libraries?: string[]
customImg?: string
CustomImg?: string
}
const props = withDefaults(defineProps<{
boardsUrl?: string
selectorPath?: string
}>(), {
boardsUrl: '/boards-info.json',
selectorPath: '/upload/board-selector.html'
})
const boards = ref<Board[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const expandedModules = reactive<Record<string, boolean>>({})
const expandedLibraries = reactive<Record<string, boolean>>({})
const searchQuery = ref('')
const wizardBridge = ref<string | null>(null)
const wizardHardware = ref<string | null>(null)
const bridgeOptions = [
{ key: 'ble', label: 'Bluetooth devices' },
{ key: 'rf433', label: 'RF devices (433/868/915MHz)' },
{ key: 'ir', label: 'Infrared' },
{ key: 'lora', label: 'LoRa' }
]
const hardwareOptions = [
{ key: 'esp32dev', label: 'Generic ESP32' },
{ key: 'esp32c3', label: 'ESP32-C3' },
{ key: 'esp32s3', label: 'ESP32-S3' },
{ key: 'lilygo', label: 'LilyGo / Heltec / TTGO' },
{ key: 'm5', label: 'M5Stack' },
{ key: 'esp8266', label: 'ESP8266 / NodeMCU' },
{ key: 'none', label: "I don't have hardware yet" }
]
const popularEnvironments = [
'esp32dev-ble',
'theengs-bridge-v11',
'lilygo-rtl_433',
'lilygo-rtl_433-fsk',
'esp32dev-ir'
]
function buildUrl(path: string): string {
const base = import.meta.env.BASE_URL || '/'
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
const cleanPath = path.startsWith('/') ? path : `/${path}`
return `${cleanBase}${cleanPath}`
}
const resolvedBoardsUrl = computed(() => buildUrl(props.boardsUrl))
const resolvedSelectorUrl = computed(() => buildUrl(props.selectorPath))
function getMcuFamily(mcu: string | undefined): string | null {
if (!mcu) return null
const m = mcu.toLowerCase()
if (m.includes('esp32-s3') || m.includes('esp32s3') || m.includes('atoms3') || m.includes('lilygo-t3-s3')) return 'ESP32-S3'
if (m.includes('esp32-c3') || m.includes('esp32c3') || m.includes('lolin_c3') || m.includes('airm2m')) return 'ESP32-C3'
if (m.includes('esp32') || m.includes('m5st') || m.includes('heltec') || m.includes('ttgo') || m.includes('lolin32') || m.includes('pico32') || m.includes('tinypico') || m.includes('feather')) return 'ESP32'
if (m.includes('esp8') || m.includes('nodemcu')) return 'ESP8266'
return 'Other'
}
function matchesBridge(board: Board, bridge: string): boolean {
const mods = (board.modules || []).map(m => m.toLowerCase())
switch (bridge) {
case 'ble': return mods.some(m => m.includes('bt'))
case 'rf433': return mods.some(m => m.includes('rf') || m.includes('rtl_433') || m.includes('pilight'))
case 'ir': return mods.some(m => m.includes('ir'))
case 'lora': return mods.some(m => m.includes('lora'))
default: return true
}
}
function matchesHardware(board: Board, hardware: string): boolean {
const haystack = `${board.environment} ${board.microcontroller || ''}`.toLowerCase()
switch (hardware) {
case 'esp32dev': return board.environment.toLowerCase().includes('esp32dev')
case 'esp32c3': return getMcuFamily(board.microcontroller) === 'ESP32-C3'
case 'esp32s3': return getMcuFamily(board.microcontroller) === 'ESP32-S3'
case 'lilygo': return /lilygo|heltec|ttgo/.test(haystack)
case 'm5': return haystack.includes('m5')
case 'esp8266': return getMcuFamily(board.microcontroller) === 'ESP8266'
default: return true // 'none': no hardware constraint
}
}
// Preferred defaults per bridge choice, consulted before the generic
// popular-first order. All entries must be web-flashable environments.
const preferredByBridge: Record<string, string[]> = {
ble: ['esp32dev-ble', 'esp32c3-dev-c2-ble', 'esp32s3-dev-c1-ble', 'esp32-m5atom-lite', 'heltec-ble', 'lilygo-ble'],
rf433: ['lilygo-rtl_433', 'esp32dev-rtl_433', 'heltec-rtl_433', 'nodemcuv2-rf'],
ir: ['esp32dev-ir', 'nodemcuv2-ir', 'esp32-m5atom-lite'],
lora: ['ttgo-lora32-v21', 'heltec-wifi-lora-32', 'ttgo-lora32-v1', 'ttgo-t-beam']
}
const recommendedBoard = ref<Board | null>(null)
const manifestCache = new Map<string, boolean>()
// Not every environment in boards-info.json has a pre-built firmware
// manifest; only recommend ones that can actually be web-flashed.
async function hasManifest(env: string): Promise<boolean> {
const cached = manifestCache.get(env)
if (cached !== undefined) return cached
let ok = false
try {
const response = await fetch(buildUrl(`/firmware_build/${env}.manifest.json`), { method: 'HEAD' })
ok = response.ok
} catch {
ok = false
}
manifestCache.set(env, ok)
return ok
}
let recommendationToken = 0
watch([wizardBridge, wizardHardware, boards], async () => {
const token = ++recommendationToken
const bridge = wizardBridge.value
if (!bridge) {
recommendedBoard.value = null
return
}
const candidates = boards.value.filter(b =>
matchesBridge(b, bridge) &&
(!wizardHardware.value || matchesHardware(b, wizardHardware.value))
)
const preferred = preferredByBridge[bridge] || []
const ordered = [...candidates].sort((a, b) => {
const ai = preferred.indexOf(a.environment)
const bi = preferred.indexOf(b.environment)
return (ai === -1 ? preferred.length : ai) - (bi === -1 ? preferred.length : bi)
})
for (const candidate of ordered.slice(0, 8)) {
if (await hasManifest(candidate.environment)) {
if (token === recommendationToken) recommendedBoard.value = candidate
return
}
}
if (token === recommendationToken) recommendedBoard.value = null
})
const shoppingHint = computed<string>(() => {
switch (wizardBridge.value) {
case 'ble':
case 'ir': return 'Any ESP32 development board will do.'
case 'rf433': return 'A LILYGO LoRa32 or Heltec LoRa V2 board matching your devices\' frequency (433/868/915MHz) needs no soldering.'
case 'lora': return 'Get a LILYGO LoRa32 or Heltec LoRa board matching the frequency used in your region.'
default: return ''
}
})
const buildsUrl = computed(() => buildUrl('/upload/builds.html'))
const hasActiveFilters = computed(() =>
searchQuery.value || wizardBridge.value || wizardHardware.value
)
const filteredBoards = computed<Board[]>(() =>
boards.value.filter(board => {
if (wizardBridge.value && !matchesBridge(board, wizardBridge.value)) return false
if (wizardHardware.value && wizardHardware.value !== 'none' && !matchesHardware(board, wizardHardware.value)) return false
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase()
const haystack = [
board.environment,
board.description || '',
board.microcontroller || '',
(board.modules || []).join(' ')
].join(' ').toLowerCase()
if (!haystack.includes(q)) return false
}
return true
})
)
function clearFilters() {
searchQuery.value = ''
wizardBridge.value = null
wizardHardware.value = null
}
async function loadBoards() {
loading.value = true
error.value = null
try {
const response = await fetch(resolvedBoardsUrl.value)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
if (!Array.isArray(data)) {
throw new Error('boards-info.json must be an array')
}
boards.value = data
.filter((board: Board) => board && typeof board.environment === 'string')
.sort((a: Board, b: Board) => {
const aPopular = popularEnvironments.includes(a.environment)
const bPopular = popularEnvironments.includes(b.environment)
if (aPopular && !bPopular) return -1
if (!aPopular && bPopular) return 1
return a.environment.localeCompare(b.environment)
})
} catch (err: unknown) {
console.error('Failed to load boards-info:', err)
error.value = err instanceof Error ? err.message : 'Unable to load board information'
} finally {
loading.value = false
}
}
function toggleModules(env: string) {
expandedModules[env] = !expandedModules[env]
}
function toggleLibraries(env: string) {
expandedLibraries[env] = !expandedLibraries[env]
}
function openSelector(environment: string) {
if (!environment) return
const url = `${resolvedSelectorUrl.value}?env=${encodeURIComponent(environment)}`
window.location.href = url
}
function getBoardImageUrl(board: Board): string {
const customImg = board.customImg || board.CustomImg
if (customImg) {
if (customImg.startsWith('http')) {
return customImg
}
return buildUrl(customImg)
}
return buildUrl('/img/microcontroller.gif')
}
onMounted(() => {
// Preset the guided picker from query parameters, e.g. ?bridge=ble&hardware=esp32dev
const params = new URLSearchParams(window.location.search)
const bridgeParam = params.get('bridge')
if (bridgeParam && bridgeOptions.some(b => b.key === bridgeParam)) {
wizardBridge.value = bridgeParam
}
const hardwareParam = params.get('hardware')
if (hardwareParam && hardwareOptions.some(h => h.key === hardwareParam)) {
wizardHardware.value = hardwareParam
}
loadBoards()
})
</script>
<style scoped>
.board-environment-list {
margin: 2rem 0;
}
/* Guided picker */
.wizard {
display: flex;
flex-direction: column;
gap: 10px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
margin-bottom: 1.25rem;
}
.wizard-title {
font-weight: 600;
font-size: 0.95rem;
}
.wizard-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.wizard-result {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 12px;
font-size: 0.9rem;
}
.wizard-result__text {
display: flex;
flex-direction: column;
gap: 2px;
}
.wizard-result__desc {
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.wizard-result__cta {
background: var(--vp-c-brand-1);
color: var(--vp-c-white, #fff);
border: none;
border-radius: 20px;
padding: 8px 18px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s ease;
}
.wizard-result__cta:hover {
background: var(--vp-c-brand-2);
}
.wizard-result--none {
display: block;
color: var(--vp-c-text-2);
}
.wizard-result--none a {
color: var(--vp-c-brand-1);
}
/* Search and Filter Bar */
.filter-bar {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.search-box {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: var(--vp-c-text-2);
pointer-events: none;
}
.search-input {
width: 100%;
padding: 0.6rem 2.2rem 0.6rem 2.4rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 0.9rem;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
transition: border-color 0.2s;
}
.search-input:focus {
outline: none;
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.15);
}
.search-input::placeholder {
color: var(--vp-c-text-3);
}
.search-clear {
position: absolute;
right: 8px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--vp-c-text-2);
border-radius: 50%;
display: flex;
align-items: center;
}
.search-clear:hover {
background: var(--vp-c-bg-soft);
}
.filter-groups {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group__label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.filter-chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.filter-chip {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
padding: 0.25rem 0.7rem;
border-radius: 16px;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.filter-chip:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.filter-chip--active {
background: var(--vp-c-brand-1);
color: #ffffff;
border-color: var(--vp-c-brand-1);
}
.filter-chip--active:hover {
color: #ffffff;
opacity: 0.9;
}
.filter-status {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.filter-clear {
background: none;
border: none;
color: var(--vp-c-brand-1);
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
padding: 0;
}
.filter-clear:hover {
text-decoration: underline;
}
.no-results {
text-align: center;
padding: 3rem 1rem;
color: var(--vp-c-text-2);
}
.no-results p {
margin-bottom: 1rem;
}
/* Grid Layout */
.boards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1.5rem;
}
/* Card Design */
.board-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.board-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-color: var(--vp-c-brand-1);
}
/* Image Section */
.board-card__image {
background: var(--vp-c-bg-soft);
padding: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
border-bottom: 1px solid var(--vp-c-divider);
position: relative;
}
.popular-badge {
position: absolute;
top: 8px;
right: 8px;
background: #ff9800;
color: #ffffff;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
.board-card__image img {
max-width: 100px;
max-height: 100px;
object-fit: contain;
}
/* Content Section */
.board-card__content {
padding: 1.25rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Header */
.board-card__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.75rem;
margin-bottom: 0.25rem;
}
.board-card__title {
margin: 0;
font-size: 1rem;
font-weight: 600;
line-height: 1.4;
flex: 1;
}
.board-card__title code {
background: transparent;
color: var(--vp-c-brand-1);
font-size: 0.9em;
padding: 0;
font-weight: 600;
}
.board-card__chip {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
padding: 0.3rem 0.8rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
border: 1px solid var(--vp-c-divider);
}
/* Description */
.board-card__description {
margin: 0;
font-size: 0.9rem;
line-height: 1.5;
color: var(--vp-c-text-2);
}
/* Libraries Section */
.board-card__libraries {
margin-top: 0.5rem;
padding-top: 0.25rem;
border-top: 1px solid var(--vp-c-divider);
}
.modules-label-row {
display: flex;
align-items: center;
gap: 0.2em;
margin-bottom: 0.5rem;
}
.libraries-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
display: inline-block;
}
.libraries-badges {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.lib-badge {
display: inline-block;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid var(--vp-c-divider);
}
/* Action Footer */
.board-card__action {
padding-top: 0.75rem;
margin-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.action-text {
color: var(--vp-c-brand-1);
font-size: 0.85rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.25rem;
}
.board-card:hover .action-text {
text-decoration: underline;
}
/* Loading Spinner */
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
min-width: 16px;
border: 2px solid #cfd8dc;
border-top-color: var(--vp-c-brand-1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 0.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive */
@media (max-width: 768px) {
.filter-groups {
flex-direction: column;
gap: 0.5rem;
}
.filter-group {
flex-wrap: wrap;
}
.boards-grid {
grid-template-columns: 1fr;
}
.board-card__image {
min-height: 100px;
padding: 1rem;
}
.board-card__image img {
max-width: 80px;
max-height: 80px;
}
}
@media (max-width: 480px) {
.board-card__header {
flex-direction: column;
align-items: flex-start;
}
.board-card__chip {
align-self: flex-start;
}
}
/* Espansione animata per badge */
.expand-fade-enter-active, .expand-fade-leave-active {
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
}
.expand-fade-enter-from, .expand-fade-leave-to {
opacity: 0;
max-height: 0;
}
.expand-fade-enter-to, .expand-fade-leave-from {
opacity: 1;
max-height: 500px;
}
.expand-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--vp-c-brand-1);
cursor: pointer;
margin-top: 0.3em;
margin-left: 0.2em;
padding: 0.1em 0.2em;
border-radius: 50%;
transition: background 0.2s;
}
.expand-icon-btn:hover {
background: rgba(62, 175, 124, 0.08);
}
.expand-icon-btn svg {
transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
vertical-align: middle;
}
.expand-icon-btn svg.rotated {
transform: rotate(90deg);
}
</style>
@@ -0,0 +1,730 @@
<template>
<div class="flash-selector">
<!-- Loading State -->
<div v-if="loading" class="custom-block tip">
<p class="custom-block-title">
<span class="loading-spinner"></span>
Loading environments...
</p>
</div>
<!-- Error State -->
<div v-if="error" class="custom-block danger">
<p class="custom-block-title">Error</p>
<p>{{ error }}</p>
</div>
<!-- Board Details (when selected) -->
<Transition name="fade">
<div v-if="currentEnvironment && !loading" class="board-details">
<!-- Hero Section - Clean and Focused -->
<div class="board-hero">
<div class="board-hero__content">
<!-- Inline Board Switcher -->
<h2>
<div class="board-switcher">
<label for="envSelect" class="switcher-label">Environment:</label>
<div class="switcher-select-wrapper">
<select
id="envSelect"
v-model="selectedEnvironment"
class="switcher-select">
<option
v-for="env in environments"
:key="env.environment"
:value="env.environment">
{{ env.environment }}
</option>
</select>
<span class="switcher-icon">⚙️</span>
</div>
</div>
</h2>
<div v-if="currentEnvironment.microcontroller" class="board-hero__meta">
<span class="meta-label">MCU:</span>
<span class="meta-value">{{ currentEnvironment.microcontroller }}</span>
</div>
<p v-if="currentEnvironment.description" class="board-hero__description">
<span class="meta-label">Description:</span> <span v-html="currentEnvironment.description"></span>
</p>
<!-- Primary Action -->
<div class="hero-action">
<esp-web-install-button
v-if="manifestUrl"
:manifest="manifestUrl"
aria-label="Flash firmware to device">
</esp-web-install-button>
<button v-else class="connect-button connect-button--disabled" disabled>
CONNECT
</button>
</div>
</div>
<!-- Image Optional -->
<div v-if="boardImageUrl && !imageError" class="board-hero__image">
<img
:src="boardImageUrl"
:alt="currentEnvironment.environment"
loading="lazy"
@error="handleImageError">
</div>
</div>
<!-- Technical Details - Collapsible -->
<details class="tech-details" open>
<summary class="tech-details__summary">
<span class="tech-details__icon">📋</span>
<span class="tech-details__title">Technical Information</span>
<span class="tech-details__toggle">▼</span>
</summary>
<div class="tech-details__content">
<div class="tech-grid">
<!-- Libraries -->
<div v-if="currentEnvironment.libraries && currentEnvironment.libraries.length" class="tech-section">
<h4 class="tech-section__title">Libraries</h4>
<div class="tech-section__content">
<div class="chip-group">
<span v-for="(lib, index) in currentEnvironment.libraries" :key="index" class="chip">
{{ lib }}
</span>
</div>
</div>
</div>
<!-- Gateway Modules -->
<div v-if="currentEnvironment.modules && currentEnvironment.modules.length" class="tech-section">
<h4 class="tech-section__title">Gateway Modules</h4>
<div class="tech-section__content">
<div class="chip-group">
<span v-for="(mod, index) in currentEnvironment.modules" :key="index" class="chip">
{{ mod }}
</span>
</div>
</div>
</div>
<!-- Hardware -->
<div v-if="currentEnvironment.hardware" class="tech-section">
<h4 class="tech-section__title">Hardware</h4>
<div class="tech-section__content" >
<span class="chip" v-html="currentEnvironment.hardware"></span>
</div>
</div>
<!-- Partitions -->
<div v-if="currentEnvironment.partitions" class="tech-section">
<h4 class="tech-section__title">Partitions</h4>
<div class="tech-section__content">
<code class="code-inline">{{ currentEnvironment.partitions }}</code>
</div>
</div>
<!-- Build Options -->
<div v-if="currentEnvironment.options" class="tech-section tech-section--full">
<h4 class="tech-section__title">Build Options</h4>
<div class="tech-section__content">
<pre class="code-block">{{ formatOptions(currentEnvironment.options) }}</pre>
</div>
</div>
</div>
</div>
</details>
<!-- Post-flash next steps -->
<div class="next-steps">
<h3 class="next-steps__title">✅ After flashing</h3>
<ol class="next-steps__list">
<li>Keep the board powered — it starts a WiFi access point named <code>OpenMQTTGateway</code> or starting with <code>OMG_</code>.</li>
<li>From your phone or computer, connect to that WiFi network and follow the <a :href="portalUrl">configuration portal guide</a> to enter your WiFi and MQTT broker credentials.</li>
<li>The gateway restarts, joins your network and connects to your broker — <a :href="verifyUrl">check that it works</a>.</li>
</ol>
</div>
</div>
</Transition>
<!-- Board Detail not selected message -->
<Transition name="fade">
<div v-if="!currentEnvironment && !loading && !error" class="custom-block info">
<p class="custom-block-title">Need to select one environment...</p>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
interface Environment {
environment: string
description?: string
microcontroller?: string
modules?: string[]
libraries?: string[]
hardware?: string
partitions?: string
options?: string | string[]
customImg?: string
CustomImg?: string
}
const props = withDefaults(defineProps<{
boardsUrl?: string
firmwareBaseUrl?: string
}>(), {
boardsUrl: '/boards-info.json',
firmwareBaseUrl: '/firmware_build/'
})
const environments = ref<Environment[]>([])
const selectedEnvironment = ref<string>('')
const loading = ref<boolean>(true)
const error = ref<string | null>(null)
const imageError = ref<boolean>(false)
const currentEnvironment = computed<Environment | undefined>(() => {
return environments.value.find(env => env.environment === selectedEnvironment.value)
})
const resolvedBoardsUrl = computed<string>(() => buildUrl(props.boardsUrl))
const resolvedFirmwareBaseUrl = computed<string>(() => buildUrl(props.firmwareBaseUrl))
const manifestUrl = computed<string | null>(() => {
if (!selectedEnvironment.value) return null
return `${resolvedFirmwareBaseUrl.value}${selectedEnvironment.value}.manifest.json`
})
const portalUrl = computed<string>(() => buildUrl('/upload/portal.html'))
const verifyUrl = computed<string>(() => buildUrl('/getting-started.html#step-4-check-that-it-works'))
const boardImageUrl = computed<string | null>(() => {
if (!currentEnvironment.value) return null
const customImg = currentEnvironment.value.customImg || currentEnvironment.value.CustomImg
if (customImg && !imageError.value) {
if (customImg.startsWith('http')) {
return customImg
}
return buildUrl(customImg)
}
return buildUrl('/img/microcontroller.gif')
})
watch(selectedEnvironment, () => {
imageError.value = false
error.value = null
})
onMounted(() => {
const script = document.createElement('script')
script.type = 'module'
script.src = 'https://unpkg.com/esp-web-tools@10/dist/web/install-button.js?module'
document.head.appendChild(script)
loadEnvironments()
})
function buildUrl(path: string): string {
const base = import.meta.env.BASE_URL || '/'
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
const cleanPath = path.startsWith('/') ? path : `/${path}`
return `${cleanBase}${cleanPath}`
}
function getQueryParam(param: string): string | null {
const params = new URLSearchParams(window.location.search)
return params.get(param)
}
async function loadEnvironments(): Promise<void> {
loading.value = true
error.value = null
try {
const response = await fetch(resolvedBoardsUrl.value)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
if (!Array.isArray(data)) {
throw new Error('Invalid data format: expected an array')
}
const validEnvironments = data.filter((env: unknown) =>
env &&
typeof env === 'object' &&
(env as Environment).environment &&
typeof (env as Environment).environment === 'string'
) as Environment[]
if (validEnvironments.length === 0) {
throw new Error('No valid environments found in data')
}
environments.value = validEnvironments
const envParam = getQueryParam('env')
if (envParam) {
const matchingEnv = validEnvironments.find(env => env.environment === envParam)
if (matchingEnv) {
selectedEnvironment.value = envParam
} else {
console.warn(`Environment '${envParam}' not found in available environments`)
}
}
} catch (err) {
console.error('Failed to load environments:', err)
error.value = (err instanceof Error ? err.message : null) || 'Failed to load environments'
} finally {
loading.value = false
}
}
function formatOptions(optionsText: string | string[]): string {
if (!optionsText) return ''
let optionsTextMap: string[] = []
if (typeof optionsText === 'string') {
optionsTextMap = optionsText.split('\n')
} else if (Array.isArray(optionsText)) {
optionsTextMap = optionsText
}
return optionsTextMap.map(line => line.trim())
.filter(line => line.length > 0)
.join('\n')
}
function handleImageError(): void {
imageError.value = true
}
</script>
<style scoped>
/* Post-flash next steps */
.next-steps {
margin-top: 16px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px 24px;
}
.next-steps__title {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
}
.next-steps__list {
margin: 0;
padding-left: 20px;
}
.next-steps__list li {
margin: 6px 0;
line-height: 1.6;
}
.next-steps__list a {
color: var(--vp-c-brand-1);
font-weight: 500;
}
/* Main Container */
.flash-selector {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
/* Board Details Container */
.board-details {
margin-top: 2rem;
}
/* Hero Section - Material Design 3 */
.board-hero {
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
padding: 32px;
margin-bottom: 16px;
box-shadow: 0 1px 2px 0 var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
}
.board-hero__content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
.board-hero__title {
margin: 0 0 8px 0;
font-size: 1.75rem;
font-weight: 400;
color: var(--vp-c-brand-1);
font-family: 'Monaco', 'Courier New', monospace;
line-height: 1.2;
letter-spacing: -0.01em;
}
/* Board Switcher - Material Chip Style */
.board-switcher {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
min-width: 0;
}
.switcher-label {
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.1em;
color: var(--vp-c-text-2);
}
.switcher-select-wrapper {
position: relative;
display: inline-flex;
align-items: center;
min-width: 0;
max-width: 100%;
}
.switcher-select {
appearance: none;
max-width: 100%;
text-overflow: ellipsis;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: none;
padding: 6px 32px 6px 12px;
border-radius: 16px;
font-size: 0.875rem;
font-weight: 500;
font-family: 'Monaco', 'Courier New', monospace;
cursor: pointer;
transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
min-height: 32px;
min-width: 280px;
width: auto;
}
.switcher-select:hover {
background: var(--vp-c-bg-soft);
}
.switcher-select:focus {
outline: 2px solid var(--vp-c-brand-1);
outline-offset: 2px;
}
.switcher-icon {
position: absolute;
right: 8px;
pointer-events: none;
font-size: 1rem;
opacity: 0.6;
}
.board-hero__meta {
display: flex;
align-items: baseline;
gap: 8px;
margin: 0;
}
.meta-label {
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.1em;
color: var(--vp-c-text-2);
}
.meta-value {
font-size: 0.875rem;
font-weight: 500;
color: var(--vp-c-text-1);
font-family: 'Monaco', 'Courier New', monospace;
}
.board-hero__description {
margin: 0;
font-size: 1rem;
line-height: 1.5;
color: var(--vp-c-text-1);
max-width: 600px;
}
.board-hero__image {
display: flex;
align-items: flex-start;
justify-content: center;
min-width: 0;
max-width: 200px;
}
.board-hero__image img {
max-width: 100%;
max-height: 180px;
object-fit: contain;
}
/* Primary Action - Material Filled Button */
.hero-action {
margin-top: 8px;
}
.connect-button {
background: #1976d2;
color: #ffffff;
border: none;
padding: 10px 24px;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15);
text-transform: uppercase;
letter-spacing: 0.125em;
height: 40px;
min-width: 120px;
}
.connect-button:hover:not(:disabled) {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15);
}
.connect-button:active:not(:disabled) {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15);
}
.connect-button--disabled {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
cursor: not-allowed;
box-shadow: none;
}
/* Technical Details - Material Surface */
.tech-details {
background: var(--vp-c-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 2px 0 var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
}
.tech-details__summary {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
cursor: pointer;
user-select: none;
background: var(--vp-c-bg-alt);
transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
list-style: none;
}
.tech-details__summary::-webkit-details-marker {
display: none;
}
.tech-details__summary:hover {
background: var(--vp-c-bg-soft);
}
.tech-details__icon {
font-size: 1.25rem;
opacity: 0.6;
}
.tech-details__title {
font-size: 1rem;
font-weight: 500;
color: var(--vp-c-text-1);
flex: 1;
letter-spacing: 0.01em;
}
.tech-details__toggle {
font-size: 0.875rem;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0.6;
}
.tech-details[open] .tech-details__toggle {
transform: rotate(180deg);
}
.tech-details__content {
padding: 24px;
}
/* Tech Grid */
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
.tech-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.tech-section--full {
grid-column: 1 / -1;
}
.tech-section__title {
margin: 0;
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 500;
letter-spacing: 0.1em;
color: var(--vp-c-text-2);
}
.tech-section__content {
font-size: 0.875rem;
line-height: 1.5;
color: var(--vp-c-text-1);
}
/* Chips - Material Design */
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
padding: 6px 12px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 400;
border: none;
height: 32px;
display: inline-flex;
align-items: center;
}
/* Code Styles - Material Surface */
.code-inline {
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875rem;
font-family: 'Monaco', 'Courier New', monospace;
color: var(--vp-c-text-1);
}
.code-block {
background: var(--vp-c-bg-soft);
padding: 16px;
border-radius: 8px;
font-size: 0.8125rem;
line-height: 1.5;
overflow-x: auto;
margin: 0;
border: 1px solid var(--vp-c-divider);
white-space: pre-wrap;
word-break: break-word;
font-family: 'Monaco', 'Courier New', monospace;
color: var(--vp-c-text-1);
}
/* Loading Spinner */
.loading-spinner {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(62, 175, 124, 0.3);
border-top-color: var(--vp-c-brand-1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 0.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Fade Transition */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* Accessibility */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/* Responsive Design */
@media (max-width: 968px) {
.board-hero {
grid-template-columns: 1fr;
}
.board-hero__image {
order: -1;
max-width: 150px;
margin: 0 auto;
}
.tech-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.selector-card {
padding: 1.5rem;
}
.board-hero {
padding: 1.5rem;
}
.board-hero__title {
font-size: 1.5rem;
}
.connect-button {
width: 100%;
padding: 0.9rem 2rem;
}
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<template>
<div class="omg-pipeline-wrap">
<div class="omg-pipeline">
<div class="omg-node">
<span class="omg-node-icon">🌡️</span>
<strong>Your devices</strong>
<span class="omg-node-sub">BLE · RF · IR · LoRa</span>
</div>
<span class="omg-arrow">⇄</span>
<div class="omg-node omg-node-brand">
<span class="omg-node-icon">📟</span>
<strong>OpenMQTTGateway</strong>
<span class="omg-node-sub">ESP32 / ESP8266</span>
</div>
<span class="omg-arrow">⇄</span>
<div class="omg-node">
<span class="omg-node-icon">🔀</span>
<strong>MQTT broker</strong>
<span class="omg-node-sub">Mosquitto...</span>
</div>
<span class="omg-arrow">⇄</span>
<div class="omg-node">
<span class="omg-node-icon">🏠</span>
<strong>Your controller</strong>
<span class="omg-node-sub">Home Assistant, OpenHAB...</span>
</div>
</div>
</div>
</template>
<style scoped>
.omg-pipeline-wrap {
container-type: inline-size;
margin: 24px 0;
}
.omg-pipeline {
display: flex;
align-items: stretch;
gap: 8px;
}
.omg-node {
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 4px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px 10px;
}
.omg-node-brand {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 1px var(--vp-c-brand-1);
}
.omg-node-icon {
font-size: 28px;
}
.omg-node strong {
font-size: 15px;
line-height: 1.3;
}
.omg-node-sub {
color: var(--vp-c-text-2);
font-size: 13px;
}
.omg-arrow {
align-self: center;
flex-shrink: 0;
color: var(--vp-c-brand-1);
font-size: 20px;
font-weight: 700;
}
/* Narrow containers (doc column, tablets): tighten the nodes, keep one row */
@container (max-width: 760px) {
.omg-pipeline {
gap: 6px;
}
.omg-node {
padding: 12px 6px;
}
.omg-node strong {
font-size: 13px;
}
.omg-node-sub {
font-size: 11px;
}
.omg-node-icon {
font-size: 22px;
}
.omg-arrow {
font-size: 16px;
}
}
/* Narrow (phones, small tablets): stack vertically. Below ~680px the four nodes
no longer leave room for the longest label ("OpenMQTTGateway"), which spilled
out of its box and over the arrows. */
@container (max-width: 680px) {
.omg-pipeline {
flex-direction: column;
}
.omg-arrow {
transform: rotate(90deg);
}
.omg-node strong {
font-size: 15px;
}
.omg-node-sub {
font-size: 13px;
}
}
</style>
+147
View File
@@ -0,0 +1,147 @@
// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { commonNav, commonHead } from './commonConfig.mts'
const __dirname = dirname(fileURLToPath(import.meta.url))
// Load metadata with override support
import defaults from './defaults.json'
let meta = { ...defaults }
try {
const overrides = await import('./meta.json')
meta = { ...meta, ...overrides.default }
} catch {
console.warn('meta.json not found or not valid. Using default configuration.')
}
export default defineConfig({
title: `${meta.title} - ${meta.version}`,
description: 'One gateway, many technologies: MQTT gateway for ESP8266 or ESP32 with bidirectional 433mhz/315mhz/868mhz/915mhz RF, Infrared communications, BLE, LoRa, beacons detection, mi flora / mi jia / LYWSD02/ Mi Scale compatibility, SMS & LORA.',
base: meta.url_prefix as '/' | `/${string}/`,
outDir: resolve(__dirname, '../../', meta.dest),
// Internal working documents, not part of the published site
srcExclude: ['superpowers/**'],
head: commonHead,
ignoreDeadLinks: [
/localhost/,
/LoraTemperature/
],
lastUpdated: true,
markdown: {
lineNumbers: true
},
themeConfig: {
siteTitle: 'OpenMQTTGateway',
// @ts-expect-error custom fields consumed by index.md to render the dev-version banner
mode: meta.mode,
// @ts-expect-error
version: meta.version,
// @ts-expect-error
repo: meta.theme_config_repo,
nav: commonNav,
socialLinks: [
{ icon: 'github', link: `https://github.com/${meta.theme_config_repo}` }
],
editLink: {
pattern: `https://github.com/${meta.theme_config_repo}/edit/development/docs/:path`
},
sidebar: [
{ text: 'What is it for', link: '/' },
{ text: 'Getting Started', link: '/getting-started' },
{
text: '1 - Prerequisites',
collapsed: true,
items: [
{ text: 'Devices', link: '/prerequisites/devices' },
{ text: 'Board', link: '/prerequisites/board' },
{ text: 'Parts', link: '/prerequisites/parts' },
{ text: 'Broker', link: '/prerequisites/broker' },
{ text: 'Controller', link: '/prerequisites/controller' }
]
},
{
text: '2 - Upload',
collapsed: true,
link: '/upload/',
items: [
{ text: '1. Web installer (easiest)', link: '/upload/web-install' },
{ text: '2. Ready-to-go binaries', link: '/upload/binaries' },
{ text: '3. Build from source', link: '/upload/builds' },
{ text: 'Configuration portal', link: '/upload/portal' },
{ text: 'Portal HTTP API', link: '/upload/portal-api' },
{ text: 'Advanced Configuration', link: '/upload/advanced-configuration' },
{ text: 'Troubleshoot', link: '/upload/troubleshoot' }
]
},
{
text: '3 - Hardware setup',
collapsed: true,
items: [
{ text: 'RF', link: '/setitup/rf' },
{ text: 'BLE', link: '/setitup/ble' },
{ text: 'IR', link: '/setitup/ir' },
{ text: 'LoRa', link: '/setitup/lora' },
{ text: 'GSM', link: '/setitup/gsm' },
{ text: 'Serial', link: '/setitup/serial' },
{ text: 'Sensors', link: '/setitup/sensors' },
{ text: 'Actuators', link: '/setitup/actuators' }
]
},
{
text: '4 - Use',
collapsed: true,
items: [
{ text: 'RF', link: '/use/rf' },
{ text: 'BLE', link: '/use/ble' },
{ text: 'IR', link: '/use/ir' },
{ text: 'LoRa', link: '/use/lora' },
{ text: 'GSM', link: '/use/gsm' },
{ text: 'Serial', link: '/use/serial' },
{ text: 'RFM69', link: '/use/rfm69' },
{ text: 'Sensors', link: '/use/sensors' },
{ text: 'Actuators', link: '/use/actuators' },
{ text: 'Boards', link: '/use/boards' },
{ text: 'Displays', link: '/use/displays' },
{ text: 'Gateway', link: '/use/gateway' },
{ text: 'WebUI', link: '/use/webui' }
]
},
{
text: '5 - Integrate',
collapsed: true,
items: [
{ text: 'OpenHAB 2', link: '/integrate/openhab2' },
{ text: 'OpenHAB 3', link: '/integrate/openhab3' },
{ text: 'Home Assistant', link: '/integrate/home_assistant' },
{ text: 'Node-RED', link: '/integrate/node_red' },
{ text: 'AWS IoT', link: '/integrate/aws_iot' },
{ text: 'Jeedom', link: '/integrate/jeedom' }
]
},
{
text: '6 - Participate',
collapsed: true,
link: '/participate/',
items: [
{ text: 'Contributor Quick Start', link: '/participate/quick_start' },
{ text: 'Support', link: '/participate/support' },
{ text: 'Development', link: '/participate/development' },
{ text: 'Adding Protocols', link: '/participate/adding-protocols' },
{ text: 'Community', link: '/participate/community' },
{ text: 'License', link: meta.url_license_file }
]
}
],
search: {
provider: 'local'
}
},
sitemap: {
hostname: meta.hostname
},
vite: {
publicDir: '.vitepress/public'
}
})
+12
View File
@@ -0,0 +1,12 @@
{
"title": "Theengs OpenMQTTGateway",
"version": "edge",
"url_prefix": "/",
"dest": "generated/site",
"mode": "prod",
"url_license_file": "https://github.com/1technophile/OpenMQTTGateway/blob/development/LICENSE.txt",
"url_community_forum": "https://community.openmqttgateway.com",
"hostname": "https://docs.openmqttgateway.com",
"repo": "1technophile/OpenMQTTGateway",
"theme_config_repo": "1technophile/OpenMQTTGateway"
}
File renamed without changes.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

File renamed without changes.
+123
View File
@@ -0,0 +1,123 @@
/* docs/.vitepress/theme/custom.css */
/* Prevent long site title from overlapping sidebar/nav */
.VPNavBarTitle .title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The shared Theengs nav has seven top-level entries, and the default theme
switches from the hamburger to the full nav bar at 768px -- far too early for
this menu. Between 768px and ~1180px the entries overflowed: Shop, Sponsor,
the appearance switch and the GitHub link ended up clipped or off-screen, and
the page picked up a horizontal scrollbar. Keep the compact nav (hamburger +
full-screen menu, which already lists every entry plus the appearance switch
and social links) until the full menu genuinely fits. */
@media (min-width: 768px) and (max-width: 1199px) {
.VPNav .VPNavBar .VPNavBarMenu.menu,
.VPNav .VPNavBar .VPNavBarExtra.extra {
display: none;
}
.VPNav .VPNavBar .VPNavBarHamburger.hamburger {
display: flex;
}
header.VPNav .VPNavScreen {
display: block;
}
}
/* Widen the entire content column on pages without aside (board/flash pages) */
.VPDoc:not(.has-aside) .content-container,
.VPDoc:not(.has-aside) .container {
max-width: 100% !important;
}
.VPDoc:not(.has-aside) .vp-doc {
max-width: 100% !important;
}
/* Enlarge the home hero image beyond the theme's ~320px default cap */
.VPHome .VPHero .image-container {
transform: none;
width: 100%;
height: auto;
margin: 0 auto;
}
.VPHome .VPHero .image-bg {
display: none;
}
.VPHome .VPHero .image-src {
position: static;
transform: none;
width: 100%;
max-width: 420px;
max-height: none;
}
/* Below the desktop breakpoint the default theme stacks the image on top of the
hero text and pulls it with negative margins (-76px -24px -48px), which only
works because the image is normally absolutely positioned inside a fixed
320px box. Since we let it flow, those margins made it bleed off-screen and
collide with the hero title -- reset them. */
@media (max-width: 959px) {
.VPHome .VPHero .image {
margin: 0 0 16px;
}
}
/* On phones the stacked image is a wall of unreadable logos that pushes the
title, tagline and call-to-action buttons below the fold. Drop it and let the
hero lead with the text. */
@media (max-width: 639px) {
.VPHome .VPHero .image {
display: none;
}
}
@media (min-width: 960px) {
.VPHome .VPHero .image-src {
max-width: 520px;
}
}
/* Indented (non-fenced) markdown code blocks render as a bare <pre>; the default
theme only makes code scroll inside its language-* wrapper, so long lines such
as the esptool.py commands pushed the whole page sideways on phones. */
.vp-doc pre {
overflow-x: auto;
}
/* Embeds are written as raw HTML in markdown with fixed width/height attributes
(e.g. <iframe width="560" height="315"> for YouTube), which pushes the whole
page sideways on phones. */
.vp-doc iframe {
max-width: 100%;
}
.vp-doc iframe[src*="youtube.com/embed"],
.vp-doc iframe[src*="youtube-nocookie.com/embed"],
.vp-doc iframe[src*="player.vimeo.com"] {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
/* Gray backdrop when ESP Web Tools install dialog is open */
ewt-install-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
+36
View File
@@ -0,0 +1,36 @@
// docs/.vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import mediumZoom from 'medium-zoom'
import { onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vitepress'
import './custom.css'
import BoardEnvironmentTable from '../components/BoardEnvironmentTable.vue'
import FlashEnvironmentSelector from '../components/FlashEnvironmentSelector.vue'
import OmgPipeline from '../components/OmgPipeline.vue'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('BoardEnvironmentTable', BoardEnvironmentTable)
app.component('FlashEnvironmentSelector', FlashEnvironmentSelector)
app.component('OmgPipeline', OmgPipeline)
},
setup() {
const route = useRoute()
const initZoom = () => {
mediumZoom('.vp-doc img:not(.no-zoom)', {
background: 'var(--vp-c-bg)'
})
}
onMounted(() => {
initZoom()
})
watch(
() => route.path,
() => nextTick(() => initZoom())
)
}
}
-117
View File
@@ -1,117 +0,0 @@
const commonConfig = require('./public/commonConfig');
module.exports = {
...commonConfig,
title: 'Theengs OpenMQTTGateway version_tag',
base: '/',
description: 'One gateway, many technologies: MQTT gateway for ESP8266, ESP32, Sonoff RF Bridge or Arduino with bidirectional 433mhz/315mhz/868mhz, Infrared communications, BLE, beacons detection, mi flora / mi jia / LYWSD02/ Mi Scale compatibility, SMS & LORA.',
head: [
...commonConfig.head,
['script', {type: 'module', src: 'https://unpkg.com/esp-web-tools@9.4.3/dist/web/install-button.js?module'}]
],
themeConfig: {
repo: '1technophile/OpenMQTTGateway',
docsDir: 'docs',
...commonConfig.themeConfig,
sidebar: [
['/','0 - What is it for 🏠'],
{
title: '1 - Prerequisites🧭', // required
//collapsable: true, // optional, defaults to true
sidebarDepth: 1, // optional, defaults to 1
children: [
'prerequisites/devices',
'prerequisites/board',
'prerequisites/parts',
'prerequisites/broker',
'prerequisites/controller']
},
{
title: '2 - Set it up 🔨', // required
sidebarDepth: 1, // optional, defaults to 1
children: [
'setitup/rf',
'setitup/ble',
'setitup/ir',
'setitup/lora',
'setitup/gsm',
'setitup/rs232',
'setitup/sensors',
'setitup/actuators'
]
},
{
title: '3 - Upload ➡️', // required
sidebarDepth: 1, // optional, defaults to 1
children: [
'upload/web-install',
'upload/binaries',
'upload/builds',
'upload/gitpod',
'upload/portal',
'upload/advanced-configuration',
'upload/troubleshoot'
]
},
{
title: '4 - Use ✈️', // required
sidebarDepth: 1, // optional, defaults to 1
children: [
'use/rf',
'use/ble',
'use/ir',
'use/lora',
'use/gsm',
'use/rs232',
'use/rfm69',
'use/sensors',
'use/actuators',
'use/boards',
'use/displays',
'use/gateway',
'use/webui'
]
},
{
title: '5 - Integrate 🎉', // required
sidebarDepth: 1, // optional, defaults to 1
children: [
'integrate/openhab2',
'integrate/openhab3',
'integrate/home_assistant',
'integrate/node_red',
'integrate/aws_iot',
'integrate/jeedom'
]
},
{
title: '6 - Participate 💻', // required
sidebarDepth: 1, // optional, defaults to 1
children: [
'participate/support',
'participate/development',
'participate/adding-protocols',
'participate/community',
['https://github.com/1technophile/OpenMQTTGateway/blob/development/LICENSE.txt','License']
]
}
]
},
plugins: {
'sitemap': {
hostname: 'https://docs.openmqttgateway.com',
urls: [
'https://decoder.theengs.io/devices/devices.html',
'https://community.openmqttgateway.com/',
'https://shop.theengs.io/',
'https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption',
'https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna',
'https://app.theengs.io/',
'https://gateway.theengs.io/',
'https://decoder.theengs.io/',
'https://parser.theengs.io/',
'https://www.theengs.io/'
],
},
}
}
-13
View File
@@ -1,13 +0,0 @@
div.table-generated-page
main
div.content__default,
footer.page-edit,
div.page-nav
// Display full with the API pages
max-width: 65%
.sidebar
z-index 6
.navbar
z-index 6
-113
View File
@@ -1,113 +0,0 @@
OpenMQTTGateway aims to unify various technologies and protocols into a single firmware. This reduces the need for multiple physical bridges and streamlines diverse technologies under the widely-used [MQTT](http://mqtt.org/) protocol.
![Overview of the protocols and compatible controllers](./img/OpenMQTTGateway.png)
# What is an MQTT gateway or bridge ?
MQTT, short for Message Queuing Telemetry Transport, is a lightweight messaging protocol ideal for IoT devices.
An MQTT gateway or bridge plays a pivotal role in the MQTT ecosystem:
* Protocol Translation: It converts non-MQTT protocols (like LoRa or Bluetooth) into MQTT, enabling broader network communication.
* Data Aggregation: Combines data from multiple devices into single messages, optimizing network use.
* Security: Incorporates features like SSL/TLS encryption to safeguard data during transmission.
* Device Management: Handles tasks like firmware updates and remote configuration changes.
In essence, an MQTT gateway ensures smooth communication between devices and MQTT brokers, enhancing the efficiency and security of IoT systems.
# What OpenMQTTGateway can do ?
OpenMQTTGateway integrates with established technologies, such as 433mhz/315mhz protocols and infrared (IR), allowing you to upgrade and repurpose older devices. Additionally, OMG is compatible with modern technologies like Bluetooth Low Energy (BLE) and LoRa.
To have an overview of the supported PIR, door, water, temperature, smoke sensors, sirens, rings, beacons, switches & weather stations you can take a look to the
[compatible devices list](https://compatible.openmqttgateway.com/index.php/devices)
You can run OpenMQTTGateway on a wide variety of [boards](https://compatible.openmqttgateway.com/index.php/boards/), ESP32, ESP8266, Arduino MEGA, UNO (with limitation).
BLE to MQTT gateway can also run on Raspberry Pi, Windows or Unix computers thanks to [Theengs Gateway](https://theengs.github.io/gateway/).
Using MQTT, you can seamlessly integrate with home automation platforms such as OpenHAB, Home Assistant, and others, or with IoT software like Node-Red.
# Use cases
Leveraging OpenMQTTGateway with a controller allows you to:
* Monitor a garden with a Mi Flora BLE sensor and control an irrigation valve depending on the soil moisture,
* Trigger a fan depending on the temperature and humidity thanks to a Mi Jia/LYWSD03MMC BLE sensor,
* Follow your meat temperature when cooking with an Inkbird IBBQ
* Alert yourself by a controller notification if the temperature of a fridge or freezer is too high,
* Detect a beacon/smartwatch so as to trigger a special scenario when you come home,
* [Lose weight with the help of a complete log system](https://www.youtube.com/watch?v=noUROhtf0E0&t=18s), video from [@Andreas Spiess](https://www.youtube.com/channel/UCu7_D0o48KbfhpEohoP7YSQ)
* Detect opened door or windows through 433mhz or BLE and alert yourself when leaving
* Detect water leakage or smoke remotely
* Actionate a siren if something is going wrong
* Detect if your far mailbox has been opened by the postman with LoRa
* Make smart your old TV or AC system through infrared control
* Monitor vehicle tire pressure
The limit is your imagination 😀
# Functions
Under the hood, OpenMQTTGateway offers features such as:
* Deduplication
* Simple and lite API
* Strong integrations with libraries used
* Signal forward/repeat
* Wifi web portal onboarding
* Web portal configuration
* Whitelist & Blacklist management
* Secure connections
* Over the air updates
* Local or cloud, your choice
## Using OpenMQTTGateway ?
Support open-source development through sponsorship and gain exclusive access to our private forum. Your questions, issues, and feature requests will receive priority attention, plus you'll gain insider access to our roadmap.
<div style="text-align: center;">
<iframe src="https://github.com/sponsors/theengs/button" title="Sponsor Theengs" height="32" width="228" style="border: 0; border-radius: 6px;"></iframe>
</div>
## Products powered by OpenMQTTGateway
### Theengs Bridge, BLE gateway with external antenna
[Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) is a powerfull BLE to MQTT gateway for over [90 sensors](https://decoder.theengs.io/devices/devices.html). Equipped with an Ethernet port, and external antenna, ensuring an enhanced range for your BLE sensors. It supports also WiFi connectivity.
[![Theengs bridge view](./img/Theengs-Bridge-ble-gateway.png)](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna)
### Theengs Plug, BLE gateway and Smart Plug
[Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption) brings the following features:
* BLE to MQTT gateway, tens of [Bluetooth devices](https://compatible.openmqttgateway.com/index.php/devices/ble-devices/) supported thanks to Theengs Decoder library. The plug uses an ESP32 acting as a BLE to Wifi gateway to scan, decode and forward the data of the nearby sensors,
* Smart plug that can be controlled remotely,
* Energy consumption monitoring,
* Device tracker,
* Presence detection (beta),
* Local connectivity first.
[![Theengs plug view](./img/Theengs-Plug-OpenMQTTGateway.png)](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
Support the project by purchasing the [Theengs bridge](https://shop.theengs.io/products/theengs-bridge-esp32-ble-mqtt-gateway-with-ethernet-and-external-antenna) or the [Theengs plug](https://shop.theengs.io/products/theengs-plug-smart-plug-ble-gateway-and-energy-consumption)
## Media
* [Hackaday - ARDUINO LIBRARY BRINGS RTL_433 TO THE ESP32](https://hackaday.com/2023/01/13/arduino-library-brings-rtl_433-to-the-esp32)
* [CNX Software - 433 MHz is not dead! Using an ESP32 board with LoRa module to talk to 433 MHz sensors](https://www.cnx-software.com/2023/01/14/esp32-board-with-lora-433-mhz-sensors/)
* [RTL_433 PORTED TO ESP32 MICROCONTROLLERS WITH CC1101 OR SX127X TRANSCEIVER CHIPS](https://www.rtl-sdr.com/rtl_433-ported-to-esp32-microcontrollers-with-cc1101-or-sx127x-transceiver-chips/)
* [Using low-cost wireless sensors in the unlicensed bands](https://lwn.net/Articles/921497/)
* [SMART PLUG ESP32 OPENMQTTGATEWAY SERVING AS AN BLE MQTT GATEWAY AND A POWER METER](https://www.electronics-lab.com/smart-plug-esp32-openmqttgateway-serving-as-an-ble-mqtt-gateway-and-a-power-meter/)
### Theengs Plug
[![Theengs Plug video ElektroMaker](https://img.youtube.com/vi/nUwMt9p2U7o/0.jpg)](https://www.youtube.com/watch?v=nUwMt9p2U7o&t=427s)
### 433Mhz and BLE
[![433Mhz and BLE gateway video by Andreas Spiess](https://img.youtube.com/vi/_gdXR1uklaY/0.jpg)](https://www.youtube.com/watch?v=_gdXR1uklaY)
### BLE
[![BLE gateway video by Andreas Spiess](https://img.youtube.com/vi/noUROhtf0E0/0.jpg)](https://www.youtube.com/watch?v=noUROhtf0E0)
### 433Mhz
[![RTL_433 video by TECH MIND](https://img.youtube.com/vi/H-JXWbWjJYE/0.jpg)](https://www.youtube.com/watch?v=H-JXWbWjJYE)
### LORA
[![LORA video by Priceless Toolkit](https://img.youtube.com/vi/6DftaHxDawM/0.jpg)](https://www.youtube.com/watch?v=6DftaHxDawM)
::: warning Note
The material and information contained in this documentation is for general information purposes only. You should not rely upon the material or information on this documentation as a basis for making any business, legal or any other decisions. There is no warranty given on this documentation content. If you decide to follow the information and materials given it is at your own risk. I will not be liable for any false, inaccurate, inappropriate or incomplete information presented on this website.
:::
+73
View File
@@ -0,0 +1,73 @@
# Getting Started
This guide walks you through the two most popular setups from start to finish, flashed **from your browser** and integrated with **Home Assistant**. No soldering, no development environment, about 15 minutes:
* **Bluetooth sensors** — any ESP32 board reading thermometers, plant sensors, scales...
* **RF devices (433/315/868/915MHz)** — a LILYGO LoRa32 board receiving weather stations, door sensors, PIR sensors...
Using another controller (OpenHAB, Node-RED...) or another technology (IR, LoRa)? Follow the same steps and take the detours linked along the way.
## How the pieces fit together
OpenMQTTGateway (OMG) is a firmware for your board that translates signals from your devices (Bluetooth sensors, RF remotes, weather stations...) into MQTT messages, and the other way around:
<OmgPipeline />
The **broker** is the message hub; the **controller** is the home automation software where you see and use the data.
## What you need
* **A board** — for **Bluetooth**, any ESP32 development board. For **RF**, a LILYGO® LoRa32 V2.1 or a Heltec LoRa V2 matching your devices' frequency — 433MHz for most European devices, 915MHz in North America (no soldering needed). See the [board guide](prerequisites/board.md) for other cases, or get a pre-flashed [Theengs Bridge](https://shop.theengs.io/) and skip the flashing step.
* **A data USB cable** — some cables are charge-only and won't work.
* **An MQTT broker** — see Step 1.
* **A device to observe** — e.g. a [compatible Bluetooth sensor](https://decoder.theengs.io/devices/devices.html) such as a Mi Flora, LYWSD03MMC, Govee or Inkbird thermometer, or any RF sensor around you (weather station, door or PIR sensor...).
## Step 1 — Set up the MQTT broker
If you use **Home Assistant**, the simplest option is the Mosquitto add-on:
1. In Home Assistant go to **Settings → Add-ons → Add-on store**, install **Mosquitto broker** and start it.
2. Install the **MQTT integration** (**Settings → Devices & services → Add integration → MQTT**) if it isn't set up already.
3. Create a dedicated user for the gateway in **Settings → People → Users** (e.g. `omg` — it doesn't need to be an administrator). The gateway will log in with it.
No Home Assistant? Install [Mosquitto](https://mosquitto.org/) on any always-on machine, or see the [broker page](prerequisites/broker.md) for alternatives.
## Step 2 — Flash the firmware from your browser
1. Open the [web installer](upload/web-install.md) in **Chrome, Edge or Opera** (Firefox and Safari don't support web flashing).
2. Connect the board to your computer with the USB cable.
3. Pick the firmware matching your board and use case — the two most flashed:
* **Bluetooth** on a generic ESP32: <a href="./upload/board-selector.html?env=esp32dev-ble"><strong>esp32dev-ble</strong></a>
* **RF** on a LILYGO LoRa32: <a href="./upload/board-selector.html?env=lilygo-rtl_433"><strong>lilygo-rtl_433</strong></a> (the radio frequency is configurable afterwards)
4. Click **Connect**, select the board's USB port, and let the installer finish.
Prefer a desktop tool, or need a custom configuration? See the other [upload options](upload/index.md).
## Step 3 — Connect the gateway to your WiFi and broker
After flashing, the gateway starts its own WiFi access point:
1. From your phone or computer, connect to the WiFi network named **OpenMQTTGateway** or starting with **OMG_** (no password, except for Theengs devices — see the [portal page](upload/portal.md)).
2. A configuration page opens (on Android, accept "stay connected without internet"). Click **Configure WiFi**.
3. Select your WiFi network and enter its password.
4. Enter your broker's address as MQTT server (for the Home Assistant add-on, `homeassistant.local` or the server IP), keep port `1883`, and fill in the MQTT username and password from Step 1.
5. Click **Save**. The gateway restarts and joins your network.
The full list of portal fields is described on the [configuration portal page](upload/portal.md).
## Step 4 — Check that it works
* In Home Assistant, go to **Settings → Devices & services → MQTT**: after a minute you should see a device named **OpenMQTTGateway**, created automatically through MQTT discovery.
* **Bluetooth**: bring a compatible sensor nearby — it appears as its own device with its measurements as entities. Some devices only advertise on an event: press a button or trigger a reading if nothing shows up.
* **RF**: supported devices appear automatically as soon as they transmit — weather stations send every minute or two, door and PIR sensors on an event, so trigger one to see it arrive.
* You can also watch the raw messages with [MQTT Explorer](https://mqtt-explorer.com/): connect to your broker and look for `home/OpenMQTTGateway/LWT` = `online` and messages under `home/OpenMQTTGateway/`.
Something not right? Head to the [troubleshooting page](upload/troubleshoot.md) or ask on the [community forum](https://community.openmqttgateway.com).
## Where to go next
* **More RF options** — other <a href="./upload/web-install.html?bridge=rf433">compatible boards and firmwares</a>, transmitting, frequency tuning, and protocol details in [RF hardware setup](setitup/rf.md) and [RF usage](use/rf.md).
* **Infrared** — wire an IR emitter/receiver following the [IR hardware setup](setitup/ir.md).
* **Tune the gateway** — the [WebUI](use/webui.md) and [gateway commands](use/gateway.md) let you configure it at runtime over MQTT.
* **Other controllers** — [OpenHAB](integrate/openhab3.md), [Node-RED](integrate/node_red.md), [Jeedom](integrate/jeedom.md), [AWS IoT](integrate/aws_iot.md).
* **Full Home Assistant details** — [Home Assistant integration](integrate/home_assistant.md).
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Loaded 100 of 430 files, more files were not shown because too many files have changed in this diff. Show more