Test bench for OpenTherm on the KC1245 gateway and the PCB1246 OpenTherm plug-in board, built on the Strux framework. The goal is a rig that simulates both ends of the OpenTherm bus, a thermostat and a boiler, so OpenTherm devices and the gateway can be tested without real equipment. Work in progress. Lives at KooleControls/KC-OT-Testbench.
gateway/: ESP32 firmware for the KC1245 gateway. A copy of Strux at commit3c2e4ca.opentherm/: LPC11U68 firmware for the PCB1246 (bare metal, no RTOS, Strux-style). Laid out like the gateway: the board, the Strux command/settings/system managers, and the OpenTherm modem with its link to the gateway.
Each folder holds its full source; nothing is shared between the two.
Decided so far:
- The LPC runs bare metal: one main loop, interrupts only set flags and fill buffers. No RTOS.
- The gateway and the PCB1246 talk over the UART using the Strux channel protocol instead of KC2.
- On the UART, each channel frame is COBS-framed with a CRC-16:
0x00 | COBS(frame | crc16) | 0x00. A frame that fails the check is dropped and counted. The code islib/protocol/SerialFraming.h, meant to be the same file on both sides. - The PCB1246 is a modem and does not look inside a frame. Frames from either line go to the gateway on a stream the PCB1246 opens (
opentherm stream); the gateway sends withopentherm send side=<thermostat|boiler> frame=<uint32>. Everything else is the gateway's. - The gateway keeps the PCB1246's firmware up to date. A release builds
opentherm/first and links its.bininto the gateway app, so one gateway OTA updates both boards. At boot,ModuleFirmwareManagerresets the module into its ISP bootloader and compares the 32-byte image info at 0x300 (version, length, CRC-32, written bytools/lpc_image_info.py) with the image it carries. On a mismatch it flashes, with page 0 written last.module status/module update; themodule.updatesetting turns the boot check off. - The gateway's end of the modem link is
ModuleLinkManager. It runs the same framing and handshake as the module, sendsopentherm sendrequests, and records the module's frame stream, plus what it sent, in a ring of the last 256 frames. The web UI's OpenTherm page polls that ring withmodule frames after=<seq>, decodes each frame for display (frontend/src/lib/opentherm.ts), and can compose and send one. The module's UART is shared withModuleFirmwareManagerunder its lock; after every ISP session the link starts over. - How the thermostat and boiler simulation works is still open.
Gateway, board pcb1245, with ESP-IDF 6.0:
cd gateway
idf.py -DBOARD=pcb1245 set-target esp32
idf.py -DBOARD=pcb1245 buildOpenTherm board, board pcb1246, with the Arm GNU Toolchain (arm-none-eabi-gcc, on PATH or in its default Windows install folder), CMake 3.20+, Ninja and Python 3:
cd opentherm
cmake -S . -B build -G Ninja
cmake --build buildOutput in opentherm/build/: opentherm.elf, .hex, .bin and .map, with the LPC boot checksum and the image info already patched in.
Build opentherm/ before gateway/: the gateway build embeds opentherm/build/opentherm.bin when it exists, and warns and leaves the module alone when it does not (-DPCB1246_FIRMWARE=<path> points it elsewhere).
Push a tag vX.Y.Z (or vX.Y.Z-rc.N for a pre-release). .github/workflows/release.yml builds both boards, checks that both report the tag's version, and publishes the gateway app and factory images plus the PCB1246 .bin/.hex. Running the workflow by hand builds the same files as artifacts without releasing.