/** * Custom Chip part — registers the 'custom-chip' metadata id with the * PartSimulationRegistry. On every sim start (hexEpoch change), instantiates * a ChipInstance per Custom Chip on the canvas and disposes it on cleanup. * * Wires are resolved using the standard `getArduinoPinHelper` provided by * DynamicComponent — chip pin names from chip.json get mapped to real * Arduino pin numbers based on the diagram's wire connections. */ import { PartSimulationRegistry } from './PartSimulationRegistry'; import { ChipInstance, decodeWasmBase64, ensureUartBridge, ensureSpiBridge, getSimulatorBridges, avrUartTx, getI2CBus, } from '../customChips'; import { useSimulatorStore } from '../../store/useSimulatorStore'; PartSimulationRegistry.register('custom-chip', { attachEvents: (_element, simulator, getArduinoPin, componentId) => { const sim = simulator as any; const component = useSimulatorStore .getState() .components.find((c) => c.id === componentId); if (!component) { console.warn(`[custom-chip] component ${componentId} not found in store`); return () => {}; } const props = component.properties as Record; const wasmBase64 = String(props.wasmBase64 ?? ''); const chipJsonStr = String(props.chipJson ?? '{}'); if (!wasmBase64) { console.info(`[custom-chip] ${componentId} has no compiled WASM yet — skipping.`); return () => {}; } let pins: string[] = []; let display: { width: number; height: number } | null = null; try { const obj = JSON.parse(chipJsonStr); if (Array.isArray(obj.pins)) { // Pin entries may be strings (Wokwi) or {name,x,y} objects. pins = obj.pins.map((p: unknown) => { if (typeof p === 'string') return p; if (p && typeof p === 'object') return String((p as any).name ?? ''); return ''; }); } if (obj.display && typeof obj.display.width === 'number' && typeof obj.display.height === 'number') { display = { width: obj.display.width, height: obj.display.height }; } } catch (e) { console.warn(`[custom-chip] ${componentId} chip.json parse error:`, e); return () => {}; } // Pull saved attribute values from the component's properties. const attrsObj: Record = {}; try { const raw = (props.attrs ?? {}) as Record; for (const [k, v] of Object.entries(raw)) { const n = typeof v === 'number' ? v : parseFloat(String(v)); if (!Number.isNaN(n)) attrsObj[k] = n; } } catch { /* ignore */ } // ── ESP32 path ────────────────────────────────────────────────────────── // The chip's WASM runs in the backend QEMU worker process so I2C events // are answered synchronously. See docs/wiki/custom-chips-esp32-backend-runtime.md. if (typeof sim.registerSensor === 'function') { // Resolve each chip pin name → ESP32 GPIO via the diagram's wires. The // backend runtime uses this to call qemu_picsimlab_set_pin when the chip // does vx_pin_write, and to read live GPIO state for vx_pin_read. const pinMap: Record = {}; for (const name of pins) { if (!name) continue; const gpio = getArduinoPin(name); if (gpio !== null && gpio >= 0) pinMap[name] = gpio; } // Synthetic slot — backend doesn't index custom-chip sensors by pin. const virtualPin = 0xFF; try { sim.registerSensor('custom-chip', virtualPin, { wasm_b64: wasmBase64, attrs: attrsObj, pin_map: pinMap, }); console.info( `[custom-chip:${componentId}] sent to backend ESP32 worker (chip runs synchronously inside QEMU process). pinMap=${JSON.stringify(pinMap)}`, ); } catch (e) { console.error(`[custom-chip:${componentId}] failed to register on ESP32 backend:`, e); } // Cleanup: the QEMU instance is torn down on stop_esp32 — no client-side state. return () => {}; } // ── End ESP32 path ────────────────────────────────────────────────────── // Resolve every chip pin name to its wired Arduino pin (if any). const wires = new Map(); for (const name of pins) { if (!name) continue; const arduinoPin = getArduinoPin(name); if (arduinoPin !== null && arduinoPin >= 0) { wires.set(name, arduinoPin); } } // Convert the attribute map into a Map for the JS runtime. const attrs = new Map(Object.entries(attrsObj)); // Lazily install the per-simulator bridges. Idempotent — safe to call // even if other custom chips have already wired them up. ensureUartBridge(sim); ensureSpiBridge(sim); const bridges = getSimulatorBridges(sim); // Async create — wrap so we can dispose even if create is still in-flight // when the user stops the simulation. let instance: ChipInstance | null = null; let uartListener: ((byte: number) => void) | null = null; let disposed = false; (async () => { try { const wasm = decodeWasmBase64(wasmBase64); const inst = await ChipInstance.create({ wasm, pinManager: sim.pinManager, // Polymorphic I2C: AVR returns the I2CBusManager directly, RP2040 // returns a thin adapter, ESP32 returns null (chip won't get I2C). i2cBus: getI2CBus(sim, 0) as any, spiBus: bridges.spiBus, wires, attrs, display, log: (s) => console.log(`[chip:${componentId}] ${s.replace(/\n$/, '')}`), }); if (disposed) { inst.dispose(); return; } instance = inst; inst.start(); // Bridge UART: AVR Serial.write(byte) → chip.feedUart(byte). // Chip's vx_uart_write(byte) → simulator.usart.writeByte (Serial.read). if (inst.hasUart) { uartListener = (byte: number) => inst.feedUart(byte); bridges.uartListeners.add(uartListener); inst.onUartTx((byte) => avrUartTx(sim, byte)); } // Bridge framebuffer → chip's web component canvas (when chip has display). const el = document.getElementById(componentId) as HTMLElement | null; if (el && typeof (el as any).paintFramebuffer === 'function' && inst.hasFramebuffer) { inst.onFramebufferUpdate((rgba, width, height) => { try { (el as any).paintFramebuffer(rgba, width, height); } catch { /* swallow */ } }); } } catch (e) { console.error(`[custom-chip] ${componentId} failed to load:`, e); } })(); return () => { disposed = true; if (uartListener) bridges.uartListeners.delete(uartListener); if (instance) instance.dispose(); instance = null; }; }, });