ESP32 implementation of the Moqui PLC framework — a faithful C (MISRA C:2012) port of IEC 61131-3 Structured Text programs.
The Moqui IoT framework allows IEC 61131-3 Structured Text programs (function blocks, state machines, PID controllers, recipe loaders) to run on embedded MCUs with MQTT telemetry. This repository contains the ESP32 port built on ESP-IDF 6.2 and FreeRTOS.
The included HVAC application (runtime/component/mantle-hvac) is an example component and a collection of patterns for implementing custom runtime components — it shows how to structure device logic, recipe loading, rule engines, and test suites on top of the framework.
The framework is designed to be portable. See PORTING.md for STM32, Infineon, and TI targets.
Core 1 — Control Task (100 ms) Core 0 — IoT Task
MoquiStart_Update MqttClient_Task
<application>_Update drain pub queue -> broker
MqttLogAppender_Update
Platform Abstraction Layer (platform/platform.h)
platform/esp32/ ← swap to port to another MCU
All framework and runtime code sits above the PAL and has no ESP-IDF dependencies.
- Docker (no local toolchain needed)
mosquitto-clientsfor the MQTT test (apt install mosquitto-clients)- Docker Compose for the broker test (ships with
moqui-framework)
docker run --rm -v "$PWD":/project -w /project espressif/idf:latest \
/bin/bash -lc "idf.py build"Clean build artifacts (files inside the container are owned by root — use Docker to remove them):
docker run --rm -v "$PWD":/project espressif/idf:latest \
/bin/bash -lc "rm -rf /project/build /project/sdkconfig /project/sdkconfig.old"The HvacTestSuite validates the framework with 30 functional scenarios. No hardware or network required.
# 1. Merge binaries into a single QEMU flash image
docker run --rm -v "$PWD":/project -w /project espressif/idf:latest bash -lc "
cd /project/build &&
esptool.py --chip=esp32 merge_bin --output=qemu_flash.bin --pad-to-size=4MB \
--flash-mode dio --flash-freq 40m --flash-size 4MB \
0x1000 bootloader/bootloader.bin \
0x8000 partition_table/partition-table.bin \
0x10000 iot_firmware.bin"
# 2. Run on QEMU
docker run --rm -v "$PWD":/project -w /project espressif/idf:latest bash -lc "
timeout 60 qemu-system-xtensa -M esp32 -m 4M \
-drive file=/project/build/qemu_flash.bin,if=mtd,format=raw \
-global driver=timer.esp32.timg,property=wdt_disable,value=true \
-nographic 2>&1"Expected output ends with:
I (...) HvacTestSuite: All 30 HvacTestSuite scenarios PASSED!
This test exercises the full log path: LoggerFacade ring buffer -> MqttLogAppender -> broker.
Log persistence uses model identifiers directly. loggerName is the exact
owning Device.deviceId; an empty source produces a DeviceLog, while a
non-empty source is the exact pre-existing Parameter.parameterId and
produces a ParameterLog. The HVAC ParameterLogger emits the 29 modeled
numeric parameters once per clock1minute pulse after DeviceManager_Update.
The ring retains a complete snapshot across multiple ten-event MQTT batches.
QEMU does not emulate WiFi.
tools/qemu_log_bridge.shbridges QEMU stdout to the broker by converting eachESP_LOGline into aLogEventJSON published viamosquitto_pub.
1. Start ActiveMQ — the compose file is in the moqui-framework repository:
# From the moqui-framework docker/ directory
docker compose -f activemq-compose.yml up moqui-broker1 -d2. Subscribe (terminal 1):
mosquitto_sub -h localhost -p 1883 -u artemis -P artemis -t "moqui-log" -W 603. Run QEMU through the bridge (terminal 2):
docker run --rm --network=host -v "$PWD":/project -w /project espressif/idf:latest bash -lc "
timeout 30 qemu-system-xtensa -M esp32 -m 4M \
-drive file=/project/build/qemu_flash.bin,if=mtd,format=raw \
-global driver=timer.esp32.timg,property=wdt_disable,value=true \
-nographic 2>&1" | bash tools/qemu_log_bridge.sh localhost 1883Each log line arrives on moqui-log as:
{"id":"00000035","logger":"HvacTestSuite","level":2,"marker":0,"ts":2445,"src":"HvacTestSuite","msg":"pass[1]: Init -> Standby","rpt":0}The project ships with .vscode/launch.json containing three configurations:
| Configuration | When to use |
|---|---|
| ESP32 GDB Debug | Host GDB against the compiled ELF |
| ESP32 LLDB Debug | macOS / LLVM workflows |
| Build & Debug ESP32 | Builds first, then attaches Docker-based xtensa-esp32-elf-gdb |
Quick start:
- Build (
Ctrl+Shift+B-> build:esp32) - Open Run and Debug (
Ctrl+Shift+D), select a configuration, pressF5 - Breakpoints: click the gutter; conditional breakpoints: right-click -> Add Conditional Breakpoint
Debug controls: F5 continue · F10 step over · F11 step into · Shift+F11 step out
| Option | Default | Description |
|---|---|---|
MOQUI_MQTT_BROKER_URI |
mqtt://moqui-device-gateway:1883 |
Broker URI |
MOQUI_MQTT_DEVICE_TOPIC_PREFIX |
moqui/device |
Topic prefix; MAC address is appended automatically |
MOQUI_CONTROL_TASK_STACK |
8192 | Core 1 stack (bytes) |
IOT_FIRMWARE_TASK_STACK |
4096 | Core 0 MQTT stack (bytes) |
MOQUI_ENABLE_TEST_SUITE |
y |
Include HvacTestSuite; set to n for production builds |
| Target | Status |
|---|---|
| ESP32 (ESP-IDF 6.2) | Production — verified |
| STM32H7 / STM32L4 | Outlined — see PORTING.md |
| Infineon PSoC 6 / XMC | Outlined — see PORTING.md |
| TI C2000 | Out of scope (no OS, no TCP/IP) — see PORTING.md |
| Linux | Not supported — ESP-IDF 6.2 bug in linux target |