fix(esp32sim): a rebuilt board adopts the I2C devices its parts attached

The pro overlay loads async, so a deep-linked example attaches its part
device models to the shim wired around the STOCK QEMU bridge — whose
attachSyncI2cDevice is a silent optional no-op — and only then does the
overlay's factory rebuild the board with the js-engine bridge. The
rebuild made a brand-new shim; the parts on the canvas hold the old one
in their attach closures and never re-attach. Result: the engine booted
with an EMPTY I2C bus and arduino's i2c-ng driver failed every
transaction with ESP_ERR_INVALID_STATE (the seated C6 gesture example —
measured live with the bus device table at Map(0)). Whether it broke
was a chunk-load race; the pro bundle's growth turned it into a sure
loss.

The shim now records every sync-attached device and a rebuilt shim
adopts its predecessor's — through addI2CDevice, so the new Delegating
bridge buffers them pre-connect and its start() replays them into the
engine after reset, as it always did for attaches it actually heard.
This commit is contained in:
David Montero Crespo 2026-08-09 22:06:51 +02:00
parent 8b2715affd
commit 77e947ef98
1 changed files with 28 additions and 0 deletions

View File

@ -421,8 +421,20 @@ class Esp32BridgeShim {
* find the device. ProtocolParts calls this on the ESP32 path
* alongside the existing `registerSensor` + `addI2CTransactionListener`.
*/
/** Sync-attached part devices, kept so a bridge REBUILD can adopt them.
* The overlay loads async: a deep-linked example attaches its parts to the
* shim wired around the stock QEMU bridge (whose attachSyncI2cDevice is
* missing — a silent no-op), and only then does the overlay's factory
* rebuild the board. Without this record the part's device existed
* nowhere: the C6 gesture example booted with an EMPTY engine I2C bus and
* arduino's i2c-ng driver failed every transaction (ESP_ERR_INVALID_STATE)
* — whether it broke depended on a chunk-load race the pro bundle's growth
* turned into a sure loss. */
private syncI2cParts = new Map<number, I2CDevice>();
addI2CDevice(device: I2CDevice, _bus: 0 | 1 = 0): void {
this.i2cBusInstance.addDevice(device);
this.syncI2cParts.set(device.address, device);
// An in-browser JS-emulator substitute bridge (velxio-prod overlay) plugs
// the part's real device model straight onto the engine's synchronous I2C
// bus, so the firmware's own reads hit it (sensors answer, displays
@ -431,6 +443,14 @@ class Esp32BridgeShim {
(this.bridge as { attachSyncI2cDevice?: (d: I2CDevice) => void }).attachSyncI2cDevice?.(device);
}
/** A rebuilt board gets a fresh shim; the parts on the canvas do NOT
* re-attach (their closures hold the old shim), so the new shim re-plays
* everything the old one had. Runs through addI2CDevice so the new bridge
* (a pre-connect-buffering Delegating bridge) hears about each device. */
adoptPartsFrom(prev: Esp32BridgeShim): void {
for (const dev of prev.syncI2cParts.values()) this.addI2CDevice(dev);
}
/**
* Attach (or clear, with null) the microphone sample source feeding the
* board's I2S RX path: one signed 16-bit sample per call. Same forwarding
@ -477,6 +497,7 @@ class Esp32BridgeShim {
/** Remove a previously-registered virtual device. */
removeI2CDevice(addr: number, _bus: 0 | 1 = 0): void {
this.i2cBusInstance.removeDevice(addr);
this.syncI2cParts.delete(addr);
(this.bridge as { detachSyncI2cDevice?: (a: number) => void }).detachSyncI2cDevice?.(addr);
}
@ -1333,6 +1354,13 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
if (existingShim?.clearAllProxies) {
try { existingShim.clearAllProxies(); } catch { /* ignore */ }
}
// The parts on the canvas attached their I2C device models to the OLD
// shim (their attach closures hold it) and will not re-attach for a
// bridge rebuild — carry them over, or the rebuilt engine boots with an
// empty I2C bus and every Wire transaction fails.
if (existingShim instanceof Esp32BridgeShim) {
try { shim.adoptPartsFrom(existingShim); } catch { /* ignore */ }
}
simulatorMap.set(id, shim);
// The Interconnect's serial fan-out wraps the INSTANCE's
// onSerialData; a fresh simulator (compile, reset, engine