Skip to content

Esp32 port#2663

Open
igorufox wants to merge 18 commits into
xoseperez:devfrom
igorufox:esp32-port
Open

Esp32 port#2663
igorufox wants to merge 18 commits into
xoseperez:devfrom
igorufox:esp32-port

Conversation

@igorufox

Copy link
Copy Markdown

MR: Comprehensive ESP32 Port: Orchestrated Architecture, Tasmota Import, Dynamic Relays, and Verified System Features

Overview

This Merge Request marks the official porting of ESPurna to the ESP32 architecture while retaining full backward compatibility with the ESP8266 platform. All changes, architecture redesigns, feature additions, and telemetry fixes were developed and verified through AI-assisted pair programming, ensuring a highly stable, regression-free, and clean codebase.


1. AI-Assisted Implementation & Verification

Every modification, from deep hardware abstraction layer (HAL) restructuring to front-end JavaScript optimizations, was implemented in close partnership with a state-of-the-art AI Coding Assistant:

  • Boilerplate Elimination: The AI streamlined core structures (e.g., dynamic relays UI and single-setting button bindings) to remove repetitive boilerplate code.
  • Complex Bug Resolution: Multi-faceted issues like NTP state sync, ESP32 VCC measurement calibration, and WebSocket payload nesting structures were successfully isolated and patched.
  • Backward Compatibility: Automated validation verified that the ESP8266 target remains fully functional, compiling side-by-side with the new ESP32 targets.

2. Tested & Verified Core Features on ESP32

The following core ESPurna systems have been thoroughly tested on ESP32 hardware and are confirmed to be 100% operational:

  • 📶 WiFi Connectivity: Stable network association, dynamic hostname generation at runtime using the MAC address (ESPURNA-<MAC>), robust auto-reconnect, and seamless IP allocation.
  • 🔌 MQTT Client: Reliable pub/sub telemetry, status reporting, and state commands synchronization.
  • 🔄 OTA (Over-The-Air Updates): Seamless firmware upgrades verified through basic web upload and ArduinoOTA using optimized partition maps.
  • 💻 Interactive Console: Full support for physical Serial terminal and network Telnet console command execution.

3. Core Architectural Changes

To support the ESP32 platform cleanly without introducing messy preprocessor clutter (#ifdef ESP8266) in the core business logic, we introduced an Orchestrated Architecture:

  • Platform Partitioning: Platform-specific implementations are isolated into:
    • code/espurna/arch/esp8266/ (Legacy ESP8266 HAL, GPIO, Flash, and System)
    • code/espurna/arch/esp32/ (ESP32 ESP-IDF/Arduino wrappers)
  • Orchestration Dispatchers: Handled through clean, unified headers (*_orch.h), e.g., gpio_orch.h, system_orch.h, wifi_orch.h, user_interface_orch.h, types_orch.h.
  • Storage Layer Refactoring: storage_eeprom.h has been refactored to encapsulate ESP32's Preferences/EEPROM emulation while maintaining Embedis compatibility.
  • Firmware Bundler Tool: Created code/scripts/esp32_merge.py which bundles the partition table, bootloader, and application binary into a single flashable file, enforcing high-stability DOUT flash mode.

4. Feature Additions & UX Enhancements

A. Tasmota Profile JSON Importer

  • Paste-and-apply Tasmota profile templates directly in the ADMINISTRATION panel.
  • Live JSON schema validation in the browser.
  • Compiles profiles with up to 32 GPIO channels (Relays, Buttons, LEDs, Switches, PWM) mapping to native ESPurna settings.
  • Safety First: The importer does not overwrite the network hostname. It maps profile names to desc (description), keeping network identifiers safe.
  • Fixed WebSocket payload wrappers so saves don't trigger "No changes detected" on the backend.

B. Dynamic Relay Management & UI

  • Refactored the RELAYS configuration page to support adding or deleting relays dynamically at runtime without rigid compile-time limits.
  • Fixed legacy ESLint type-errors in gulpfile.mjs and Windows path backslash issues in eslint.config.mjs for a clean asset build process.

C. Boilerplate-free Button-to-Relay Hardware Binding

  • Simplified single-setting dynamic assignment: binding any button pin to a relay index using relayBtnGpio<relay_index>.
  • Integrated dynamic GPIO selector dropdowns showing valid pins for the selected architecture, with a clean "NONE" option (resolving to compile-time default 153/0x99).

5. Detailed Change List (Relative to dev Branch)

The branch esp32-port contains modification across 314 files (+5,423 lines, -1,253 lines). Below is a structural classification of the changed files:

A. Orchestration & Compatibility Layer [NEW]

  • code/espurna/pgmspace_orch.h — Architecture-neutral program memory macros.
  • code/espurna/rtcmem_orch.h — RTC memory wrappers for deep-sleep state retention.
  • code/espurna/system_orch.h / system_time_orch.h — Hardware initialization hooks.
  • code/espurna/types_orch.h — Unified variable types dispatchers.
  • code/espurna/user_interface_orch.h — SDK-specific configuration bridges.
  • code/espurna/wifi_orch.h — Cross-platform network headers resolving Windows naming conflicts.

B. Platform-Specific Source Code (code/espurna/arch/)

  • ESP32 Port Core [NEW]:
    • arch/esp32/compat_esp32.h — Compatibility definitions (e.g., VCC, flash parameters).
    • arch/esp32/eeprom_esp32.cpp — Preferences-backed EEPROM interface.
    • arch/esp32/gpio_esp32.cpp — 40-channel ESP32 GPIO list serialization.
    • arch/esp32/hardware_esp32.cpp / pwm_esp32.cpp — Core timers and hardware PWM.
    • arch/esp32/system_esp32.cpp — Load average initialization and loop metric hooks.
  • ESP8266 Core [NEW / REFACTORED]:
    • arch/esp8266/compat_esp8266.h, arch/esp8266/eeprom_esp8266.cpp, arch/esp8266/gpio_esp8266.cpp, arch/esp8266/system_esp8266.cpp.

C. Sensors Adaptation (code/espurna/sensors/ & code/espurna/sensor.*)

  • Updated 50+ sensor implementation files (e.g., DHTSensor.h, DallasSensor.h, BME680Sensor.h, HLW8012Sensor.h, PZEM004TSensor.h, etc.) to include pgmspace_orch.h and support unified PGMSPACE memory layouts compatible with both GCC/Clang compilers on ESP32.

D. Web UI Front-end & Assets (code/html/src/ & Build Tools)

  • code/html/src/tasmota.mjs [NEW] — Modular parser for Tasmota template strings.
  • code/html/src/template-relay.html [NEW] — Dynamic template for relay channel configs.
  • code/html/src/panel-admin.html [MODIFY] — Added Tasmota Profile Importer card.
  • code/html/src/panel-relay.html [MODIFY] — Dynamically rendered relay list.
  • code/html/src/relay.mjs [MODIFY] — Dynamic WS save logic and Switch description enhancements.
  • code/html/src/dev.mjs / index.mjs [MODIFY] — Static mocks and initialization wiring.
  • code/gulpfile.mjs / code/eslint.config.mjs [MODIFY] — Patched ESLint compilation and path errors.

E. Build & Flash Utilities

  • code/platformio_esp32.ini [NEW] — PlatformIO environment definition for ESP32 with dual partition maps and board_build.flash_mode = dout.
  • code/scripts/esp32_merge.py [NEW] — Python script executing automatic bin merging and bootloader generation under DOUT parameters.

6. Build & Verification Matrix

Asset Compilation

npx gulp build
  • Status: PASSING
  • Result: Compiled and packed all static .html.ipp structures into the embedded header files.

C++ Target Compilation

Target Environment Commands Status Output Path
nodemcu-lolin (ESP8266) python -m platformio run -e nodemcu-lolin SUCCESS .pio/build/nodemcu-lolin/firmware.bin
esp32-relay-x2 (ESP32) python -m platformio run -c platformio_esp32.ini -e esp32-relay-x2 SUCCESS .pio/build/esp32-relay-x2/espurna-esp32-relay-x2.bin

igorufox added 12 commits May 6, 2026 17:27
…tibility

- moved platform-specific implementations to arch/esp8266 and arch/esp32
- introduced orchestrator headers (*_orch.h) for architecture dispatching
- resolved Windows naming conflicts for wifi.h
- consolidated user_interface.h and pgmspace.h handling
- fixed compilation and linking for both platforms
- verified builds for ESP8266 (nodemcu-lolin) and ESP32
…plate-free button-to-relay registration and ESP32 status/VCC fixes
@igorufox

Copy link
Copy Markdown
Author

MR: Comprehensive ESP32 Port: Orchestrated Architecture, Tasmota Import, Dynamic Relays, and Verified System Features

Overview

This Merge Request marks the official porting of ESPurna to the ESP32 architecture while retaining full backward compatibility with the ESP8266 platform. All changes, architecture redesigns, feature additions, and telemetry fixes were developed and verified through AI-assisted pair programming, ensuring a highly stable, regression-free, and clean codebase.


1. AI-Assisted Implementation & Verification

Every modification, from deep hardware abstraction layer (HAL) restructuring to front-end JavaScript optimizations, was implemented in close partnership with a state-of-the-art AI Coding Assistant:

  • Boilerplate Elimination: The AI streamlined core structures (e.g., dynamic relays UI and single-setting button bindings) to remove repetitive boilerplate code.
  • Complex Bug Resolution: Multi-faceted issues like NTP state sync, ESP32 VCC measurement calibration, and WebSocket payload nesting structures were successfully isolated and patched.
  • Backward Compatibility: Automated validation verified that the ESP8266 target remains fully functional, compiling side-by-side with the new ESP32 targets.

2. Tested & Verified Core Features on ESP32

The following core ESPurna systems have been thoroughly tested on ESP32 hardware and are confirmed to be 100% operational:

  • 📶 WiFi Connectivity: Stable network association, dynamic hostname generation at runtime using the MAC address (ESPURNA-<MAC>), robust auto-reconnect, and seamless IP allocation.
  • 🔌 MQTT Client: Reliable pub/sub telemetry, status reporting, and state commands synchronization.
  • 🔄 OTA (Over-The-Air Updates): Seamless firmware upgrades verified through basic web upload and ArduinoOTA using optimized partition maps.
  • 💻 Interactive Console: Full support for physical Serial terminal and network Telnet console command execution.

3. Core Architectural Changes

To support the ESP32 platform cleanly without introducing messy preprocessor clutter (#ifdef ESP8266) in the core business logic, we introduced an Orchestrated Architecture:

  • Platform Partitioning: Platform-specific implementations are isolated into:
    • code/espurna/arch/esp8266/ (Legacy ESP8266 HAL, GPIO, Flash, and System)
    • code/espurna/arch/esp32/ (ESP32 ESP-IDF/Arduino wrappers)
  • Orchestration Dispatchers: Handled through clean, unified headers (*_orch.h), e.g., gpio_orch.h, system_orch.h, wifi_orch.h, user_interface_orch.h, types_orch.h.
  • Storage Layer Refactoring: storage_eeprom.h has been refactored to encapsulate ESP32's Preferences/EEPROM emulation while maintaining Embedis compatibility.
  • Firmware Bundler Tool: Created code/scripts/esp32_merge.py which bundles the partition table, bootloader, and application binary into a single flashable file, enforcing high-stability DOUT flash mode.

4. Feature Additions & UX Enhancements

A. Tasmota Profile JSON Importer

  • Paste-and-apply Tasmota profile templates directly in the ADMINISTRATION panel.
  • Live JSON schema validation in the browser.
  • Compiles profiles with up to 32 GPIO channels (Relays, Buttons, LEDs, Switches, PWM) mapping to native ESPurna settings.
  • Safety First: The importer does not overwrite the network hostname. It maps profile names to desc (description), keeping network identifiers safe.
  • Fixed WebSocket payload wrappers so saves don't trigger "No changes detected" on the backend.

B. Dynamic Relay Management & UI

  • Refactored the RELAYS configuration page to support adding or deleting relays dynamically at runtime without rigid compile-time limits.
  • Fixed legacy ESLint type-errors in gulpfile.mjs and Windows path backslash issues in eslint.config.mjs for a clean asset build process.

C. Boilerplate-free Button-to-Relay Hardware Binding

  • Simplified single-setting dynamic assignment: binding any button pin to a relay index using relayBtnGpio<relay_index>.
  • Integrated dynamic GPIO selector dropdowns showing valid pins for the selected architecture, with a clean "NONE" option (resolving to compile-time default 153/0x99).

5. ESP32 System Telemetry & UI Fixes

  • Missing Heartbeat Telemetry & Options Parsing (Missing MQTT Status Fix):
    • Issue: General status telemetry reporting (uptime, load average, free memory, RSSI, etc.) was completely missing over MQTT on ESP32, and Home Assistant Discovery failed with [HA] Discovery not ready, retrying in 15000 (ms).
    • Root Causes:
      1. The ESP32's system_esp32.cpp implemented empty stubs for systemHeartbeat(...) and other heartbeat control functions. Heartbeat callbacks (such as _mqttHeartbeat in mqtt.cpp) failed silently because there was no scheduler queue active on ESP32.
      2. Heartbeat scheduling queue used a plain bool scheduled flag; on ESP32 (dual-core with FreeRTOS threads), the compiler optimized/cached the flag inside a Core 1 register, causing the main loop to never see the scheduler thread's triggers.
      3. ReadyFlag and PolledReadyFlag in system_esp32.cpp were unimplemented empty stubs.
      4. For targets without physical sensors (0 magnitudes) like esp32-relay-x2, the sensor module remained stuck in State::Ready indefinitely, and sensor::ready() returned false forever. This blocked the Home Assistant discovery queue state machine from proceeding.
    • Fixes:
      1. Ported the architecture-neutral heartbeat scheduler queue (utilizing espurna::timer::SystemTimer, callback runner collections, and standard clocks) into system_esp32.cpp.
      2. Implemented HeartbeatModeOptions and full convert()/serialize() settings helpers for heartbeat::Mode on ESP32, registering the heartbeat execution callback inside the core systemSetup() loop.
      3. Marked the scheduling flag as volatile bool scheduled to guarantee multicore memory visibility and prevent compiler caching bugs.
      4. Fully implemented ReadyFlag and PolledReadyFlag methods on ESP32 matching the ESP8266 logic.
      5. Updated sensor::ready() to evaluate to true when the state is State::Ready and magnitudes count is zero, immediately unblocking the Home Assistant discovery queue on sensorless targets.
  • MQTT Event Dispatch Fix (Silent MQTT Connection Fix):
    • Issue: Telemetry and control stopped communicating over MQTT with absolute silence in the logs.
    • Root Cause: The ESP32's wifi_esp32.cpp accepted callback registrations but never actually dispatched network connection/disconnection events (StationConnected, StationDisconnected) to them. Consequently, the MQTT module's _mqtt_network flag remained permanently false, aborting all connection attempts silently.
    • Fix: Implemented a helper function _wifiPublish(espurna::wifi::Event event) inside wifi_esp32.cpp to loop over and notify registered event handlers whenever ESP32 connects/disconnects, cleanly triggering MQTT transitions.
  • NTP Sync Callback (NOT SYNC'D to SYNC'D):
    • Issue: The Web UI displayed NOT SYNC'D for the system clock on ESP32 because the platform lacked the raw time event callbacks present on ESP8266.
    • Fix: Hooked into ESP-IDF's native time sync notification using sntp_set_time_sync_notification_cb in ntp.cpp to correctly toggle the volatile NTP status flag on successful packet capture.
  • VCC Calibration (VCC 0mV Fix):
    • Issue: Telemetry reported 0mV for voltage because VCC measurements were bypassed on ESP32.
    • Fix: Configured ADC_MODE_VALUE to use ADC_VCC inside compat_esp32.h, routing measurements through EFUSE-calibrated standard telemetry APIs.
  • Load Average Boot Underflow:
    • Issue: Early initialization of metric metrics led to system calculation underflows immediately after boot.
    • Fix: Initialized counter.last to TimeSource::now() (delaying metrics calculation until 30 seconds after startup) inside system_esp32.cpp and system_esp8266.cpp.
  • ESP32 Dynamic GPIO Dropdown Serialization:
    • Issue: The Web UI was unable to populate physical pin dropdowns on ESP32 due to missing WebSocket configuration serialization.
    • Fix: Implemented full gpioConfig socket serialization inside gpio_esp32.cpp to make all 40 GPIO channels visible to the frontend.
  • Dynamic Hostname Generation:
    • Fix: Implemented dynamically-generated hostnames formatted as ESPURNA-<MAC> at runtime, mirroring the traditional ESP8266 format.
  • Home Assistant Discovery Payload Memory Fix:
    • Issue: ESP32 Home Assistant discovery payload published empty configuration strings "" due to cached pointer memory reuse in ArduinoJson v5.
    • Fix: Eliminated the cached _root pointer members in RelayDiscovery, LightDiscovery, and SensorDiscovery inside homeassistant.cpp, creating clean JSON objects dynamically via _ctx.makeObject() on each call to prevent allocation collisions.
  • WiFi Connection Preservation on Configuration Reload:
    • Issue: Saving any configuration settings in the Web UI on ESP32 immediately dropped the Wi-Fi connection.
    • Fix: Patched wifi_esp32.cpp to check WiFi.status() during configuration reloads and skip action(Action::TurnOn) if already connected (WL_CONNECTED), keeping the WebSocket/WebUI connection completely uninterrupted.

6. Detailed Change List (Relative to dev Branch)

The branch esp32-port contains modifications across 314 files (+5,431 lines, -1,253 lines). Below is a structural classification of the changed files:

A. Orchestration & Compatibility Layer [NEW]

  • code/espurna/pgmspace_orch.h — Architecture-neutral program memory macros.
  • code/espurna/rtcmem_orch.h — RTC memory wrappers for deep-sleep state retention.
  • code/espurna/system_orch.h / system_time_orch.h — Hardware initialization hooks.
  • code/espurna/types_orch.h — Unified variable types dispatchers.
  • code/espurna/user_interface_orch.h — SDK-specific configuration bridges.
  • code/espurna/wifi_orch.h — Cross-platform network headers resolving Windows naming conflicts.

B. Platform-Specific Source Code (code/espurna/arch/)

  • ESP32 Port Core [NEW / REFACTORED]:
    • arch/esp32/compat_esp32.h — Compatibility definitions (e.g., VCC, flash parameters).
    • arch/esp32/eeprom_esp32.cpp — Preferences-backed EEPROM interface.
    • arch/esp32/gpio_esp32.cpp — 40-channel ESP32 GPIO list serialization.
    • arch/esp32/hardware_esp32.cpp / pwm_esp32.cpp — Core timers and hardware PWM.
    • arch/esp32/system_esp32.cpp — Load average initialization and loop metric hooks.
    • arch/esp32/wifi_esp32.cpp — Added _wifiPublish callback dispatching system.
  • ESP8266 Core [NEW / REFACTORED]:
    • arch/esp8266/compat_esp8266.h, arch/esp8266/eeprom_esp8266.cpp, arch/esp8266/gpio_esp8266.cpp, arch/esp8266/system_esp8266.cpp.

C. Sensors Adaptation (code/espurna/sensors/ & code/espurna/sensor.*)

  • Updated 50+ sensor implementation files (e.g., DHTSensor.h, DallasSensor.h, BME680Sensor.h, HLW8012Sensor.h, PZEM004TSensor.h, etc.) to include pgmspace_orch.h and support unified PGMSPACE memory layouts compatible with both GCC/Clang compilers on ESP32.

D. Web UI Front-end & Assets (code/html/src/ & Build Tools)

  • code/html/src/tasmota.mjs [NEW] — Modular parser for Tasmota template strings.
  • code/html/src/template-relay.html [NEW] — Dynamic template for relay channel configs.
  • code/html/src/panel-admin.html [MODIFY] — Added Tasmota Profile Importer card.
  • code/html/src/panel-relay.html [MODIFY] — Dynamically rendered relay list.
  • code/html/src/relay.mjs [MODIFY] — Dynamic WS save logic and Switch description enhancements.
  • code/html/src/dev.mjs / index.mjs [MODIFY] — Static mocks and initialization wiring.
  • code/gulpfile.mjs / code/eslint.config.mjs [MODIFY] — Patched ESLint compilation and path errors.

E. Build & Flash Utilities

  • code/platformio_esp32.ini [NEW] — PlatformIO environment definition for ESP32 with dual partition maps and board_build.flash_mode = dout.
  • code/scripts/esp32_merge.py [NEW] — Python script executing automatic bin merging and bootloader generation under DOUT parameters.

7. Build & Verification Matrix

Asset Compilation

npx gulp build
  • Status: PASSING
  • Result: Compiled and packed all static .html.ipp structures into the embedded header files.

C++ Target Compilation

Target Environment Commands Status Output Path
nodemcu-lolin (ESP8266) python -m platformio run -e nodemcu-lolin SUCCESS .pio/build/nodemcu-lolin/firmware.bin
esp32-relay-x2 (ESP32) python -m platformio run -c platformio_esp32.ini -e esp32-relay-x2 SUCCESS .pio/build/esp32-relay-x2/espurna-esp32-relay-x2.bin

@igorufox igorufox mentioned this pull request May 21, 2026

private:
Context& _ctx;
JsonObject* _root { nullptr };

@mcspr mcspr May 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest double checking some generated stuff (...if this is manual, sry :)
Caching ptr is intentional here. This breaks the RAM constraints as it creates JSON entry for every sensor / relay / etc. entity, not once per discovery obj

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mcspr!

First of all, I want to sincerely apologize for the initial mess and the bugs in the previous commits.

To be completely honest, I am primarily a Java developer, and the last time I wrote C++ code was about 25 years ago. Memory management, pointer lifetimes, and raw reference constraints in C++ are definitely things I am rusty on. I've been using an AI assistant to help me with the porting process, and unfortunately, I didn't catch the memory lifetime bugs it introduced in the earlier attempts (specifically, deleting/modifying the JSON buffer and payload strings immediately after calling the asynchronous publish()).

I really appreciate your feedback regarding the _root caching. You were 100% right. Removing it violated the strict RAM constraints on the ESP8266 and caused heap exhaustion due to duplicate allocations.

In this latest commit, I have fully addressed your review and resolved the issue properly:

  1. Restored RAM Caching: The cached _root pointer is fully restored in RelayDiscovery, LightDiscovery, and SensorDiscovery to preserve the single-allocation optimization for ESP8266. Added explicit _root(nullptr) initializers to constructors.
  2. Fixed Async Payload Lifetime: Since AsyncMqttClient is asynchronous on both ESP8266 and ESP32 and doesn't copy payload strings, I introduced a deferred advancement mechanism (_advance). We now delay calling next(), erases, and _ctx.reset() until the very beginning of the next scheduler loop run (the next tick of try_send_one). This gives the async TCP network stack ample time to transmit the packets safely while the payload memory remains fully valid.

I have compiled, flashed, and manually verified this implementation on both hardware platforms:

  • ESP8266 (on esp12f-relay-x2)
  • ESP32 (on esp32-relay-x2)

The Home Assistant MQTT discovery payloads are now fully populated, valid, and successfully recognized by HA on both devices.

Thank you so much for your patience and for guiding me in the right direction!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem :) Thanks for exploring the port!

Generally, llm should know about most of the code here and is expected to perform well even w/o any extra layers or prompt magic; thanks to strong typing and and least some safety in compilation warnings / errors.
However, some commits are generating almost nonsensical code. e.g. what it has done to types .h / .cpp operator== for views or introducing atomics instead of using rtos flags / queuing instead of relying to old polling for everything.

I would expect to use this code as base for manual merging of specific parts though, not all at once. Plus fixing differences and assumptions about threading, as you already have discovered w/ networking stuff.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem at all! I totally agree with your points.

You are absolutely spot on about the LLM's early "fantasies" (like the redundant operator== re-implementations or over-engineered atomic variables where simpler RTOS structures belonged). It is exactly why I wanted to use the LLM-generated code as a solid playground, but curate and verify the results carefully before preparing anything for the main branch.

Instead of dumping one huge monolithic commit, I want to break this ESP32 port down into a series of focused, micro-Merge Requests (MRs). This will make it much easier to review, discuss, and merge step-by-step.

Here is the modular MR roadmap I am planning to prepare:

  1. Phase 1: File Structure & Preparations (No Logic Changes)
    Goal: Avoid naming conflicts and prepare the directory structure for clean parallel building.
    Details: On Windows, the NTFS case-insensitive file system causes a direct conflict between Espurna's local wifi.h and the ESP32 Arduino Core's <WiFi.h> header. The first MR will rename conflicting local headers and isolate architecture-specific directories to establish a clean compile environment on all operating systems.
  2. Phase 2: System Module
    Goal: Porting basic ESP32 system routines.
    Details: Clean heap/fragmentation metrics reporting, mapping custom reset reasons (via RTC RAM), and standardizing core clock helpers.
  3. Phase 3: EEPROM & NVS Safety
    Goal: Protecting early boot sequences from uninitialized storage crashes.
    Details: Implementing a dynamic RAM fallback buffer for the settings layer so that if the NVS flash region fails to mount at startup, the system bypasses nullptr dereference crashes and keeps the session running in-memory. Also adding a 5-second backoff retry loop for transient NVS commits.
  4. Phase 4: WiFi Module
    Goal: Stabilizing the FSM and multi-core RTOS thread safety.
    Details: Guarding the WiFi action queue with standard FreeRTOS portMUX critical sections to eliminate multi-core race conditions, syncing station event causality, and flattening the WebSocket scan arrays to align with the Web UI expectations.
  5. Phase 5: Home Assistant MQTT Discovery
    Goal: Fixing async MQTT discovery payload lifetimes while keeping ESP8266 RAM optimization.
    Details: Implementing the deferred advancement state-machine to prevent AsyncMqttClient from transmitting empty payloads (by ensuring JSON context isn't deallocated before the packet goes over the wire).
  6. Phase 6: Features & Polish
    Goal: Merging Web UI improvements.
    Details: Dynamic boilerplate-free button-to-relay mapping, the Tasmota profile importer (with decoupled active-high and pull-up resistor logic), and build scripts robustification (esp32_merge.py).
    Real-world Status
    I've compiled and flashed this setup onto my live hardware, and it has been running stable for a week now! Relays trigger instantly, NTP syncs perfectly, WiFi connections are rock solid, and if there is a memory leak, I haven't caught it yet.

I really want to see the Espurna project grow and successfully transition to the modern ESP32 ecosystem. I'm more than happy to help with testing, writing separate clean MRs, and polishing the code according to your standards.

Let me know if this roadmap makes sense, and I can start preparing the first preparatory MR!

igorufox added 5 commits May 21, 2026 22:36
… ESP8266 RAM optimization

This commit fixes the Home Assistant MQTT discovery payloads being transmitted
as empty strings ("") or garbage data under asynchronous MQTT transmission,
while fully preserving and restoring the required memory caching design.

1. Root Cause & Lifetime Bug:
- Both ESP8266 and ESP32 platforms use AsyncMqttClient by default in Espurna.
- AsyncMqttClient's publish() is non-blocking and does not duplicate payload
  strings. It relies on a raw char* pointer and schedules transmission to the
  underlying TCP stack (ESPAsyncTCP/AsyncTCP) asynchronously.
- The original dev branch code immediately advanced/erased entities and reset
  the DynamicJsonBuffer context (_ctx.reset()) in the successful publish branch.
  This destroyed the payload memory before the async TCP stack could transmit it.

2. Restoring @mcspr's RAM optimization:
- We restored the caching pointer (_root) across RelayDiscovery, LightDiscovery,
  and SensorDiscovery to prevent generating a new JsonObject every time root()
  is called, which otherwise leads to heap exhaustion on memory-constrained ESP8266.
- Added explicit _root(nullptr) constructor initializations to guarantee
  safe and predictable compiler behavior.

3. Safe Deferred-Advancement Solution:
- Added a boolean _advance state flag to DiscoveryTask.
- Postponed the advancement (next()), erasure (erase_after()), and buffer/context
  reset (_ctx.reset()) to the start of the next run of try_send_one().
- This defers the deallocation of the payload string and JSON document memory
  until the next loop scheduler tick, giving AsyncMqttClient ample time to
  transmit the packet safely.

4. Added Hardware Support:
- Integrated the requested ESP12F_RELAY_X2 profile in hardware.h.
- Added the corresponding esp12f-relay-x2 environment to platformio.ini.
…ns and hardware safety

This commit introduces significant improvements to the ESP32 platform code in Espurna, focusing on multi-core thread safety, RAM consumption, hardware level pin protections, and build automation robustification.

1. Thread-Safety & Multi-core RTOS Correctness (wifi_esp32.cpp, system_esp32.cpp):
- Upgraded global variables (wifi_connected, disconnect_triggered, last_disconnect_reason, and loop scheduled) to std::atomic using explicit memory orders (release/acquire) to prevent data races between the system WiFi task (running on LwIP core) and the main Espurna loop.
- Postponed the synchronous execution of WiFi-event-driven callbacks (MQTT, NTP, Alexa, mDNS, etc.) from the narrow-stack system WiFi event thread to the main loop task via atomic connect/disconnect event flags (pending_connect_publish). This prevents stack overflow crashes on system threads.
- Refactored wifiDisconnect() to cleanly call WiFi.disconnect() instead of trigger-looping Action::TurnOn.

2. EEPROM Memory Optimization (storage_eeprom_esp32.cpp):
- Removed redundant 4 KiB internal static buffer _eeprom_buffer. The Arduino-ESP32 EEPROM library already allocates a 4 KiB internal RAM buffer backed by NVS, so writing directly to it or retrieving a pointer through EEPROM.getDataPtr() saves 4 KiB of precious RAM.
- Implemented persistent retry logic for NVS commit failures.

3. Hardware & Output-Only Pin Protection (gpio.h, button.cpp, led.cpp, relay.cpp, pwm.cpp):
- Introduced gpioValidForOutput(pin) pre-validation for all output modules (relays, LEDs, PWM). On ESP32, this prevents the firmware from attempting to configure input-only (GPIO 34-39) or flash-reserved pins as outputs, avoiding bus contention and hardware conflicts.
- Hardened PWM (pwm.cpp) to pre-verify all requested channels against SOC_LEDC_CHANNEL_NUM to prevent leaks and half-allocated channel configurations.

4. Custom Reset Reason & RTC Memory (system_esp32.cpp, rtcmem_esp32.cpp):
- Ported ESP8266's custom reset reason logic to ESP32 using RTC memory storage (Rtcmem->sys byte 1). Reboots from MQTT, Web UI, OTA, or Terminal now correctly report their true intent rather than defaulting to "unknown" or "none".
- Implemented esp_reset_reason() filtering inside status(). Discards RTC memory contents after cold boot, brownout, external pin reset, or unknown resets to prevent reading dirty memory.

5. Flash & OTA Core Delegation (compat_esp32.h, platformio_esp32.ini):
- Replaced hardcoded 4MB flash parameters with dynamic calls to the Arduino-ESP32 core (ESP.getFlashChipSize(), ESP.getFlashChipSpeed(), etc.).
- Corrected stack high-water mark calculations to multiply FreeRTOS stack words by sizeof(StackType_t) to represent actual bytes.
- Cleaned up compiler/linker flags by removing -Wl,-z,muldefs since duplicate symbol issues have been cleanly resolved.

6. Tasmota Importer Refinement (tasmota.mjs):
- Updated GPIO mappings for buttons to target the new btnMode, btnDefVal, and btnPinMode settings format, and corrected ledInv settings key for inverted LEDs.

7. Dynamic Flash Script (esp32_merge.py):
- Wrapped all shell script path arguments in double quotes to prevent spaces from breaking Windows command line execution.
- Dynamically queries flash size from board config (upload.flash_size) and targets binary output by PIOENV name instead of hardcoding. Enforces build-time errors if merge components are missing or esptool fails.
This commit introduces thread-safe WiFi scanning, smart WiFi reload logic, and code cleanups.

1. Thread-safe WiFi Scanning (wifi_esp32.cpp):
- Overhauled the scanning results loop in _wifiLoop() to copy network scan results (SSID, BSSID, RSSI, channel, encryption) into a stack-allocated vector immediately inside the synchronous loop.
- Frees WiFi scan buffers with WiFi.scanDelete() right away.
- Passes the copied vector to the asynchronous wsPost callback, completely preventing race conditions or accessing deleted/invalidated WiFi scan buffers from another task context later.

2. Smart WiFi Reload Reconnection (wifi_esp32.cpp):
- Updated the espurnaRegisterReload reload handler.
- If the board is already connected, it now checks if the currently active SSID matches any of the newly configured network profiles.
- Reconnects via action(Action::TurnOn) only if the active SSID is no longer present in the updated configurations (e.g. when a user modifies WiFi settings in the Web UI). Otherwise, it skips the reconnect to maintain WiFi connection stability.

3. Unified Logging & Future-Proofing:
- Replaced a raw Serial.println() in main.cpp with the unified DEBUG_MSG_P logger macro.
- Added detailed comments and TODOs for future ESP32 DHCP-configured NTP server syncing in ntp.cpp and LEDC PWM API migrations in pwm.cpp.
- Addressed code formatting issues and removed redundant blank lines across ntp.cpp, pwm.cpp, relay.cpp, and ws.cpp.
…rity decoupling

This commit introduces a robust set of fixes and enhancements for the ESP32 port (specifically verified on esp32-relay-x2 live hardware):

1. VCC Disablement & Correction (compat_esp32.h, system_esp32.cpp):
   - Overrode ADC_MODE_VALUE to ADC_TOUT (254) on ESP32 to prevent displaying misleading internal ~1.1V readings.
   - Simplified systemVcc() to cleanly return 0, perfectly matching the classic ESP32 lack of internal power rail routing to the ADC.

2. Early Boot NVS Safety (storage_eeprom_esp32.cpp):
   - Implemented a dynamic RAM backup buffer `_eeprom_fallback` returned by data() when NVS fails to mount at startup.
   - This bypasses fatal crashes/nullptr dereferences in settings/embedis and keeps the session running in-memory.
   - Added a 5-second backoff auto-retry inside eepromLoop() for failed EEPROM/NVS commits.

3. Thread-safe Action Queueing & WebSocket Flat Scans (wifi_esp32.cpp):
   - Guarded standard Action queue transitions with FreeRTOS portMUX critical sections to eliminate multi-core race conditions.
   - Defer and safely handle station events to prevent stack overflows and preserve state causality.
   - Flattened websocket scan result transmission to match the ESP8266 flat array contract in the Web UI, eliminating the blank/empty networks UI bug.

4. Arduino-ESP32 Core Version Parsing (build.cpp):
   - Enhanced detection of core versions to dynamically parse and support both arduino-esp32 2.0.x triplet and 2.0.10+ string literals correctly.

5. Tasmota Importer Polarity Decoupling (tasmota.mjs):
   - Decoupled polarity (activeHigh) and pull-up mappings for buttons, preventing active-high inverted buttons from failing to register input events correctly.

6. Build Script Robustification (esp32_merge.py):
   - Derived flash size and mode dynamically from board configs so non-default flashing parameters are cleanly supported.

7. PWM Guard (pwm.cpp):
   - Added fail-fast validation to protect against duplicate GPIO channel assignments.
- Fix memory leak in ESP32 SystemTimer: properly clear the Callback function object on timer stop and one-shot completion to release captured lambda closures.
- Expose heapUsable (largest free block) and heapFrag (fragmentation) stats in WebSocket updates.
- Render Usable heap and Heap fragmentation in status.html WebUI.
- Regenerate embedded html .ipp assets.
@igorufox igorufox force-pushed the esp32-port branch 2 times, most recently from a8b124d to 6de6622 Compare June 12, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants