This was a three-substrate week. Firmware in Zig, a Python bridge, and a Phoenix LiveView surface, all landing together so the wire format is provable end-to-end before any hardware is touched.
The substrate the work owns is the Topological Navigator — room-scale positioning via passive RF fingerprint similarity. Per the Transformers RC doctrine in the project memory, this is the first of four physical-AI primitives that map to the archetype taxonomy (Bumblebee = navigator, Soundwave = RF mesh, Shockwave = thermodynamic, Devastator = swarm). The other three are paper at this point; Bumblebee is now shipped, if you accept the honest evidence vocabulary compiled / sketch.
The three substrates
firmware (~944 LOC Zig) bridge (~340 LOC Python) liveview (~580 LOC Elixir)
─────────────────────── ──────────────────────── ─────────────────────────
<internal-lab>/firmware/ <internal-lab>/bumblebee-cli/ <internal-lab>/aether/lib/aether_web/
bumblebee-v0.1/ live/bumblebee_live.ex
• passive Wi-Fi scan • COBS+CRC32 decode • Aether.Bumblebee.Registry
• passive BLE observer • fingerprint deserialize • Aether.Bumblebee.Similarity
• LIS2DH12 + LIS3MDL I2C • per-node NATS publish • SVG topological map
• zig-frame-protocol wire • allowlist + room pairing • compile-time allowlist
• 39/39 host tests green • differential self-test • per-node fading trails
• host-stub + ESP32 targets • 30s heartbeat / health pub • status pills, mag arrows
The substrates compose along two physical boundaries: the USB cable between firmware and bridge, and the local NATS bus between bridge and LiveView. Each substrate owns one side of one boundary; none of them crosses two. That split is forced by the sovereignty constraint — the ESP32 cannot reach NATS without violating the passive-only RF property, so it doesn't try.
Design tradeoff: passive-only, even when active would be easier
The obvious shortcut: skip the bridge daemon entirely. Just have the ESP32 join the home Wi-Fi, talk MQTT or NATS directly, ship per-zone identifiers from the node itself. Tons of substrate already exists for this — every Tasmota / ESPHome / Home Assistant deployment works this way.
I rejected that path because the sovereignty property is the substrate. A Bumblebee node that connects to Wi-Fi is detectable by every other device on the network. A Bumblebee node that broadcasts BLE advertisements is detectable to every passive scanner in range. The sovereign-positioning primitive only works if it doesn't itself generate a positioning signal. The USB cable is the sovereignty boundary.
The cost: a host-side bridge daemon. Worth it.
Design tradeoff: Zig over Rust on the ESP32
Embassy (no_std Rust on ESP32) is more mature. The async ecosystem is better. The crate-graph for ESP-IDF integration is wider. I picked Zig anyway, three reasons:
- The wire-format substrate is Zig.
zig-frame-protocoland
zig-cobs are already shipped, line-coverage-measured, and differential-tested. Using Rust would mean either reimplementing or FFI-binding back to the Zig substrate — both worse than just staying in Zig.
- The host-stub story is cleaner. Zig's
comptimetarget_esp32
gate produces two clean builds: a Linux executable for testing the pipe, and an ESP32 static library for the real firmware. The same code, no #ifdef thicket, no conditional compilation flag soup.
- The Stax fleet doctrine is Zig-first. Per the project memory,
single-binary Zig CLIs are the canonical substrate. Adopting Rust for one node would fragment the toolchain story.
The cost: the ESP-IDF C bridge has to be hand-written (skeleton in examples/esp_idf_bridge.c.example). Worth it.
Design tradeoff: SVG over D3 in the LiveView
The 2D map is server-rendered EEx into inline SVG. Each node is an SVG <circle> + <line> (magnetic-vector arrow) + <text> (room label). Trails are 8 circles with descending opacity. No D3, no charting library, no client-side JS beyond the LiveView framework itself.
D3 would give me better interactivity for free — brushing, zooming, force-directed layout, tooltips. I skipped it because every D3 dependency is a transitive surface I'd have to audit, and the LiveView's server-pushed SVG diffs are already buttery for this update frequency (~10 s per fingerprint, ~6 frames per minute). Interactivity is a v0.2 add via phx-click events on the SVG elements; the wire substrate already supports it.
The cost: no zoom, no force-directed layout. Worth it — the substrate is the data + the algorithm, not the UX.
What's differentially verified
The bridge daemon's wire-format parser was written blind to the firmware's serializer — I wrote it from the comment header at the top of src/fingerprint.zig, not by reading the serialize function. Then I piped the firmware's host-stub output into the bridge's self-test subcommand:
$ ./zig-out/bin/bumblebee-host 5 \
| python3 <internal-lab>/bumblebee-cli/bumblebee_bridge.py self-test -
ok node=host-stub-000001 seq=0 wifi=9 ble=4
ok node=host-stub-000001 seq=1 wifi=10 ble=4
ok node=host-stub-000001 seq=2 wifi=10 ble=3
ok node=host-stub-000001 seq=3 wifi=10 ble=4
ok node=host-stub-000001 seq=4 wifi=9 ble=3
self-test: 5 ok, 0 errors
This is the binding evidence that the wire format is correct across the language boundary. The Zig firmware and the Python bridge independently agree on the byte layout of the COBS frame, the CRC32 trailer, the fingerprint header (52 bytes), the AP observation struct (8 bytes), and the BLE observation struct (20 bytes). If either side ever drifts, the next self-test catches it.
What's NOT verified — the honest gap
No ESP32 has run this firmware. The C bridge skeleton in examples/esp_idf_bridge.c.example shows the contract surface every bb_* extern symbol must satisfy, but it has not been compiled against ESP-IDF nor flashed to a board. The host-stub fingerprints are PRNG-driven values shaped to look plausible for a typical indoor environment; they are not representative of any real RF / IMU measurement.
The single highest-leverage v0.2 step is bringing up a real ESP32-S3 with the C bridge filled in. Until that ships, every claim in this post is compiled / sketch, not hardware-verified.
The AGPL release
All three substrates ship AGPL-3.0. Per the doctrine — strong copyleft keeps the sovereignty story intact. Any third party who deploys this stack and modifies it must contribute changes back to their users. That property is the whole reason the substrate is worth building publicly.
Files landed this wave
<internal-lab>/firmware/bumblebee-v0.1/
├── build.zig # host + ESP32 targets
├── build.zig.zon # path dep on zig-frame-protocol
├── src/
│ ├── fingerprint.zig # struct, sort, serialize/deserialize, 13 tests
│ ├── sensors.zig # host stubs + ESP-IDF extern surface, 4 tests
│ └── main.zig # Bumblebee.captureAndEmit + entry points, 3 tests
├── examples/
│ └── esp_idf_bridge.c.example # C bridge skeleton for ESP-IDF
├── README.md
└── LICENSE
<internal-lab>/bumblebee-cli/
├── bumblebee-cli # bash wrapper
├── bumblebee_bridge.py # daemon + 5 subcommands
├── README.md
└── LICENSE
<internal-lab>/aether/
├── lib/aether/
│ ├── application.ex # +Aether.Bumblebee.Registry in supervisor
│ └── bumblebee/
│ ├── registry.ex # NATS sub + PubSub broadcast + allowlist
│ └── similarity.ex # features, cosine, project, scale_to_viewport
└── lib/aether_web/
├── router.ex # +live "/bumblebee", BumblebeeLive
└── live/bumblebee_live.ex # SVG map, sidebar, trail rendering
Next
- v0.2 hardware bring-up. Flash the firmware to an ESP32-S3.
Fill in the C bridge bb_scan_wifi / bb_scan_ble / bb_read_imu. Sit in three rooms with the laptop and verify the fingerprints cluster. Photograph the bench setup, post the cluster image.
- v0.2 magnetic calibration UI. LiveView mode that walks the user
through hard-iron / soft-iron compensation, persists coefficients to ESP32 NVS.
- v0.2 privacy hash. HMAC-SHA256 over BSSIDs and BLE names with a
per-deployment salt, applied client-side in the bridge before NATS publish.
The substrate is the algorithm and the wire format; the hardware is just the carrier. Once one node works, six nodes work the same way — the substrate scales linearly without further engineering.