fix(sim): correct NTC temperature sensor divider + reset to default on restart
The NTC breakout's SPICE topology was inverted relative to the example sketch's decode formula (rNtc = R_PULL * v / (5 - v)), which assumes a 10k pull-up from VCC to OUT and the NTC from OUT to GND. The mapper had the NTC on top (VCC->OUT) and the pull-down on the bottom, so the recovered temperature ran backwards: dragging the slider to 100C made the sketch print -25C. Swap the two resistors so V_OUT = 5 * Rntc / (Rntc + Rpull), matching the sketch and the hand-built reference netlist in spice-avr-mixed.test.ts (T=0 -> ADC 789, T=25 -> 511, T=50 -> 270). Also replace the SensorParts linear approximation (2.5 - (t-25)*0.02) with the same beta-model divider so the non-SPICE ADC injection decodes back to the slider value, and drop the dead onInput path that treated the element's value as a raw ADC count. Reset now restores interactive sensors (temperature/lux/gas sliders) to their configured defaults: resetBoard re-dispatches each sensor's default into the running sim and bumps sensorResetNonce so the open SensorControlPanel remounts and the slider snaps back. Previously a restart left the NTC frozen at the last dragged temperature. Updated the examples netlist snapshot for the swapped NTC cards.
This commit is contained in:
parent
e51d26ce33
commit
608c2538c9
|
|
@ -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"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SensorControlPanel
|
||||
key={sensorControlComponentId}
|
||||
key={`${sensorControlComponentId}:${sensorResetNonce}`}
|
||||
componentId={sensorControlComponentId}
|
||||
metadataId={sensorControlMetadataId}
|
||||
sensorName={meta?.name ?? sensorControlMetadataId}
|
||||
|
|
|
|||
|
|
@ -62,26 +62,29 @@ PartSimulationRegistry.register('tilt-switch', {
|
|||
* NTC thermistor sensor — injects analog voltage representing temperature.
|
||||
* Default 25°C → 2.5V. SensorControlPanel slider adjusts temperature.
|
||||
*
|
||||
* Linear approximation: volts = clamp(2.5 - (temp - 25) * 0.02, 0, 5)
|
||||
* (25°C = 2.5V; lower temp = higher voltage, higher temp = lower voltage)
|
||||
* The injected OUT voltage uses the SAME β-model voltage divider the circuit
|
||||
* (and the example sketch) assume: a 10k pull-up from VCC to OUT and the NTC
|
||||
* from OUT to GND, so V_OUT = 5 · R_ntc / (R_ntc + R_pull) with
|
||||
* R_ntc(T) = R0 · exp(β · (1/T − 1/T0)). This makes the injected ADC voltage
|
||||
* decode straight back to the slider value (set 50°C → read 50°C). When the
|
||||
* electrical (SPICE) engine is active it drives A0 from the ngspice solve
|
||||
* instead, using the matching topology in componentToSpice.ts — both agree.
|
||||
*/
|
||||
PartSimulationRegistry.register('ntc-temperature-sensor', {
|
||||
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
||||
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);
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -700,9 +700,11 @@ const MAPPERS: Record<string, Mapper> = {
|
|||
},
|
||||
|
||||
// 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<string, Mapper> = {
|
|||
}
|
||||
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(),
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<SimulatorState>((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<SimulatorState>((set, get) => {
|
|||
running: false,
|
||||
compiledHex: null,
|
||||
hexEpoch: 0,
|
||||
sensorResetNonce: 0,
|
||||
serialOutput: '',
|
||||
serialBaudRate: 0,
|
||||
serialMonitorOpen: false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue