Kevinkv/#
Edge and device layer — design partner engagements

Field hardware on the same bus as everything else you run.

The platform includes an ESP32-S3 firmware framework, a MAC-keyed device registry and an over-the-air firmware path addressed by device identity rather than by IP address. Telemetry from physical hardware arrives on the same broker and the same topic namespace as the rest of the system. This is engineering work done with you, not a product you download and switch on.

does work stateful data third party initiates — this page is drawn in the platform's own diagram grammar

Why an inference platform has a device layer at all

At the edge the expensive question is rarely which model to use. It is whether the data reaches a model at all, and what it took to get there. A site with intermittent uplink, a radio link and no operator on the premises is an integration project before it is an AI project.

The managed alternatives are region-scoped, not premises-scoped. An inference endpoint in the nearest cloud region is not the same thing as one in the building, and none of the routing or serving vendors sell a device layer at all — that part of the problem is left to you and a separate IoT platform. Here the device layer, the event bus, the service fleet and the inference path are one system with one operational surface. That is worth something specific: telemetry is queryable next to service state without an integration between two vendors' data models.

Two hardware targets, today

Heltec WiFi LoRa 32 V3 and V4, both ESP32-S3. V4 adds native USB CDC, PSRAM and GPS; its PA front-end pins are reserved. The build targets are separate and never mixed.

firmware/mx — heltec_v3 · heltec_v4

Layered firmware, not a sketch

A hardware abstraction layer, a transport layer, a message framework and application managers, over FreeRTOS tasks. Managers are singletons; the main loop stays empty and work is timer, ISR or event driven.

lib/HAL · lib/Transport · lib/Mx · lib/App

Events, not polling

A two-tier heartbeat runs at 2 s when something is happening and 5 min when nothing is. It is kicked by real events — a command, a relay change, an HTTP hit — never by a periodic tick asking whether anything changed.

lib/App/heartbeat.cpp

Telemetry lands on the bus, not in a vendor silo

Every node below is a real module. The paths under them are real.

Field device

Publishes battery, RSSI, heap and uptime on its own topic. Offline status is an MQTT last-will, so a dead device announces itself.

lib/Transport/mqtt_transport.cpp

Your broker

Mosquitto or EMQX, on your network. Third-party and replaceable — the platform holds a topic contract, not a broker dependency.

Telemetry persister

Subscribes the device topics and owns the whole path: subscribe, persist, serve. No ingest agent, no external time-series database.

daemon/src/telemetry_store.py

Canonical fields

Raw device fields are mapped to canonical metric names and each carries its own unit label, so a chart does not have to hardcode which field is volts.

battery_mv → battery_voltage_v

Telemetry store

SQLite in write-ahead mode on your disk. One row per field per sample, indexed by device and by time.

KEVIN_HOME/db — table telemetry

Bus fan-out

The same sample is republished on the local event bus, so live operator views are a second reader of one ingest — not a second ingest.

device:telemetry

The wire contract

Topic patterns are generic subjects; domain meaning lives in the subscriber's config.

TopicDirection
kv/{node}/telemetrydevice → platform
kv/{node}/statusdevice → platform, as the last-will
kv/{node}/heartbeatdevice → platform
kv/{node}/cmdplatform → device
kv/{node}/msgdevice → platform, command ack
kv/class/{class}platform → one hardware class
kv/broadcast/commandplatform → fleet

Delivery guarantees are set per subject, not globally: telemetry is fire-and-forget because a dropped sample is cheaper than a stalled radio, status and heartbeat are at-least-once, and commands are exactly-once.

daemon/src/mqtt_config.py · firmware/mx/lib/Transport/mqtt_transport.h

Telemetry is telemetry. Decisions are elsewhere.

Battery voltage is reported by the firmware and acted on by nobody on the device. Mains-loss alerting is a rule in the daemon, where it can be changed without a reflash. The split is deliberate: firmware that makes policy decisions is firmware you have to reflash to change your mind, across every device, in the field.

Devices go offline honestly

A detector polls the registry every 30 s and marks devices whose heartbeat has gone stale past the configured timeout, publishing a fleet event when it does. An absent device is a state, not a gap in a chart.

fleet:device_offline

Firmware is delivered to an identity, not to an address

Hardcoded IP addresses are how fleets get bricked. A device is addressed by registry identity; the registry resolves what hardware it is and where it currently lives, and the hardware class it reports decides which build it is allowed to receive.

Flash request

Names a device, never an address.

POST /api/ota/flash
{"device_id": "KV-XXXX"}

Device registry

MAC-keyed. Holds hardware class, current firmware version, address and last-seen. Unknown device ID is a 404, not a guess.

daemon/src/device_registry.py

Class guard

Hardware class selects the build environment. A V3 image cannot be sent to a V4 device, because nothing in the path lets you name the image directly.

daemon/src/ota_manager.py

The sequence, end to end

StepInterface
Device boots and self-registers by MACPOST /api/registry/register
Unknown MAC is held for an operator to approvePOST /api/registry/devices/{id}/approve
Flash one device by identityPOST /api/ota/flash
Or every online device of one hardware classPOST /api/ota/flash/by-class
Poll the job log while it runsGET /api/ota/status/{job_id}
Read the version back off the deviceGET /api/version

Pull-style updates are supported: the daemon serves the compiled image and the device fetches it. A non-HTTPS update URL is rejected before any job is created, rather than failing somewhere inside a flash.

Version is the flash confirmation

There is one rule that makes fleet updates auditable rather than hopeful: if a device reports the same version after an upload, the flash failed. Not "probably succeeded". Failed. Firmware version strings carry their hardware target, and the repository holds independent counters per target so a V3 and a V4 build can never be confused for one another.

0.0.64V4  ·  0.0.64V3

What the registry row carries

One row per device, MAC as the durable key.

device_id · friendly_nameStable identity, human label.
mac_addressThe key that survives a DHCP lease change.
hardware_classDecides which build is legal for this device.
current_versionCompared against the readback after a flash.
ip_addressCurrent address. Derived, never authoritative.
status · last_seenOnline, offline, or awaiting approval.

New devices are not trusted automatically. An unrecognised MAC self-registers into a pending state and stays there until an operator approves it. Automatic approval exists as a configuration flag for lab benches; it is not the default.

Links for sites that do not have a network

A device that only speaks IP is useless in a field, a basement or a trailer. The transport layer treats radio, serial and IP links as interchangeable carriers of the same messages, and a message router moves traffic between them.

LoRa — SX1262

Long-range sub-GHz radio, no infrastructure required. Packets are encrypted with AES-128-GCM through the platform's crypto library and deduplicated against a rolling-hash window on receive. RSSI and SNR are reported per packet.

lib/Transport/lora_transport.cpp

ESP-NOW — peer to peer

MAC-addressed WiFi-band messaging with no access point and no association. The peer registry persists across reboots in non-volatile storage, with a fixed peer ceiling.

lib/Transport/espnow_transport.cpp

BLE and serial — the fallbacks

A GATT terminal for a phone standing next to the device, and USB serial for the bench. Both carry the same command set as the network transports, so commissioning does not need a working uplink.

lib/Transport/ble_transport.cpp

Any transport in, any transport out

The message router registers every active transport, polls them and dispatches packets between them. In practice that means one device with an uplink carries traffic for devices that have none: a radio message arrives on LoRa and leaves on MQTT, without either end knowing which link the other used.

lib/Transport/message_router.cpp

What this is not

It is not a routed multi-hop mesh. Radio links are point to point, plus the transport bridging above. A gossip dissemination layer exists in the tree but its transmit path is a stub and it is not wired to a radio — so it is not a capability, and we will not describe it as one. If your topology needs multi-hop, that is design work in the engagement, not a feature you are buying.

How this is sold: design partners, not seats

Everything above is real and running, and none of it is a self-serve product. Two variants of one board are supported. Your sensors, your enclosure, your radio plan and your site are not in the tree yet. So the engagement is scoped as non-recurring engineering against a defined site, with the work landing in the platform rather than in a fork.

1 — Site definition

What is measured, what is actuated, what the uplink actually is on a bad day, and what has to keep working when it is gone. Written down before anything is built.

2 — Bring-up on real hardware

Every firmware feature gets a spec with observable pass or fail criteria — a serial pattern or an HTTP response — and no change is accepted until those cases run on the bench.

3 — Fleet handover

Registry, OTA path and telemetry running on your hardware, operated by your people. The deployment does not depend on us continuing to exist.

Two design partners at a time, priced as engineering. We do not forecast revenue from this in the first year, and you should not plan around us shipping a general-purpose device product on a schedule.

Where this is the wrong choice

This is the deepest thing we have built and the slowest thing we sell. It is also the easiest to buy for the wrong reason. These are the cases where you should not.

Worth a conversation when…

  • You have physical sites where the uplink is unreliable or absent, and telemetry currently arrives by someone driving there.
  • You already want inference on premises, and the device data is the reason.
  • You are running firmware updates by hand, by IP address, and it has already gone wrong once.
  • You want device state and platform state queryable in the same place, without an integration between two vendors.
  • You can staff a design partnership — a person who knows the site and can answer questions for months.

Do not buy this when…

  • You need compliance attestations. There is no SOC 2, no HIPAA BAA and no FedRAMP authorization. In regulated procurement that is disqualifying and no architecture argument fixes it.
  • Your hardware is not an ESP32-S3. Two Heltec board variants are supported. Anything else is a port, and a port is months, not a configuration flag.
  • You need certified radios. We do not certify hardware. Band selection, duty cycle and any regulatory approval for your deployment sit with you.
  • You already have a working historian or SCADA platform. Buy inference and connect it. Replacing a working device layer to gain one substrate is a bad trade.
  • Per-device key management is a hard requirement today. The radio link ships with a build-time default pre-shared key and no per-device provisioning. It is overridable at build; it is not yet a key management system, and we would rather you hear that here.
  • Vendor survival is a gating risk in your review. We are small. The honest mitigation is structural — it runs on your hardware and the deployment outlives us — not a balance sheet.