An ESP32 (TTGO LoRa32) gateway that receives JSON sensor readings over LoRa and publishes them to MQTT with Home Assistant MQTT-discovery, so each node shows up automatically as a device with its own sensors.
Every push to main automatically builds the firmware and attaches it as a
downloadable artifact — no local PlatformIO setup needed to get a flashable
binary. The badge above shows whether the latest build succeeded.
This is an application-image update, not a full/blank-chip flash. The
artifact contains only firmware.bin — no bootloader or partition-table
binaries — so it only works on a board that already has this project's
bootloader and partition layout on it (e.g. one that's ever been flashed via
pio run -e ttgo-lora32-v21 -t upload, which writes all three at first
flash). Setting up a brand-new/erased board still needs that full PlatformIO
upload once; after that, this artifact can be used for updates.
- Open the
build-artifact.ymlworkflow runs page. - Click the topmost run (it's the latest build off
main; a green check means it succeeded). - Scroll down to the Artifacts section at the bottom of the run page.
- Download the
firmware-<commit-sha>zip and unzip it — it containsfirmware.bin. Flash it at the application offset (0x10000on this board) withesptool.py --chip esp32 write_flash 0x10000 firmware.bin, or drop it in place of.pio/build/ttgo-lora32-v21/firmware.binand run the PlatformIO upload tools.
Note: downloading workflow artifacts requires being signed in to GitHub, since that's a GitHub-wide restriction on Actions artifacts, not something this repo can turn off.
Battery
Humidity
Low Battery
Signal
Temperature

Temperature/Humidity go unknown in Home Assistant (rather than showing a
stale or garbage value) whenever the sensor node reports a failed DHT read.
Boot Count Error Sequence
The gateway also publishes its own diagnostic device, covering:
- WiFi Signal
- Free Memory
- Packets Received
- Queue Depth — messages currently held because MQTT is unreachable (see "Resilience" below)
- Packets Dropped — messages permanently discarded because that queue filled up while still offline
- Store-and-forward MQTT queue. If the MQTT broker is unreachable, LoRa readings (and Home Assistant discovery messages) are queued instead of dropped, then flushed once the broker is reachable again. The queue is bounded (20 messages); if it fills while still offline, the oldest entry is dropped to make room for the newest.
- Non-blocking WiFi + MQTT reconnect. Both connections are retried on their own backoff timers without blocking LoRa packet reception, so a dropped router/AP or broker doesn't stall the gateway.
- Untrusted-input hardening. LoRa has no authentication, so a node id is treated as attacker-controlled: it's sanitized before use in MQTT topics, escaped before rendering on the device-management web page, and restricted to Home Assistant's stricter discovery-topic character set before being used in a discovery topic/unique ID — with a stable, collision-resistant fallback for ids containing characters outside that set (e.g. from a rare over-the-air bit error).
- Device management. New nodes appear as "pending" at
http://<gateway-ip>/devicesuntil approved, so only known nodes get published and auto-discovered.
The gateway's LoRa/MQTT/WiFi/allowlist logic is separated from the ESP32 hardware so it can be unit tested off-target:
include/payload_parser.h/src/payload_parser.cpp— pure payload parsing, routing decisions, the node allowlist, and MQTT/HA message builders. No Arduino dependency.include/hal.h/include/hal_esp32.h/src/hal_esp32.cpp— hardware interfaces (LoRa, MQTT, WiFi, display, storage, clock) and their thin ESP32 implementations.include/orchestrator.h/src/orchestrator.cpp—GatewayOrchestrator, which drives LoRa ingestion, routing, publishing, the store-and-forward queue, and WiFi/MQTT reconnect via the HAL interfaces above.src/main.cpp— wires the concrete ESP32 adapters together;loop()mostly just feeds the watchdog and callsorchestrator.tick().
This is a PlatformIO project.
# Native unit tests (no hardware required)
pio test -e native
# Build firmware for the TTGO LoRa32 v2.1
pio run -e ttgo-lora32-v21Sensor repo: https://github.com/GuvHas/loratemp