Repository navigation
FUG-107: auto hardware discovery — qwiic sensor drivers in the effects VM - #96
Open
issuefleet[bot] wants to merge 15 commits into
Open
issuefleet[bot] wants to merge 15 commits into
issuefleet[bot] wants to merge 15 commits into
Conversation
A sensor driver is ordinary .fxb bytecode with a poll() entry point that talks to a qwiic module over a new I2cBus trait and writes readings into `export` state slots. Same VM, sandboxing and perf model as effects. fx_vm: - I2cBus trait (register-oriented; firmware backs it with ESP-IDF I2C, host/browser with a mock register map). - I2cWrite / I2cRead opcodes, threaded through run() as Option<&mut dyn I2cBus> (None for shade/update — inert there). - .fxb v2 header adds poll_entry (v1 still parses, poll = NO_ENTRY). - Vm::run_poll() runs poll() with a bus; Vm::export() reads results back. fx_compiler: - `export <type> name "unit";` decls (state-backed) + exports manifest. - `poll()` recognized as a void entry; a program is an effect (shade) XOR a driver (poll), never both; export requires a driver. - i2c_write / i2c_read8 / i2c_read16 builtins. - v2 .fxb emission + disassembler poll label; filled in the missing ABS_I..MIX_FIX disasm arms (were truncating disasm) + I2C ops. - Lexer: decode string literals as UTF-8 (units like "°C" no longer mangled). Host-tested end-to-end against a mock I2C bus: read into exports, write config reg, bus-error (-1/0), effect/driver exclusivity, v2 back-compat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire contract for auto hardware discovery:
- ScanI2c -> I2cScanResult{addresses}: enumerate the qwiic bus.
- SubmitDriver{fxb, poll_interval_ms, bindings[], activate} -> DriverState:
upload a compiled driver; DriverBinding maps a driver export slot to an
active-effect uniform slot.
- RemoveDriver -> DriverState.
New ClientMessage arms 32-34, ServerMessage arms 19-20. micropb caps:
I2cScanResult.addresses=128 (0x08..0x77 space), SubmitDriver.bindings
hand-walked on firmware (cap 1) like SubmitEffect. Player core replies
"unsupported" (the fx/ffi layer intercepts these); exhaustive-match arms
added in player and the phone_client_frames conformance test. TS bindings
regenerated. conformance_test + phone_client_frames_test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ffi.rs: - ARM 32/33/34 dispatch: handle_scan_i2c, handle_submit_driver (hand-walks fxb/interval/bindings/activate like submit_effect), handle_remove_driver. - A second fx_vm instance (DRV_VM) runs the driver's poll() off the render loop via lm_drv_poll(now_ms) — self-paced to poll_interval_ms — backed by an I2cBus that calls the C++ qwiic hooks. Each poll copies exports into the active effect's uniform slots per the app-supplied bindings (the same path set_uniforms uses). lm_drv_running() gates the call. main.cpp: bring up the qwiic I2C bus (driver/i2c.h, legacy master API) on overridable pins (default SDA=6/SCL=7 @100khz); implement lm_i2c_write / lm_i2c_read (register-pointer + repeated-start) / lm_i2c_scan (probe 0x08..0x77); i2c_init() in setup(); lm_drv_poll() in loop() under player_mutex. player_ffi.h: driver + I2C hook prototypes. Build-verified: `bazel build -c opt //firmware/player_app:esp32c6` links clean. Real-sensor bring-up is hardware-pending (no qwiic module on the bench). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The authoring side of auto hardware discovery: - hardware/sensorDb.ts: a curated qwiic module DB keyed by I2C address, with identifyScan() to turn a bus scan into per-address candidate lists (unit tested). Address collisions surface all matches for the user to disambiguate. - hardware/driverBindings.ts: computeBindings() matches a driver's exports to the effect's uniforms by name+width → the submit_driver bindings (tested). - net/client.ts: scanI2c / submitDriver / removeDriver request methods. - net/proto.ts: the three new client arms + two server arms + flat shapes. - fx compiler wasm now returns the export manifest (fx/preview FxExport, compile-worker plumbing). - effects/ai: DRIVER_TOOLS (scan_bus, set_driver) + driver-system-prompt.ts teaching the model the driver dialect (poll/export/i2c_*, bind-by-name), wired into the editor's AI chat when a device is connected — the model can scan the bus, identify the part, write a driver, compile it, bind it, and install it. Host ffi_test extended to drive scan_i2c → submit_driver → poll → remove_driver end-to-end; SubmitDriver.fxb host micropb cap sized like UploadChunk.payload (firmware hand-walks it). Full web app type-checks; 67 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New "Sensor drivers (auto hardware discovery)" section: the poll()/export/ i2c_* driver dialect, the export→uniform bind-by-name bridge, the I2cBus seam (IDF on device, mock on host/browser), the .fxb v2 header, the scan/submit wire path, and the editor AI's scan_bus/set_driver tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
|
…oom) The HITL lane (BLE-provision → wss TLS handshake) is heap/.bss-sensitive on the C6 — the arena was trimmed specifically because that headroom gates the mbedtls record-buffer alloc. The initial firmware paid for I2C at boot (driver-install heap + a 4 KB static driver buffer), eating into that headroom. - I2C bus init is now LAZY (i2c_ensure, on first scan/driver use) instead of in setup(), so a device with no sensor in use pays no boot-time heap/.bss for it. - DRV_BYTES 4 KB → 1 KB (a driver .fxb is a few hundred bytes; submit_driver rejects larger). Net boot-time .bss added by FUG-107 is now ~1 KB. Behavior is unchanged when no driver is loaded (lm_drv_poll is gated on lm_drv_running). Firmware rebuilds clean; host ffi_test still exercises scan→submit→poll→remove. Could not reproduce on a rig from this worktree (the container is logged out of Tailscale); relying on the CI HITL re-run to confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rdware-discovery # Conflicts: # firmware/player_app/tests/ffi.rs
FUG-121's transport_bench (host rust_binary linking player_ffi_host) landed in the merge; it hits `undefined reference to lm_i2c_scan` because ffi.rs now imports the qwiic hooks (defined by main.cpp on-device / stubbed in tests/ffi.rs). `bazel test //...` builds every target, so this broke the test / test-macos lanes even though no test asserts it. Add the same no-op stubs the host ffi_test uses. Verified: `bazel build //firmware/... //fx_compiler/... //tools/... //web/... //solver/... //shared/...` all green (the only player_ffi_host consumers are ffi_test and transport_bench, both now stubbed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two browser-side .fxb parsers hardcoded the v1 18-byte header, so they broke on the v2 .fxb this PR emits (20-byte header + FUG-121's now-embedded manifest): - costModel.ts parseFxb() REJECTED version != 1 and sliced the code segment at offset 18 → the perf estimator (estimate_performance) threw on every v2 effect. - fxbTextures.ts read the buffer/texture table at offset 18 → wrong texture dimensions, so set_texture would drop frames for effects with textures. Both now read the version and use a 20-byte header for v2 (18 for v1). Runtime- only paths (node tests don't emit real .fxb), so a new fxbV2.test.ts pins both parsers against v1 and v2 (with a non-empty manifest, catching a header slip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rdware-discovery # Conflicts: # firmware/fx_vm/src/lib.rs # fx_compiler/src/lib.rs # fx_compiler/tests/compile.rs
…-FUG-122 merge) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-107-auto-hardware-discovery Resolved conflicts in fx_vm/src/lib.rs, fx_compiler/src/lib.rs, and fx_compiler/tests/compile.rs: appended FUG-107's I2C opcodes (I2cWrite/I2cRead) AFTER FUG-122's new fixed/int ops so their discriminants are unchanged (I2C mirror consts renumbered 102/103), threaded the I2cBus param through FUG-122's new run() signature (resident stack/locals/call_stack, 4-tuple return) incl. run_poll, and kept both features' compile tests. All host rust tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…into agent/fug-107-auto-hardware-discovery Resolved conflicts in fx_vm/src/lib.rs, fx_vm/tests/vm.rs, ffi.rs, and fx_compiler/src/lib.rs (rerere replayed most): appended FUG-107's I2cWrite/ I2cRead opcodes AFTER FUG-125's superinstructions + JitCall (discriminants 106/107; mirror consts + op_len/op_to_ir in fx_jit), threaded the I2cBus param through FUG-122/125's run() incl. the JIT table, and merged the imports/ classifier arms. FIX: FUG-125's bytecode optimizer (opt.rs) remapped only update/shade entries and DCE'd from those roots — so it silently dropped every sensor driver's poll() body and left poll_entry stale. Threaded poll through decode/encode/optimize + the CFG-reachability roots so poll() survives + is repointed. Driver compile tests (which run poll() against a mock I2C bus through the optimized .fxb) pass. All host rust tests green (fx_vm/fx_compiler/fx_jit/ffi/osc/player/conformance). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…into agent/fug-107-auto-hardware-discovery Resolved conflicts in fx_vm, fx_compiler, ffi.rs, and the op-classifier test: - Opcodes: FUG-125 took discriminants 102-105 (TeeLocal/IncLocalI/BrCmpI/ JitCall); FUG-107's I2cWrite/I2cRead re-appended at 106/107 (enum + fx_vm_op mirror + disassembler + opt::op_len + fx_jit::op_len, which is exhaustive). - run(): merged FUG-125's JIT-table params (jit_blocks/len/consts) with the existing I2cBus param; run_poll passes both. - Optimizer now repoints poll_entry too (opt::optimize/decode/encode/Prog + remove_unreachable root), so a driver's poll() is optimized + relocated correctly — verified by the compile+run driver tests. - fx_jit op_to_ir/branch_target bail on I2C ops via their _ => None (I/O isn't JIT-able); op_len (exhaustive) sizes them. All host rust tests green (fx_vm, fx_compiler incl. driver + optimizer, fx_jit, ffi driver+OSC, osc, player, conformance) + transport_bench. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plug any qwiic/I2C sensor into a splanc device and the model can enumerate the
bus, identify the part, write a driver for it, and install it — the driver runs
in the effects VM and exports sensor values as uniforms that effects
reference. Implements the FUG-107 vision end-to-end.
Design: a driver is just an effect with a different entry point
Rather than add a second interpreter, a sensor driver is ordinary
.fxbbytecode run by the same
fx_vmas effects — same sandbox, same per-invocationinstruction budget, same host/wasm/device parity. The differences:
void poll()entry instead ofshade()(a program is an effect XOR adriver);
export <type> name "unit";declarations — the valuespoll()writes;i2c_write/i2c_read8/i2c_read16.On the device each poll's exports are copied into the active effect's uniform
of the same name/width — the exact
set_uniformsslot path a slider uses — soa driver and an effect authored independently line up automatically. In the VM
the intrinsics call an
I2cBustrait: the firmware backs it with the ESP-IDFqwiic master, the host/browser back it with a mock register map, so drivers are
authored and unit-tested with no hardware.
The pipeline (bottom-up, one commit per layer)
firmware/fx_vm,fx_compiler) —I2cBustrait +I2cWrite/I2cReadopcodes threaded throughrun();.fxbv2 header addspoll_entry(v1 still parses);Vm::run_poll/Vm::export; the compilergains
exportdecls + exports manifest, apoll()entry, and thei2c_*builtins. (Also fixed two latent bugs the driver AI would trip on:the disassembler truncated at
min/max/mix, and string literals manglednon-ASCII units.)
ScanI2c → I2cScanResult{addresses},SubmitDriver{fxb, poll_interval_ms, bindings, activate} → DriverState,RemoveDriver.main.cpp(scanprobes0x08–0x77); a second
fx_vmruns the driver'spoll()off the render loop(
lm_drv_poll), applying exports → effect uniforms each poll.identifyScan),export→uniform bind-by-name (
computeBindings),scanI2c/submitDriverclient methods, and two AI tools (
scan_bus,set_driver) + a driver-dialectsystem prompt wired into the editor's chat when a device is connected. The
model can scan the bus, identify the part, write a driver from its register
map, compile it, bind it, and install it — iterating on the first poll result.
EFFECTS.mddocuments the whole driver stack.Verification
(read→exports, config write, bus-error
-1/0, effect/driver exclusivity,v2 back-compat).
conformance_test+phone_client_frames_testgreen.ffi_testdrivesscan_i2c → submit_driver → poll → remove_driverend-to-end on the host.bazel build -c opt //firmware/player_app:esp32c6linksclean.
sensorDb/driverBindingsunit-tested; 67web tests green.
Hardware-pending
Real-sensor bring-up is the one step I couldn't run — there's no qwiic module on
the HITL bench. The qwiic pins in
main.cpp(default SDA=6/SCL=7 @100 kHz) arebuild-time overridable and will likely need tuning to the actual board's qwiic
connector. The export→uniform bridge matches by name+width; the AI is instructed
to export the type the target uniform expects (client-side type matching would
need the scalar type threaded into the uniform manifest — a small follow-up).
🤖 Generated with Claude Code
Update (HITL fix): the first HITL run failed on the BLE-provision → wss TLS
handshake lane. Root-caused to boot-time footprint — the firmware paid for I2C at
boot (driver-install heap + a 4 KB static buffer), eating into the mbedtls
handshake headroom the arena sizing is tuned around. Fixed: I2C init is now lazy
(first sensor use, not boot) and the driver buffer is 1 KB, so FUG-107 adds ~1 KB
.bssand zero boot-time heap on a sensor-less device. (Couldn't reproduce on arig from the fleet worktree — it's logged out of Tailscale — so this leans on the
root-cause analysis + the CI re-run.)
Update (merge): merged
origin/main(FUG-121 native OSC input landed). Theoverlap was in the same firmware files but disjoint code regions — resolved one
conflict in
tests/ffi.rs(both features append host assertions; kept both). The.fxbfinish()now carries both my v2poll_entryheader and FUG-121'sembedded uniform manifest. Re-verified post-merge: all host rust tests, the
esp32c6image, web type-check + tests, and both wasm bundles build/pass.Update (test-lane fix): the post-merge
test/test-macosfailure was a linkerror, not a test assertion — FUG-121's
transport_bench(a host binary linkingplayer_ffi_host, whichbazel test //...builds) hitundefined reference to lm_i2c_scanbecause ffi.rs now imports the qwiic hooks. Added the same no-opstubs the host
ffi_testuses. Verifiedbazel buildis green across//firmware/... //fx_compiler/... //tools/... //web/... //solver/... //shared/....Update (v2
.fxbweb parsers): fixed two browser-side.fxbparsers thathardcoded the v1 18-byte header and so broke on this PR's v2
.fxb—parseFxb(perf estimator) rejected
version != 1, andparseFxbTexturesread the buffertable at the wrong offset. Both now handle v2 (20-byte header); a new
fxbV2.test.tspins them. These are runtime-only paths (node tests don't emitreal
.fxb), which is why they surfaced late. All.fxbconsumers (Rust VM +disassembler, both web parsers) now agree on the v2 layout.
Update (merge 2 — FUG-122): rebased onto
mainagain after FUG-122 (FX-VMnative datatypes + framing) landed — a large overlapping change to the VM
opcodes,
run(), the.fxbwriter, and the compiler. Resolved: appendedFUG-107's
I2cWrite/I2cReadopcodes AFTER FUG-122's new fixed/int ops so theirdiscriminants are unchanged (I2C mirror consts renumbered 102/103); threaded the
I2cBusparam through FUG-122's rewrittenrun()(resident stack/locals/call_stack + 4-tuple return) including
run_poll; kept both features' compiletests and extended FUG-122's opcode-classifier test to cover the I2C ops. The
.fxbfinish()now composes v2poll_entry(FUG-107) + embedded manifest(FUG-121) + the new datatype flags (FUG-122). Re-verified on the merged tree:
all host rust tests, the
esp32c6image, web type-check + 65 unit tests, andboth freshness gates (
fx_vm_perf_doc_freshness,codegen_freshness) pass.Update (merge 3 — FUG-125): rebased onto
mainagain after FUG-125 (FXengine optimizer + VM superinstructions + on-device RV32 JIT) landed — another
large overlapping change. Resolved: FUG-107's
I2cWrite/I2cReadre-appended atdiscriminants 106/107 (after FUG-125's 102–105) across all coupled sites — the
Openum,from_u8, the run() dispatch, thefx_vm_opmirror, thedisassembler,
opt::op_len, andfx_jit::op_len(exhaustive, so it had to besized); merged FUG-125's JIT-table
run()params with theI2cBusparam(
run_pollpasses both). The bytecode optimizer now repointspoll_entrytoo (threaded through
optimize/decode/encode/Prog+ the reachabilityroot), so a driver's
poll()is optimized + relocated exactly like an effect —verified by the compile→optimize→run driver tests.
fx_jitcorrectly declinesto JIT the I2C ops (I/O, not in the straight-line integer subset). Re-verified on
the merged tree: all host rust tests +
transport_bench, theesp32c6image,web type-check + 68 unit tests, and both freshness gates pass.
Closes-Linear: FUG-107 (https://linear.app/fughilli/issue/FUG-107/auto-hardware-discovery)