Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
branches: ["main"]

# Cada run compila y sube los binarios como artefacto (Actions -> run -> Artifacts).
# En push a main ademas crea la Release v<FIRMWARE_VERSION> con esos binarios,
# salvo que ya exista (hay que subir FIRMWARE_VERSION para publicar otra).
# En push a main ademas crea la Release v<FW_VERSION> con esos binarios,
# salvo que ya exista (hay que subir FW_VERSION para publicar otra).
#
# Se compila con secrets.h.template: un equipo nuevo arranca en el portal de
# Setup y ahi se configura; un equipo ya configurado conserva su NVS al actualizar.
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
- name: Package binaries
id: pkg
run: |
VERSION=$(grep -oP 'const String FIRMWARE_VERSION = "\K[^"]+' src/proxy_operativo.hpp)
VERSION=$(grep -oP '#define FW_VERSION "\K[^"]+' src/proxy_operativo.hpp)
BUILD=.pio/build/esp32_usb
DIST=dist
NAME=modbusproxy-$VERSION
Expand Down Expand Up @@ -95,7 +95,7 @@ jobs:
run: |
TAG="v$VERSION"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::notice::La release $TAG ya existe. Sube FIRMWARE_VERSION para publicar una nueva."
echo "::notice::La release $TAG ya existe. Sube FW_VERSION para publicar una nueva."
exit 0
fi

Expand All @@ -109,6 +109,8 @@ jobs:
| \`$NAME-bootloader.bin\` / \`$NAME-partitions.bin\` | Flasheo por partes (\`0x1000\` / \`0x8000\`) |
| \`$NAME-sha256.txt\` | Checksums |

**Actualizar un equipo que ya funciona:** abre \`http://<IP_del_proxy>/firmware\`, elige \`$NAME-firmware.bin\` y pulsa *Actualizar*. El fichero se verifica antes de instalarse y, si el firmware nuevo no arranca bien, el equipo vuelve solo a la versión anterior. También vale \`pio run -e PROD_ota -t upload\` (OTA). Las versiones anteriores a la 6.2-0 no tienen la pestaña Firmware: la primera actualización a 6.2-0 hay que hacerla por OTA o USB.

Flasheo de un equipo nuevo:

\`\`\`bash
Expand Down
48 changes: 43 additions & 5 deletions README.en.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 🔋 ESP32 Modbus TCP Proxy & Multiplexer for Huawei EMMA & SUN2000 (v6.1-0)
# 🔋 ESP32 Modbus TCP Proxy & Multiplexer for Huawei EMMA & SUN2000 (v6.2-0)

This repository contains industrial-grade firmware developed for **ESP32** (optimized for wired connection via the native Ethernet stack of the LAN8720 chip on WT32-ETH01 boards, or via its internal WiFi antenna). The device acts as a **network shield, transparent on-demand proxy, and Modbus TCP channel multiplexer**.

Expand Down Expand Up @@ -150,7 +150,15 @@ SERVIDORES = {

## 🚀 Version History and Changelog

### 🟢 Version 6.1-0 (Current)
### 🟢 Version 6.2-0 (Current)

* **Web firmware update (issue #16):** New **Firmware** tab showing the installed version, running partition, validation state and available space. Upload the `modbusproxy-<version>-firmware.bin` from the [releases](https://github.com/shosoin/modbusproxy/releases) without PlatformIO or a USB cable.
* **Verification before and during upload:** The browser checks that the file is an ESP32 application image (and warns if `factory.bin` was picked by mistake), that it carries the project marker `MBPXY-FW:<version>;`, and compares versions (newer / same / downgrade). Accepting a disclaimer is mandatory. The ESP32 searches for the marker again while receiving and discards any foreign firmware without touching the boot partition; `Update` validates integrity (SHA-256).
* **Automatic rollback:** A freshly installed firmware (web or OTA) boots *pending validation* and is only confirmed after 30 s running with network. If it reboots before that, the bootloader goes back to the previous version by itself.
* **Automatic releases:** For every new version on `main`, CI publishes a Release with `firmware.bin` (updates), `factory.bin` (new devices), bootloader, partitions and checksums.
* **Single version source:** The version lives in `#define FW_VERSION` (read by the firmware, CI and `pre_upload.py`). `/api/status` adds `firmware_version`.

### 🔵 Version 6.1-0

* **Configurable Test Target (issue #12):** The boot Modbus check is no longer hard-wired to the EMMA. In `/config` and in the Setup portal, inside "Modbus TCP target", you choose the device type (`EMMA`, `SUN2000` or `Custom`), enable or disable the check and set the 4 request fields: Unit ID, function, start register and number of registers. With Custom all of them are free and the web warns that they are the user's responsibility.
* **Persistence and migration:** Values are stored in NVS. When upgrading from 6.0-0 they are initialised with the EMMA ones (`0 / FC03 / 30000 / 15`, check enabled), so behaviour is unchanged until they are modified.
Expand Down Expand Up @@ -199,7 +207,7 @@ The complete behavior of the board and network stack is managed via parameteriza
* **Persistence:** stored in NVS, surviving reboots and flashing (USB or OTA). When upgrading from an older firmware, missing values are initialised with the EMMA ones.
* `/api/status` exposes `target_profile`, `startup_check`, `test_unit_id`, `test_function`, `test_register` and `test_reg_count`.
* `RECONNECT_DELAY` (`const uint32_t`): Wait time in milliseconds (`5000` ms) the proxy applies before retrying a connection to Huawei's port 502 if the socket breaks, preventing bans from burst infinite retries (Anti-DDoS).
* `FIRMWARE_VERSION` (`const String`): Stores the semantic version of the active program, rendered centered in the local menu. Also used as the suffix of the WiFi AP name in Setup Mode (`modbusproxy-<version>`).
* `FW_VERSION` (`#define`) / `FIRMWARE_VERSION` (`const String`): Semantic version of the active program; change it only in `FW_VERSION`. Also used by CI (Release name), `pre_upload.py` and the `FW_MARKER` checked by the Firmware tab. It is, rendered centered in the local menu. Also used as the suffix of the WiFi AP name in Setup Mode (`modbusproxy-<version>`).

---

Expand Down Expand Up @@ -234,6 +242,35 @@ The device runs a web server on standard port `80` that auto-refreshes every 5 s

---

## ⬆️ Web Firmware Update (Firmware tab)

For devices that already run the firmware. A brand new ESP32 is flashed once over USB with `modbusproxy-<version>-factory.bin`:

```bash
esptool.py --chip esp32 write_flash 0x0 modbusproxy-<version>-factory.bin
```

**Steps:**

1. Download `modbusproxy-<version>-firmware.bin` from [GitHub Releases](https://github.com/shosoin/modbusproxy/releases).
2. Open `http://<ESP32_IP>/firmware`. It shows the installed version, the running partition (`app0`/`app1`), the state and the available space.
3. Choose the file. Before uploading anything, the browser checks:
* that it is an ESP32 application image (`0xE9` header, ESP32 chip). If you pick `factory.bin`, it tells you to use `firmware.bin`;
* that it carries the project marker `MBPXY-FW:<version>;` (firmware from other projects is rejected);
* the version: *current → file*, telling whether it is newer, the same or a downgrade;
* that the file fits in the partition.
4. Accept the disclaimer and press **Update**. The proxy stops serving clients, the OLED shows the progress (`UPDATE WEB`) and the device reboots when done. The page waits for it to come back and shows the new version.

**Process safety:**

* The ESP32 writes to the **free** OTA partition: the running version is untouched until the new one is complete and verified.
* The ESP32 searches for the project marker again in the received stream. If missing, the upload is discarded without changing the boot partition, even if someone bypasses the browser check. `Update` also validates the image integrity (SHA-256).
* **Rollback:** the new firmware boots *pending validation* and is confirmed after 30 s running with network (or when reaching the Setup portal). If it reboots before (crash, reset), the bootloader goes back to the previous version. This also covers ArduinoOTA updates. A firmware that hangs **without** rebooting is not rolled back: it would need USB recovery.
* The configuration (NVS) is kept.
* **Limits:** web/OTA updates cannot change the partition table or the bootloader; that requires USB. The tab has no password, just like `/config`: the proxy is meant for trusted local networks.

---

## 📊 Remote Monitoring via REST JSON API

To enable native integrations with external systems or create dedicated diagnostic sensors in Home Assistant, the firmware exposes a read-only optimized JSON endpoint at:
Expand All @@ -243,6 +280,7 @@ The response is generated by concatenating memory buffers to eliminate heavy thi

```json
{
"firmware_version": "6.2-0",
"uptime_seconds": 3245,
"backend_state": "CONNECTED",
"device_model": "SmartHEMS",
Expand Down Expand Up @@ -371,7 +409,7 @@ When NVS is empty (freshly flashed device or erased flash), `runSetup` does not

When entering Setup Mode, the ESP32:

1. Creates an **open WiFi access point** named `modbusproxy-6.1-0` (no password).
1. Creates an **open WiFi access point** named `modbusproxy-6.2-0` (no password).
2. Assigns IP `192.168.1.1` to its own interface.
3. Any device connecting to that WiFi and opening a browser is **automatically redirected** to the configuration page (captive portal — works just like hotel WiFi).
4. The wizard lets you configure:
Expand All @@ -398,7 +436,7 @@ If you still have access to the proxy's web interface:
1. Open `http://<proxy_IP>/config` in your browser.
2. Scroll down to the **Danger Zone** section.
3. Press **Factory Reset (Enter Setup Mode)** and confirm.
4. The device restarts and brings up the `modbusproxy-6.1-0` AP.
4. The device restarts and brings up the `modbusproxy-6.2-0` AP.

---

Expand Down
48 changes: 43 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 🔋 ESP32 Modbus TCP Proxy & Multiplexer for Huawei EMMA & SUN2000 (v6.1-0)
# 🔋 ESP32 Modbus TCP Proxy & Multiplexer for Huawei EMMA & SUN2000 (v6.2-0)

Este repositorio contiene el firmware de grado industrial desarrollado para **ESP32** (optimizado para conexión por cable físico mediante la pila Ethernet nativa del chip LAN8720 en placas WT32-ETH01, o mediante su antena WiFi interna). El dispositivo actúa como un **escudo de red, proxy transparente bajo demanda (On-Demand) y multiplexor de canales Modbus TCP**.

Expand Down Expand Up @@ -153,7 +153,15 @@ SERVIDORES = {

## 🚀 Historial de Versiones y Novedades

### 🟢 Versión 6.1-0 (Actual)
### 🟢 Versión 6.2-0 (Actual)

* **Actualización de firmware desde la web (issue #16):** Nueva pestaña **Firmware** con la versión instalada, la partición en uso, el estado de validación y el espacio disponible. Permite subir el `modbusproxy-<versión>-firmware.bin` de las [releases](https://github.com/shosoin/modbusproxy/releases) sin PlatformIO ni cable USB.
* **Verificación antes y durante la subida:** El navegador comprueba que el fichero es una imagen de aplicación ESP32 (y avisa si se elige por error la `factory.bin`), que lleva la marca del proyecto `MBPXY-FW:<versión>;` y compara versiones (más nueva / igual / downgrade). Es obligatorio aceptar un aviso de responsabilidad. El ESP32 vuelve a buscar la marca mientras recibe el fichero y descarta cualquier firmware ajeno sin tocar el arranque; `Update` valida la integridad (SHA-256).
* **Rollback automático:** Un firmware recién instalado (por web u OTA) arranca *pendiente de validar* y solo se confirma tras 30 s funcionando con red. Si se reinicia antes, el bootloader vuelve solo a la versión anterior.
* **Releases automáticas:** El CI publica en cada versión nueva de `main` una Release con `firmware.bin` (actualizaciones), `factory.bin` (equipos nuevos), bootloader, particiones y checksums.
* **Versión única:** La versión se define en `#define FW_VERSION` (la leen el firmware, el CI y `pre_upload.py`). `/api/status` añade `firmware_version`.

### 🔵 Versión 6.1-0

* **Test Target configurable (issue #12):** El test Modbus de arranque deja de estar fijado a la EMMA. En `/config` y en el portal de Setup, dentro de "Destino Modbus TCP", se elige el tipo de dispositivo (`EMMA`, `SUN2000` o `Personalizado`), se activa o desactiva el test y se definen los 4 datos de la petición: Unit ID, función, registro inicial y número de registros. En Personalizado todos son libres y la web avisa de que van bajo responsabilidad del usuario.
* **Persistencia y migración:** Los valores se guardan en NVS. Al actualizar desde 6.0-0 se inicializan con los de la EMMA (`0 / FC03 / 30000 / 15`, test activado), por lo que el comportamiento no cambia hasta que se modifiquen.
Expand Down Expand Up @@ -202,7 +210,7 @@ El comportamiento completo de la placa y la pila de red se administra mediante v
* **Persistencia:** se guardan en NVS y sobreviven a reinicios y flasheos (USB u OTA). Al actualizar desde un firmware anterior, si no existen, se inicializan con los valores de la EMMA.
* `/api/status` expone `target_profile`, `startup_check`, `test_unit_id`, `test_function`, `test_register` y `test_reg_count`.
* `RECONNECT_DELAY` (`const uint32_t`): Tiempo de espera en milisegundos (`5000` ms) que aplica el proxy antes de reintentar una conexión contra el puerto 502 de Huawei si el socket se rompe, evitando baneos por reintentos infinitos en ráfaga (Anti-DDoS).
* `FIRMWARE_VERSION` (`const String`): Almacena la versión semántica del programa activo que se renderizará de forma centrada en el menú local. También se usa como sufijo del nombre del AP WiFi en Modo Setup (`modbusproxy-<version>`).
* `FW_VERSION` (`#define`) / `FIRMWARE_VERSION` (`const String`): Versión semántica del programa activo; se cambia solo en `FW_VERSION`. La usan también el CI (nombre de la Release), `pre_upload.py` y la marca `FW_MARKER` que verifica la pestaña Firmware. Se que se renderizará de forma centrada en el menú local. También se usa como sufijo del nombre del AP WiFi en Modo Setup (`modbusproxy-<version>`).

---

Expand Down Expand Up @@ -237,6 +245,35 @@ El dispositivo levanta un servidor web en el puerto estándar `80` que se refres

---

## ⬆️ Actualización de Firmware desde la Web (pestaña Firmware)

Para equipos que ya tienen el firmware funcionando. Un ESP32 nuevo se flashea una única vez por USB con `modbusproxy-<versión>-factory.bin`:

```bash
esptool.py --chip esp32 write_flash 0x0 modbusproxy-<versión>-factory.bin
```

**Pasos:**

1. Descarga `modbusproxy-<versión>-firmware.bin` de [GitHub Releases](https://github.com/shosoin/modbusproxy/releases).
2. Abre `http://<IP_ESP32>/firmware`. Verás la versión instalada, la partición en uso (`app0`/`app1`), el estado y el espacio disponible.
3. Elige el fichero. Antes de subir nada, el navegador comprueba:
* que es una imagen de aplicación ESP32 (cabecera `0xE9`, chip ESP32). Si eliges `factory.bin`, avisa de que uses `firmware.bin`;
* que lleva la marca del proyecto `MBPXY-FW:<versión>;` (no deja instalar firmwares de otros proyectos);
* la versión: *actual → fichero*, indicando si es más nueva, la misma o un downgrade;
* que el fichero cabe en la partición.
4. Acepta el aviso de responsabilidad y pulsa **Actualizar**. El proxy deja de atender clientes, el OLED muestra el progreso (`UPDATE WEB`) y al terminar el equipo se reinicia. La página espera a que vuelva y muestra la versión nueva.

**Seguridad del proceso:**

* El ESP32 escribe en la partición OTA **libre**: la versión en uso no se toca hasta que la nueva está completa y verificada.
* El ESP32 vuelve a buscar la marca del proyecto en el flujo recibido. Si no está, descarta la subida sin cambiar el arranque, aunque alguien se salte la verificación del navegador. `Update` valida además la integridad de la imagen (SHA-256).
* **Rollback:** el firmware nuevo arranca *pendiente de validar* y se confirma tras 30 s funcionando con red (o al llegar al portal de Setup). Si se reinicia antes (crash, reset), el bootloader vuelve solo a la versión anterior. Vale también para las actualizaciones por ArduinoOTA. Un firmware que se cuelgue **sin** reiniciarse no se revierte: habría que recuperarlo por USB.
* La configuración (NVS) se conserva.
* **Límites:** por web u OTA no se pueden cambiar la tabla de particiones ni el bootloader; eso requiere USB. La pestaña no tiene contraseña, igual que `/config`: el proxy está pensado para redes locales de confianza.

---

## 📊 Monitorización remota vía API REST JSON

Para permitir integraciones nativas con sistemas externos o crear sensores de diagnóstico dedicados en Home Assistant, el firmware expone un endpoint JSON optimizado de solo lectura en la ruta:
Expand All @@ -246,6 +283,7 @@ La respuesta se genera concatenando buffers en memoria para eliminar el uso de l

```json
{
"firmware_version": "6.2-0",
"uptime_seconds": 3245,
"backend_state": "CONNECTED",
"device_model": "SmartHEMS",
Expand Down Expand Up @@ -376,7 +414,7 @@ Cuando la NVS está vacía (dispositivo recién flasheado o flash borrada), `run

Al entrar en Modo Setup el ESP32:

1. Levanta un **punto de acceso WiFi abierto** con el nombre `modbusproxy-6.1-0` (sin contraseña).
1. Levanta un **punto de acceso WiFi abierto** con el nombre `modbusproxy-6.2-0` (sin contraseña).
2. Asigna la IP `192.168.1.1` a su propia interfaz.
3. Cualquier dispositivo que se conecte a esa WiFi y abra un navegador es **redirigido automáticamente** a la página de configuración (portal cautivo — funciona igual que los WiFi de hoteles).
4. El asistente permite configurar:
Expand All @@ -403,7 +441,7 @@ Si todavía tienes acceso a la interfaz web del proxy:
1. Abre `http://<IP_del_proxy>/config` en el navegador.
2. Baja hasta la sección **Zona de Peligro**.
3. Pulsa **Factory Reset (Entrar en Modo Setup)** y confirma.
4. El dispositivo reinicia y levanta el AP `modbusproxy-6.1-0`.
4. El dispositivo reinicia y levanta el AP `modbusproxy-6.2-0`.

---

Expand Down
2 changes: 1 addition & 1 deletion pre_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def find_file(name, dirs):
# -----------------------------------------------------------------------
fw_version = extract_first(
proxy_content,
r'const\s+String\s+FIRMWARE_VERSION\s*=\s*"([^"]+)"'
r'#define\s+FW_VERSION\s+"([^"]+)"'
)


Expand Down
Loading
Loading