From 77e947ef98341fc5e42bcaab93990ef50b6c4228 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sun, 9 Aug 2026 22:06:51 +0200 Subject: [PATCH] fix(esp32sim): a rebuilt board adopts the I2C devices its parts attached MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/store/useSimulatorStore.ts | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 7c1161f0..4343fcd4 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -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(); + 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((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