README preview — how these render on GitHub (temp page):front pageble-listenersht30-nodeds18b20-nodesdi12-readermodbus-readeranalog-readercwsi-nodeloadcell-water

OAT BLE Sensor Listener

Hear cheap Bluetooth thermometers; push the readings to a place you own.

An ESP32 BLE listener gateway hearing nearby Bluetooth sensors

Status Liveflash from the browser, no IDE needed
Hears Govee, Xiaomi, Inkbird, Ruuvi, SwitchBot + ~120 device types (Theengs Decoder)
Chips ESP32 · S3 · C3 · C6
Delivers Webhook (HTTP POST) · MQTT — oat-ods/0.3
License GPL-family (via the Theengs Decoder dependency — see below)

The first entry in the OAT Sketch Library, and the template the rest follow. A self-configuring ESP32 node that listens for BLE sensor broadcasts (Govee, Xiaomi, Inkbird, Ruuvi, SwitchBot and ~120 other devices via Theengs Decoder), harvests every measurand each device emits — temperature, humidity, soil moisture, conductivity, CO₂, PM2.5, illuminance, pressure, and the rest — folds the chatty readings into steady values, and pushes them to an endpoint the grower owns — by webhook or MQTT. What each key means is defined once in the shared measurand dictionary (oat-measurands.yamloat_measurands.h).

A grower flashes it from the browser at openagriculturetechnology.com/build/sketches/ble-sensor-listener/ (ESP Web Tools / Web Serial), then configures it on the device's own captive portal. No IDE, no command line, no code editing.

Files

File What it is
oat_ble_listener.ino The firmware source (Arduino / ESP32).
platformio.ini Multi-chip build matrix (ESP32, S3, C3, C6). Pinned libs.
merge_bin.py Post-build hook → one merged factory image per chip at out/firmware-<mcu>.bin, flashable at offset 0.
make_manifest.py Assembles manifest.json from whatever bins exist (data-driven).
build.sh Builds every chip, copies bins + manifest into the site assets.

Build

PlatformIO core only — no Arduino IDE:

pipx install platformio        # or: pip install --user platformio
./build.sh

build.sh produces one merged, flash-at-offset-0 image per chip in out/, plus the manifest.json the OAT site's flash-from-browser page serves.

First-build knob: if pio run can't resolve the platform, bump the pioarduino release pin in platformio.ini (it tracks Arduino-ESP32 3.x, which NimBLE 2.x and the native-USB chips need). That pin is the one thing to verify.

The chip matrix — support broad, recommend narrow

Built for every WiFi+BLE ESP32; the site's buy-list recommends the native-USB ones (no driver — the #1 newbie wall):

Chip USB In the build Recommended to buy
ESP32 (classic) external bridge (CP2102/CH340) yes no (driver friction)
ESP32-S3 native yes yes
ESP32-C3 native yes yes (cheapest)
ESP32-C6 native yes yes (WiFi 6)
ESP32-S2 / H2 / P4 no — (no BLE / no WiFi / no radio)

One ESP Web Tools button serves them all: it reads the chip and auto-picks the matching build. Adding a chip = add an env in platformio.ini; make_manifest.py picks it up.

What it sends — oat-ods/0.3

A signed JSON batch: a shared source (assignable gateway id, tier, fw) plus a messages array of per-measurement records. Each record carries its logical stream (the sensor), the canonical measurement + SenML unit, the folded value, an agg block (window, samples, method), and swappable source provenance (physical MAC, brand, model, battery, rssi). One record per measurand — a flower-care sensor emits temperature + soil_moisture + soil_conductivity + illuminance + battery, not just temp.

Units are canonical on the wire (°C, %RH, ppm, lx …); the endpoint derives dewpoint and VPD from temperature + humidity. Big fan-ins are chunked into bounded, independently-signed POSTs (chip-aware) so the body + JSON DOM + TLS block always fit in heap. MQTT sends one small retained message per measurement. A decoder key the dictionary hasn't mapped is forwarded raw (no unit) and listed on /status so it can be promoted. The schema id travels in the packet so the endpoint can version it.

Gotchas (also in the sketch header)

  1. ArduinoJson flag alignment with TheengsDecoder (you may need to drop ARDUINOJSON_USE_LONG_LONG=1). https://decoder.theengs.io/use/include.html
  2. PubSubClient's 256-byte default buffer is too small — set at build + runtime.
  3. NimBLE 2.x API (differs from 1.x). Paired with Arduino-ESP32 3.x.
  4. TLS uses setInsecure() for newbie-friendly first contact; pin a cert for production.

License

TheengsDecoder is GPL-family, so a distributed binary inherits that obligation — which is exactly why this source ships openly. Unlike its Apache-2.0 siblings in this library, treat this sketch as GPL-governed when you distribute builds of it. Keep it open; it's the right posture for OAT anyway.


Docs: CC BY 4.0 · An OAT sketch — an OpenCDC initiative.