diff --git a/frontend/src/__tests__/__snapshots__/examples-netlist-snapshot.test.ts.snap b/frontend/src/__tests__/__snapshots__/examples-netlist-snapshot.test.ts.snap index 5d314957..eb2b7294 100644 --- a/frontend/src/__tests__/__snapshots__/examples-netlist-snapshot.test.ts.snap +++ b/frontend/src/__tests__/__snapshots__/examples-netlist-snapshot.test.ts.snap @@ -1416,8 +1416,8 @@ R_autopull_n6 n6 0 100Meg exports[`netlist snapshot — circuits (41 examples) > nano-sensor-station 1`] = ` "* Velxio circuit V_VCC_RAIL vcc_rail 0 DC 5 -R_ntc_ntc vcc_rail n1 11441.484668193882 -R_ntc_pull n1 0 10000 +R_ntc_pull vcc_rail n1 10000 +R_ntc_ntc n1 0 11441.484668193882 R_ldr_ldr vcc_rail n0 400000 R_ldr_pull n0 0 10000 .op @@ -1446,8 +1446,8 @@ R_autopull_n5 n5 0 100Meg exports[`netlist snapshot — circuits (41 examples) > ntc-temperature 1`] = ` "* Velxio circuit V_VCC_RAIL vcc_rail 0 DC 5 -R_ntc_ntc vcc_rail n0 10000 -R_ntc_pull n0 0 10000 +R_ntc_pull vcc_rail n0 10000 +R_ntc_ntc n0 0 10000 .op .end" `; diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 1b5bd6ce..0ad774b6 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -118,6 +118,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { addBoard, components, running, + sensorResetNonce, pinManager, initSimulator, updateComponentState, @@ -2490,14 +2491,15 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { key={sensorControlComponentId} forces a fresh mount when the user clicks a different instance of the same sensor type (e.g. a second photoresistor); the slider state is local and would otherwise show the previously-clicked sensor's - value until the user manually moved it. */} + value until the user manually moved it. The sensorResetNonce suffix also remounts + it on Reset, so the slider snaps back to the sensor's default value. */} {sensorControlComponentId && sensorControlMetadataId && (() => { const meta = registry.getById(sensorControlMetadataId); return ( { + attachEvents: (_element, simulator, getArduinoPinHelper, componentId) => { const pin = getArduinoPinHelper('OUT'); - const tempToVolts = (temp: number) => Math.max(0, Math.min(5, 2.5 - (temp - 25) * 0.02)); + const NTC_R0 = 10_000; + const NTC_BETA = 3950; + const R_PULL = 10_000; + const tempToVolts = (temp: number) => { + const rNtc = NTC_R0 * Math.exp(NTC_BETA * (1 / (temp + 273.15) - 1 / 298.15)); + return Math.max(0, Math.min(5, 5 * (rNtc / (rNtc + R_PULL)))); + }; // Room temperature default if (pin !== null) setAdcVoltage(simulator, pin, tempToVolts(25)); - const onInput = () => { - const val = (element as any).value; - if (val !== undefined && pin !== null) { - setAdcVoltage(simulator, pin, (val / 1023.0) * 5.0); - } - }; - element.addEventListener('input', onInput); - registerSensorUpdate(componentId, (values) => { if ('temperature' in values) { if (pin !== null) { @@ -94,7 +97,6 @@ PartSimulationRegistry.register('ntc-temperature-sensor', { }); return () => { - element.removeEventListener('input', onInput); unregisterSensorUpdate(componentId); }; }, diff --git a/frontend/src/simulation/spice/componentToSpice.ts b/frontend/src/simulation/spice/componentToSpice.ts index 2d546c55..684b03e7 100644 --- a/frontend/src/simulation/spice/componentToSpice.ts +++ b/frontend/src/simulation/spice/componentToSpice.ts @@ -700,9 +700,11 @@ const MAPPERS: Record = { }, // NTC temperature sensor — 3-pin breakout module (VCC, GND, OUT). - // Internal topology: NTC thermistor between VCC and OUT, plus an internal - // 10k pull-down from OUT to GND. Temperature up → R_ntc down → V_OUT up. - // Matches the β-model math used in the ntc-temperature example. + // Internal topology: a 10k pull-up from VCC to OUT, with the NTC thermistor + // from OUT to GND, so V_OUT = Vcc · R_ntc / (R_ntc + R_pull). Temperature up + // → R_ntc down → V_OUT down. This is the exact divider the ntc-temperature + // example sketch inverts to recover R_ntc (rNtc = R_PULL · v / (5 − v)); with + // VCC and GND swapped (NTC on top) the decoded temperature ran backwards. 'ntc-temperature-sensor': (comp, netLookup) => { const vcc = netLookup('VCC'); const gnd = netLookup('GND'); @@ -719,7 +721,7 @@ const MAPPERS: Record = { } const Rpull = parseValueWithUnits(comp.properties.pullup, 10_000); return { - cards: [`R_${comp.id}_ntc ${vcc} ${out} ${Rntc}`, `R_${comp.id}_pull ${out} ${gnd} ${Rpull}`], + cards: [`R_${comp.id}_pull ${vcc} ${out} ${Rpull}`, `R_${comp.id}_ntc ${out} ${gnd} ${Rntc}`], modelsUsed: new Set(), }; }, diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 5ed6f009..ebc88606 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -37,6 +37,8 @@ import { updateWires as icUpdateWires, setInterconnectRuntime, } from '../simulation/Interconnect'; +import { SENSOR_CONTROLS } from '../simulation/sensorControlConfig'; +import { dispatchSensorUpdate } from '../simulation/SensorUpdateRegistry'; // ── Sensor pre-registration ────────────────────────────────────────────────── // Maps component metadataId → { sensorType, dataPinName, propertyKeys } @@ -817,6 +819,9 @@ interface SimulatorState { running: boolean; compiledHex: string | null; hexEpoch: number; + /** Bumped on every Reset so the open SensorControlPanel remounts and + * re-reads each interactive sensor's freshly-defaulted value. */ + sensorResetNonce: number; serialOutput: string; serialBaudRate: number; serialMonitorOpen: boolean; @@ -1914,6 +1919,29 @@ export const useSimulatorStore = create((set, get) => { ...(isActive ? { running: false, serialOutput: '', serialBaudRate: 0 } : {}), }; }); + + // Reset interactive sensors (temperature / lux / gas sliders, etc.) back + // to their configured defaults so a restart starts from a clean state + // instead of freezing on the last slider position the user dragged to. + // dispatchSensorUpdate re-injects the default into the running sim (so the + // NTC's injected ADC voltage and the SPICE solve both return to 25°C / + // 2.5V) and refreshes the panel's cached value; bumping sensorResetNonce + // remounts the open SensorControlPanel so its slider snaps back too. + const sensorComps = get().components.filter( + (c) => c.metadataId && SENSOR_CONTROLS[c.metadataId], + ); + if (sensorComps.length > 0) { + set((s) => ({ + components: s.components.map((c) => { + const def = c.metadataId ? SENSOR_CONTROLS[c.metadataId] : undefined; + return def ? { ...c, properties: { ...c.properties, ...def.defaultValues } } : c; + }), + sensorResetNonce: s.sensorResetNonce + 1, + })); + for (const c of sensorComps) { + dispatchSensorUpdate(c.id, SENSOR_CONTROLS[c.metadataId].defaultValues); + } + } }, // ── Legacy single-board API ─────────────────────────────────────────── @@ -1924,6 +1952,7 @@ export const useSimulatorStore = create((set, get) => { running: false, compiledHex: null, hexEpoch: 0, + sensorResetNonce: 0, serialOutput: '', serialBaudRate: 0, serialMonitorOpen: false,