velxio/frontend/src/simulation/parts/CustomChipPart.ts

180 lines
7.0 KiB
TypeScript

/**
* 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<string, unknown>;
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<string, number> = {};
try {
const raw = (props.attrs ?? {}) as Record<string, unknown>;
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<string, number> = {};
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<string, number>();
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<string, number> for the JS runtime.
const attrs = new Map<string, number>(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;
};
},
});