A real-time ESP-IDF firmware project that bridges Modbus telemetry to MQTT with deterministic execution, offline buffering, and replay support.
The system is designed around clean separation of concerns, bounded RTOS work, and resilient telemetry delivery.
Modbus Vault collects frames from Modbus devices, serializes them into compact payloads, and routes them to either MQTT or flash-backed storage depending on connectivity. When the network returns, stored data is replayed automatically.
Industrial telemetry systems often need to keep running during network outages.
This project focuses on:
- predictable execution under load
- offline-first telemetry handling
- controlled replay after reconnection
- clear firmware/hardware co-design
- Deterministic RTOS design with bounded work per activation
- Event-driven architecture with queues and task notifications
- Offline buffering with flash-backed circular storage
- Replay support for stored telemetry
- Custom slab allocator to reduce heap fragmentation
- CRC-validated persistent storage
- Modular ESP-IDF component structure
The firmware is split into four main layers:
-
Telemetry Pipeline
Parses Modbus frames and serializes them for transport. -
Router
Makes a fast online/offline decision and forwards payloads to the correct service. -
Telemetry Service
Publishes live and replayed telemetry over MQTT. -
Logger Service
Stores telemetry during outages and feeds replay data in bounded batches.
This design separates the control plane from the data path and keeps runtime behavior predictable.
components/-> core firmware modulesmain/-> application entry pointtools/-> Python tooling for telemetry and logstest_modbus_vault/-> Unity-based testsdocs/-> diagrams and generated documentation
S python scripts/parseLiveTelemetry.py
Listening for messages... Press Ctrl+C to exit.
Successfully connected to the MQTT broker.
devices/pub01/modbus/live
Modbus Frame:
-------------
Timestamp (us): 470139370
Slave Address : 1
Function Code : 3
Data Length : 8
Raw Data : [0x01 0x03 0x00 0x00 0x00 0x01 0x84 0x0A]
devices/pub01/modbus/live
Modbus Frame:
-------------
Timestamp (us): 470149446
Slave Address : 1
Function Code : 3
Data Length : 7
Raw Data : [0x01 0x03 0x02 0x00 0x00 0xB8 0x44]
----------------------------------------------
Metrics Received:
RS485 overflow error(s) : 0
RS485 parity error(s) : 0
Modbus CRC error(s) : 0
Modbus overflow error(s) : 0
Modbus no_mem error(s) : 0
Telemetry publish error(s): 0
Logger write error(s) : 0
devices/pub01/modbus/replay
Modbus Frame:
-------------
Timestamp (us): 73671620
Slave Address : 1
Function Code : 3
Data Length : 7
Raw Data : [0x01 0x03 0x02 0x00 0x00 0xB8 0x44]
flowchart LR
A[RS458 Driver] --> B[Modbus Analyzer]
B --> C[Telemetry Pipeline]
C --> D[Router]
D -->|Online| E[Telemetry Service]
D -->|Offline| F[Logger Service]
F --> G[(Blackbox Logger)]
G --> H[Replay Queue]
H --> E
E --> I[MQTT Broker]
graph TD
A[RS485 Driver] -->|Raw Bytes| B[modbus_analyzer]
B -->|Frame Event| C[Telemetry Pipeline]
C -->|Protobuf Payload| D[router]
D -->|Online Path| E[telemetry_service / mqtt_bridge]
D -->|Offline Path| F[logger_service]
E -->|MQTT Publish| G((Cloud))
F -->|Write Circular Buffer| H[(Blackbox Logger)]
D -.->|Fetch Data| H
D -->|Replay Data| E
flowchart LR
A[Telemetry Pipeline] -->|Protobuf Payload| B[Router]
B --> C[telemetry_service]
C -->|MQTT Publish| G((Cloud))
![]() |
|---|
| Online Path PulseView capture |
| Interval | Time (us) | Description |
|---|---|---|
| A:B | ~565 | End of UART frame to start of frame slicing |
| B:C | ~44 | Analyzer task time |
| C:D | ~100 | CRC check to serializer queue |
| D:E | ~226 | telemetry_pipeline (Serializer) task time |
| E:F | ~10 | Routing callback |
| F:G | ~2200 | telemetry_service (MQTT publishing) task time |
flowchart LR
A[Telemetry Pipeline] -->|Protobuf Payload| B[router]
B --> C[logger_service]
C -->|Write Circular Buffer| D[(Blackbox Logger)]
![]() |
|---|
| Offline Path PulseView capture |
| Interval | Time (us) | Description |
|---|---|---|
| A:B | ~561 | End of UART frame to start of frame slicing |
| B:C | ~42 | Analyzer task time |
| C:D | ~104 | CRC check to serializer queue |
| D:E | ~231 | telemetry_pipeline (Serializer) task time |
| E:F | ~14 | Routing callback |
| F:G | ~78 | logger_service "Entry logging" task time |
flowchart LR
C[(Blackbox Logger)] -.->|Fetch Data| B[router]
B --> D[telemetry_service]
D -->|MQTT Publish| G((Cloud))
The system is designed for deterministic execution in an RTOS environment:
- Tasks process a bounded number of items per activation
- No task drains queues completely (prevents CPU starvation)
- Replay is executed in quota-limited batches
- Work is event-driven using queues and task notifications
- Flash operations are isolated from time-critical paths
This ensures predictable behavior even under:
- High telemetry rates
- Network outages
- Replay recovery scenarios
Each service runs as a dedicated FreeRTOS task:
| Task | Responsibility |
|---|---|
| Telemetry Pipeline | Serialization |
| Telemetry Service | MQTT publishing (live + replay) |
| Logger Service | Flash persistence and replay feeding |
| System Controller | Application Lifecycle |
-
Foreground work
- Live publish
- Offline logging
-
Background work
- Replay of stored data
Foreground tasks always take priority over background recovery.
The system uses dedicated queues and event-driven tasks to ensure deterministic execution and clear ownership of responsibilities.
flowchart LR
subgraph Producers
A[Telemetry Pipeline Task]
end
subgraph System
R[Router]
end
subgraph Services
T[Telemetry Service Task]
L[Logger Service Task]
end
subgraph Queues
Q1[(Live Queue)]
Q2[(Logging Queue)]
end
subgraph Logger
F[(Blackbox Logger)]
end
A --> R
T -->|Online| R
R -->|Online| Q1
R -->|Offline| Q2
R --> T
R --> L
L --> R
Q1 --> T
Q2 --> L
L --> F
- Each queue has a single consumer, ensuring deterministic behavior
- Tasks are event-driven and activated via queue + notification coupling
- Telemetry Service prioritizes live data over replay data
- Logger Service handles both persistence and replay generation through Blackbox Logger
- Replay is fed in bounded batches to prevent system overload
sequenceDiagram
autonumber
participant UART as UART/RS485
participant Analyzer as Modbus Analyzer
participant TelePipe as Telemetry Pipeline
participant Router as Router
participant TeleServ as Telemetry Service
participant MQTT as MQTT Bridge
participant LoggerServ as Logger Service
participant Logger as Blackbox Logger
participant Flash as SPI Flash
UART->>Analyzer: Receive Raw Data Byte Stream
activate Analyzer
Note over Analyzer: Check t3.5 silence & verify CRC16
Analyzer-->>TelePipe: Pass Modbus Frame (Slab Pool Allocation)
deactivate Analyzer
activate TelePipe
TelePipe->>TelePipe: Serialize to Protobuf
TelePipe-->>Router: Pass Serialized Frame (Slab Pool Allocation)
deactivate TelePipe
alt Online Path
activate Router
Router->>Router: Read network state via telemetry_service_is_online()
Router->>TeleServ: Enqueue Live
deactivate Router
activate TeleServ
TeleServ->>MQTT: Publish Frame over Wi-Fi
activate MQTT
MQTT-->>TeleServ: QoS 0
deactivate MQTT
TeleServ-->>TeleServ: Free block to slab pool
deactivate TeleServ
else Offline Path
activate Router
Router->>Router: Read network state via telemetry_service_is_online()
Router->>LoggerServ: Enqueue
deactivate Router
activate LoggerServ
LoggerServ->> Logger: Store
Logger->>Flash: Write to Circular Buffer
Flash->>Logger: Write Success
Logger-->>LoggerServ: Store Success
LoggerServ-->>LoggerServ: Free block to slab pool
deactivate LoggerServ
end
- The system utilizes a custom NVS Manager to abstract flash operations and ensure data integrity
- Flush Policy: Changes are buffered in RAM and flushed to flash every 30 seconds to minimize flash wear. Configurable in
components/nvs_manager/private_include/nvs_manager_internal.h - Integrity: All stored blobs are protected by a Modbus-compatible CRC16. Corrupted data results in an automatic fallback to default or shadow configurations.
- State Recovery: Persistent storage includes WiFi credentials, MQTT configurations and Blackbox Logger parameters.
- Flush Policy: Changes are buffered in RAM and flushed to flash every 30 seconds to minimize flash wear. Configurable in
- Blackbox Logger circular storage preserves buffered telemetry across reboots.
The project uses Protocol Buffers for compact binary log serialization.
Field numbering is treated as stable to preserve backward compatibility with previously captured logs. Future schema evolution should use additive changes where possible and reserve deprecated field IDs.
The hardware interface is designed in KiCad and built around:
- ESP32-S3-WROOM-1 MCU
- MAX3078E RS-485 transceiver
- SM712 TVS protection
- USB-C power input
- AP2112K-3.3 regulator
- Optional 120 Ω bus termination
- 750 mA resettable fuse
The board documentation and BOM describe the protection, termination, and power path choices in more detail.
Circuit Schematic (Click to enlarge) |
Hardware 3D View (Click to enlarge) |
| Reference | Value | Package/Footprint | Description |
|---|---|---|---|
| U1 | ESP32-S3-WROOM-1 | Module_ESP32-S3 | Dual-core Wi-Fi/BT MCU |
| U2 | USBLC6-2SC6 | SOT-23-6 | ESD protection of VBUS |
| U3 | AP2112K-3.3 | SOT-23-5 | Stable Low-Dropout (LDO) regulator |
| U4 | MAX3078E | SOIC-8 | RS-485 Transceiver. 1/8-unit load receiver |
| D1 | SM712 | SOT-23 | ESD protection TVS diode array |
| D2 | Red_LED | LED_D10.0mm | Power indicator |
| C1, C3 | 0.1u F | C_1206_3216Metric | Decoupling caps |
| C2 | 100u F | Capacitor_THT:C_Radial | Power filtering |
| C4, C8 | 10u F | Capacitor_THT:C_Radial_D10.0mm_H16.0mm_P5.00mm | Power filtering |
| C5, C9 | 1u F | C_1206_3216Metric | Decoupling caps |
| C6 | 100n F | C_1206_3216Metric | Power filtering |
| C7, C10 | 4.7n F | C_1206_3216Metric | DC blocking |
| FB1 | 600 Ω @ 100M Hz | 1206 | Suppress EMI |
| H1, H2 | Mounting Hole Pad | MountingHole_2.2mm_M2_Pad | Hold PCB |
| J1 | Screw Terminal 01x03 | TerminalBlock_Phoenix_MKDS-1,5-3-5.08_1x03_P5.08mm_Horizontal | A and B bus signals and Shield |
| P1 | USB-C Plug USB 2.0 | USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal | Provides power and communication with ESP |
| R1, R2 | 10K Ω | 0805 | Pull-Up resistors |
| R3, R12 | 1M Ω | 0805 | Suppress EMI |
| R4, R5 | 5.1K Ω | 0805 | Pull-Down resistors |
| R6 | 1K Ω | 0805 | Current limiting |
| R7, R8 | 4.7K Ω | 0805 | Pull up/down resistors |
| R9, R10 | 10 Ω | R_MELF_MMB-0207 | Pulse load capability |
| R11 | 120 Ω | 0805 | Optional bus termination |
| RESET1 | SW_Tactile_SPST | SW_Tactile_SPST | ESP reset button |
| SW1 | SW_DIP | SW_DIP_SPSTx01_Slide | Enable/disable termination resistor |
| TH1 | 750mA PTC | Fuse_0805 | Resettable fuse |
This project follows a test-driven approach to ensure reliability in industrial environments. Logic validation is performed via QEMU Emulation (Xtensa LX7), allowing for 100% reproducible CI/CD pipelines.
- Framework: Unity (ESP-IDF Integrated)
- Environment: Automated QEMU Simulation
- Total Test Cases: 28
- Key Validations:
- Resilience: Power-loss recovery & torn-write skipping.
- Integrity: CRC validation and Fuzz testing for corruption resilience.
- Drivers: RS485 state machine and NVS manager safety.
- Unit-tested critical components:
- RS485 driver
- Modbus parsing
- Blackbox logger (with mocks)
- NVS manager (fault recovery scenarios)
Output sample
...
Running NVS Manager Initialization...
W (1253) NVS_MANAGER: Configurations load fail, reverting to default values
I (1253) NVS_MANAGER: Initialized
modbus_vault/components/nvs_manager/test/test_nvs_manager.c:25:NVS Manager Initialization:PASS
Running NVS Read/Write WiFi Credentials...
W (1253) NVS_MANAGER: Configurations load fail, reverting to default values
I (1253) NVS_MANAGER: Initialized
modbus_vault/components/nvs_manager/test/test_nvs_manager.c:32:NVS Read/Write WiFi Credentials:PASS
Running NVS Read/Write Numeric Values...
W (1263) NVS_MANAGER: Configurations load fail, reverting to default values
I (1263) NVS_MANAGER: Initialized
modbus_vault/components/nvs_manager/test/test_nvs_manager.c:53:NVS Read/Write Numeric Values:PASS
Running NVS Out of Bounds Key...
modbus_vault/components/nvs_manager/test/test_nvs_manager.c:73:NVS Out of Bounds Key:PASS
Running NVS Manager CRC Integrity...
W (1263) NVS_MANAGER: Configurations load fail, reverting to default values
I (1263) NVS_MANAGER: Initialized
I (1273) NVS_MANAGER: Initialized
modbus_vault/components/nvs_manager/test/test_nvs_manager.c:81:NVS Manager CRC Integrity:PASS
Running RS485 Driver Init/Deinit...
I (1333) uart: queue free spaces: 20
I (1343) RS485_DRIVER: Initialized
modbus_vault/components/rs485_driver/test/test_rs485_driver.c:16:RS485 Driver Init/Deinit:PASS
Running RS485 Driver Init Failure Safety...
E (1343) uart: uart_driver_install(2000): uart rx buffer length error
W (1353) RS485_DRIVER: Failed to initialize: (ESP_FAIL).
I (1353) RS485_DRIVER: RS485 Driver de-initialized
modbus_vault/components/rs485_driver/test/test_rs485_driver.c:25:RS485 Driver Init Failure Safety:PASS
Running Serializer Encode Modbus Success...
modbus_vault/components/serializer/test/test_serializer.c:8:Serializer Encode Modbus Success:PASS
-----------------------
28 Tests 0 Failures 0 Ignored
OK
I (1363) main_task: Returned from app_main()
QEMU: Terminated
Done
- ESP-IDF v6.x
- ESP32-S3 target
- protoc-c for generating the C protobuf sources
Network and broker credentials are configured at build time through menuconfig:
idf.py menuconfigSet:
- Wi-Fi SSID / Password
- MQTT broker URI
- MQTT credentials
- MQTT device ID
No production credentials are stored in source control. All deployment-specific settings are injected during build configuration.
Credentials are persisted for operational continuity. For production deployments, secure boot, flash encryption, and encrypted NVS should be enabled as part of the platform hardening profile.
# Set up the environment
. $IDF_PATH/export.sh
# Build the project
idf.py build
# Flash and monitor
idf.py flash monitorProtobuf sources and headers are generated during the build process from
components/serializer/proto/modbus.proto
cd test_modbus_vault
idf.py build qemuTo generate API documentation run:
doxygen DoxyfileThe tools/ folder includes scripts for:
- Generate sample log files
- Parsing live MQTT telemetry
- Decoding binary log dumps
- Simulating Modbus transactions
Enables full system observability and debugging
Check out the
tools/folder for more details
- Graceful shutdown and brownout-aware persistence
- Power-saving operational modes
- Local web dashboard
- OTA firmware updates
MIT License

