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

92 lines
3.7 KiB
TypeScript

/**
* partUtils.ts — Shared simulation helpers
*
* Provides ADC voltage injection utilities used by both ComplexParts and
* SensorParts, supporting both AVR (ATmega328p) and RP2040 boards.
*/
import type { AnySimulator } from './PartSimulationRegistry';
import { RP2040Simulator } from '../RP2040Simulator';
/** DOM event fired when a runtime part mutates a user-facing property. */
export const PROPERTY_CHANGE_EVENT = 'velxio:property-change';
export interface PropertyChangeDetail {
componentId: string;
propName: string;
value: unknown;
}
/**
* Dispatch a property change so the canvas can route it through
* `updateComponent()`. Parts call this whenever a DOM / sensor-panel value
* mutates so the SPICE netlist memo invalidates and the next `maybeSolve()`
* picks up the change. Parts stay decoupled from Zustand — `SimulatorCanvas`
* is the single listener that applies the update.
*/
export function emitPropertyChange(componentId: string, propName: string, value: unknown): void {
if (typeof window === 'undefined') return;
if (typeof window.dispatchEvent !== 'function' || typeof CustomEvent !== 'function') return;
const detail: PropertyChangeDetail = { componentId, propName, value };
window.dispatchEvent(new CustomEvent(PROPERTY_CHANGE_EVENT, { detail }));
}
/** Read the ADC instance from the simulator (returns null if not initialized) */
export function getADC(avrSimulator: AnySimulator): any | null {
return (avrSimulator as any).getADC?.() ?? null;
}
/**
* Supply rail the analog front end of THIS board runs on, in volts.
*
* A part that builds a resistive divider has to use the rail it is actually
* wired to. Assuming 5 V everywhere is not a small error on a 3.3 V board: it
* pushes the computed node voltage above the ADC's full scale, so the reading
* saturates and a whole stretch of the control slider reports the same value.
* (An NTC divider assumed at 5 V reads 1.4 C on an ESP32 when the slider says
* 25 C, and pins at full scale for everything below ~11 C — issue #233.)
*
* The split matches setAdcVoltage's own branches: AVR converts against a 5 V
* reference, RP2040 and the ESP32 bridge against 3.3 V.
*/
export function analogRailVolts(simulator: AnySimulator): number {
if (typeof (simulator as any).setAdcVoltage === 'function') return 3.3; // ESP32 bridge
if (simulator instanceof RP2040Simulator) return 3.3;
return 5.0; // AVR
}
/**
* Write an analog voltage to an ADC channel, supporting AVR, RP2040, and ESP32.
*
* AVR: pins 14-19 → ADC channels 0-5, voltage stored directly (0-5V)
* RP2040: GPIO 26-29 → ADC channels 0-3, converted to 12-bit value (0-4095)
* ESP32: GPIO 32-39 → ADC1 channels 4-11, sent via WebSocket bridge
*
* Returns true if the voltage was successfully injected.
*/
export function setAdcVoltage(simulator: AnySimulator, pin: number, voltage: number): boolean {
// ESP32 BridgeShim: delegate to bridge via WebSocket
if (typeof (simulator as any).setAdcVoltage === 'function') {
return (simulator as any).setAdcVoltage(pin, voltage);
}
// RP2040: GPIO26-29 → ADC channels 0-3
if (simulator instanceof RP2040Simulator) {
if (pin >= 26 && pin <= 29) {
const channel = pin - 26;
// RP2040 ADC: 12-bit, 3.3V reference
const adcValue = Math.round((voltage / 3.3) * 4095);
simulator.setADCValue(channel, adcValue);
return true;
}
console.warn(`[setAdcVoltage] RP2040 pin ${pin} is not an ADC pin (26-29)`);
return false;
}
// AVR: pins 14-19 → ADC channels 0-5
if (pin < 14 || pin > 19) return false;
const channel = pin - 14;
const adc = getADC(simulator);
if (!adc) return false;
adc.channelValues[channel] = voltage;
return true;
}