180 lines
7.0 KiB
TypeScript
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;
|
|
};
|
|
},
|
|
});
|