Skip to content

Architecture

HeatSync is a single-binary ESP32 sketch. The code is split into themed modules under src/ so each file has a small surface and there’s an obvious place to grow.

samsung_controller.ino ← entry point, holds the loop()
config.h ← feature flags + hardware pin defines
src/
├── nasa/ ← Vendored upstream parser (MIT, from omerfaruk-aran)
├── bus/ ← RS485 driver + protocol target + sniffer
│ ├── bus.cpp/.h TX/RX, statistics
│ ├── bus_sniffer.cpp/.h ring of recent (source, msgNo) tuples
│ └── serial_target.cpp/.h base implementation of MessageTarget
├── ha/ ← MQTT + HA discovery + live state
│ ├── ha_bridge.cpp/.h Inherits SerialBusTarget; publishes to MQTT
│ ├── mqtt_client.cpp/.h thin wrapper over PubSubClient
│ ├── live_state.cpp/.h per-address mirror of latest values (for /api/live)
│ └── device_names.cpp/.h friendly-name overrides
├── ble/ ← Passive BLE scan + room-temp injection
│ ├── ble_sensor.cpp/.h NimBLE scanner (gated on HEATSYNC_BLE_ENABLED)
│ └── room_temp_injector.cpp/.h pushes BLE-sourced temps onto the bus
├── net/ ← WiFi + captive portal
│ ├── wifi_setup.cpp/.h NTP + IPv6 + mDNS init
│ └── captive_portal.cpp/.h first-boot wizard
├── web/ ← HTTP server + UI assets
│ ├── web_server.cpp/.h routes for all HTML pages + REST API + FSV poller
│ ├── ui_styles.cpp/.h shared CSS embedded as PROGMEM
│ └── ui_demo.cpp demo-mode shim (?demo=1)
├── hw/ ← Hardware drivers
│ ├── button.cpp/.h gesture detection on the Atom's onboard button
│ ├── status_led.cpp/.h WS2812 driver + LED state machine
│ └── event_log.cpp/.h 80-entry in-RAM ring for /api/events
└── core/
└── persistence.cpp/.h NVS load/save for HvacConfig
F1/F2 ──[UART2 9600 8E1]──→ bus.cpp:loopBus()
process_data() in src/nasa/protocol.cpp
MessageSets dispatched to MessageTarget
HaBridge::set_*() (inherits SerialBusTarget)
┌───────────────┼───────────────┐
▼ ▼ ▼
LiveDeviceState MQTT publish publishField(addr, key)
(in-RAM mirror) (HA discovery)
/api/live JSON ← consumed by dashboard polling every 2s
Web UI / HA command topic
handleApi…() in web_server.cpp
│ fills a ProtocolRequest, calls publish_request()
NasaProtocol::publish_request()
│ enqueues into outgoing_queue_
│ ┌── tick every 200 ms from loopBus()
▼ ▼
NasaProtocol::protocol_update()
│ builds a Packet (DataType::Request), calls publish_data()
HaBridge::publish_data() (overrides SerialBusTarget)
bus.cpp:transmitBytes() — waits for bus quiet, drives UART2 TX

The codebase’s growth pattern is consistent — each new feature follows the same shape:

  1. Capture data in ha_bridge.cpp. Add a case to the set_custom_sensor switch (or a dedicated setter override) for the register, plus a field in LiveDeviceState if you want it persisted.
  2. Expose to HA by adding a sensor(...) or bsensor(...) line in maybePublishDiscovery(). The discovery payload tells HA how to render it (device class, unit, state class).
  3. Expose to the dashboard by adding a row inside the relevant block function (dhwBlock, heatingBlock, outdoorBlock) in web_server.cpp and pulling the field from /api/live.
  4. Add controls by writing a handleApiX function in web_server.cpp (auth + writes-enabled + targets the indoor unit address), registering the route, and adding a click handler in the dashboard JS that POSTs to it.

The schedule, DHW power, target stepper, mode pickers, water-law offset and fault detector all follow this pattern.

There’s no FreeRTOS task per module — HeatSync runs everything from the Arduino loop() on the single core. Each subsystem’s loop*() call is non-blocking and ticks once per loop() iteration.

Background ticks with their own scheduling:

TickPeriodOwner
Bus RX / NASA decoderevery loop iterationloopBus()
NASA outgoing-queue drain200 msloopBus()
HTTP server handleClient()every loop iterationloopWebServer()
FSV poller (one read per tick)250 msloopWebServer()
DHW schedule decision60 sloopWebServer()
MQTT publish/reconnectevery loop iteration (PubSubClient handles)mqtt::loop()
LED stateevery loop iterationupdateStatusLed()
Button gesturesevery loop iterationupdateButton()

This means latency-sensitive things (button press, bus RX) get serviced within ~milliseconds; lower-priority things (FSV poll, schedule decision) are batched by their tick interval to keep the loop fast.

  • Flash (PROGMEM): firmware binary + all HTML/CSS/JS strings. Currently ~77% of 1.97 MB on the OTA-friendly min_spiffs partition scheme.
  • DRAM (heap): WiFi/lwIP stack (~50 KB), web server (~10 KB), live state map, sniffer ring (~8 KB for 256 entries). Free heap typically 175 KB+ at steady state since dropping the optional BLE module.
  • NVS: HvacConfig struct — credentials, MQTT settings, schedule parameters. ~1 KB.

The contents of src/nasa/ are imported from omerfaruk-aran/esphome_samsung_hvac_bus under MIT — see src/nasa/UPSTREAM.md for the import diff. Two small HeatSync-specific extensions are flagged inline in that source: ProtocolRequest::room_temp and ProtocolRequest::water_law_offset.

Everything else under src/ is HeatSync’s own code.