Sandboxed Lua state (espressif/lua v5.5.0, base/table/string/math only, no io/os/package/debug) exposes a kvida API table (chip_temperature, set_led, publish) to a compiled-in default script that reads chip temperature, maps it to an LED color, and publishes the reading to Home Assistant. Generalizes transport's publish_chip_temperature() into publish_value(name, value), the first real use of AGENTS.md's semantic publish API. Requires -DLUA_32BITS globally since this toolchain's long long support isn't visible to Lua's default build. Verified on real ESP32-C6 hardware: no crashes/errors across multiple serial monitor windows, LED changes color, chip_temperature sensor appears in Home Assistant via MQTT Discovery. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
43 lines
6.7 KiB
Markdown
43 lines
6.7 KiB
Markdown
# transport
|
|
|
|
Wi-Fi connectivity, MQTT client, and Home Assistant MQTT Discovery payloads. Exposes a semantic `publish(topic, value)`-style API — application code (including Lua) never depends on MQTT directly, per the transport abstraction principle in `AGENTS.md`. No dependency on other Kvida components.
|
|
|
|
Also owns Wi-Fi/MQTT configuration storage for now (not broken out into its own component since `AGENTS.md` doesn't call out a separate config layer — revisit if this grows).
|
|
|
|
Future transports (Zigbee, Thread, Matter) should implement the same publish API as alternate backends behind this component's interface, without changing callers.
|
|
|
|
## Wi-Fi commissioning
|
|
|
|
`include/commissioning.h` / `src/commissioning.cpp` implement first-time Wi-Fi setup, no companion app required:
|
|
|
|
- On boot, `try_stored_credentials()` attempts to connect using credentials saved in NVS from a previous session.
|
|
- If none exist (or the caller chooses to), `start_commissioning()` opens a SoftAP (`Kvida-XXXXXX`, derived from the MAC) with a captive portal: a DNS server (`src/dns_server.c`/`.h`, vendored from ESP-IDF's official `captive_portal` example, Unlicense/CC0) answers every query with the AP's own IP, and DHCP option 114 plus a 404-redirects-to-`/` HTTP handler get most phones/laptops to auto-open the setup page (`src/portal.html`) in a plain browser.
|
|
- Submitting the form (`POST /api/wifi`, JSON body `{"ssid","password"}`) saves the credentials to NVS and switches Wi-Fi to STA mode. The AP is open (no password) by design -- see the comment above `ap_config.ap.authmode` in `commissioning.cpp` for the reasoning and the tradeoff.
|
|
- A 5-minute `esp_timer` closes the commissioning window automatically if nobody submits credentials.
|
|
- **IPv6**: once STA gets its IPv4 address (`IP_EVENT_STA_GOT_IP` -- not `WIFI_EVENT_STA_CONNECTED`, see the comment in `commissioning.cpp` for why that timing matters), `esp_netif_create_ip6_linklocal()` requests a link-local address (`CONFIG_LWIP_IPV6=y` is set explicitly in `sdkconfig.defaults`); `IP_EVENT_GOT_IP6` is logged when it arrives. Verified end-to-end on real ESP32-C6 hardware.
|
|
- Not yet wired up: re-entering commissioning on an already-configured device needs a physical trigger (the user button) per AGENTS.md's "configuration mode requires physical user interaction" principle -- blocked on `drivers` having a real button implementation. Network scanning (SSID dropdown instead of free text) was left out of this first pass to keep scope tight.
|
|
|
|
## MQTT + Home Assistant Discovery
|
|
|
|
`include/mqtt.h` / `src/mqtt.cpp`. Neither `esp_mqtt_client` (`mqtt_client.h`) nor `mdns.h` ship with ESP-IDF core in this version (both were moved to the IDF Component Registry) -- pulled in via `idf_component.yml` (`espressif/mqtt`, `mdns`), the project's first external managed dependencies.
|
|
|
|
- Once Wi-Fi STA has an IPv4 address, `start_mqtt()` advertises the device on the LAN as `kvida-xxxxxx.local` (mDNS hostname, matching the commissioning AP's SSID) and starts a persistent settings HTTP server there -- the ongoing configuration channel for anything beyond the one-time Wi-Fi bootstrap (MQTT broker/credentials now, more later).
|
|
- **Broker address**: the settings page has a required host/port field, used directly and given priority over mDNS auto-discovery. mDNS discovery of `_mqtt._tcp` (`mdns_query_ptr`) is attempted as a fallback only if no manual host is saved, retried every 30s -- **verified against a real Home Assistant + Mosquitto add-on setup that this mDNS discovery does not reliably find the broker** (confirmed with a from-scratch raw mDNS query test from the host machine: no PTR answer for `_mqtt._tcp.local` was ever received on that network), hence the manual field being the primary path rather than a rarely-needed fallback.
|
|
- **Credentials**: also collected on the same settings page (blank username = anonymous; blank password on a resubmit means "keep the current one", since the password is never echoed back into the page for basic hygiene). Real brokers, including Home Assistant's Mosquitto add-on, require auth -- confirmed on real hardware.
|
|
- On `MQTT_EVENT_CONNECTED`: publishes (retained) an HA MQTT Discovery config for a diagnostic `connectivity` binary_sensor, grouped under a `device` block (`identifiers`/`name` = the same `kvida-xxxxxx` id, `manufacturer` "Xylon", `model` "Kvida"), then publishes `online` to the availability topic. MQTT's own LWT (`session.last_will`, set at client init) publishes `offline` to the same topic if the connection drops. Verified end-to-end: device appears in Home Assistant with a "Connected" Connectivity sensor.
|
|
- **`publish_value(name, value)`**: the generic semantic publish API, finally built once there was a real caller -- `lua_runtime`'s `kvida.publish()`, not called directly by other components. Builds `kvida/<id>/<name>` (state) and `homeassistant/sensor/<id>/<name>/config` (discovery) topics from the name. No `device_class`/`unit_of_measurement` or prettified HA display name, since neither can be inferred from just a name -- the entity shows up in HA labeled with the raw name (e.g. `chip_temperature`).
|
|
|
|
## JSON API
|
|
|
|
Both httpd servers (the commissioning AP's and the settings server's) expose a small JSON API, CORS-enabled (`src/http_cors.h`) so a separately-hosted web app (`kvida-sdk`, e.g. via `npm run dev`) can call them from a browser across origins -- not just the on-device pages, which use the same endpoints via `fetch()`:
|
|
|
|
- `POST /api/wifi` (commissioning AP only) -- `{"ssid","password"}`, replaces the old form-urlencoded `/connect`.
|
|
- `GET /api/status` (settings server) -- `{"device_id","wifi_connected","mqtt_connected"}`.
|
|
- `GET /api/mqtt` / `POST /api/mqtt` (settings server) -- `{"host","port","username"[,"password"]}`; `GET` never returns the password.
|
|
|
|
No JSON library ships with ESP-IDF v6.0.2 core (verified: no `cJSON`, no bundled `json` component). Given these payloads are tiny, flat, fixed-shape objects, `src/json_helpers.h` hand-rolls minimal `strstr`-based extraction rather than adding a 4th external registry dependency (already have `mdns`, `mqtt`, `led_strip`) -- explicitly not a general parser (no nesting, arrays, or escaping beyond what these specific request bodies need).
|
|
|
|
**Important constraint that shaped this**: the Wi-Fi captive portal must keep being served by the device itself -- a phone connected to the isolated SoftAP has no route to any externally-run app. Only the MQTT/settings step (once the device has real LAN connectivity) can be driven by a separately-run web app. See `kvida-sdk`.
|
|
|
|
TODO: A/B OTA update-checking belongs here, driven by ESP-IDF's native `esp_ota_ops` against the `ota_0`/`ota_1` partitions in `partitions.csv` — no extra dependency needed either.
|