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`] = `
|
exports[`netlist snapshot — circuits (41 examples) > nano-sensor-station 1`] = `
|
||||||
"* Velxio circuit
|
"* Velxio circuit
|
||||||
V_VCC_RAIL vcc_rail 0 DC 5
|
V_VCC_RAIL vcc_rail 0 DC 5
|
||||||
R_ntc_ntc vcc_rail n1 11441.484668193882
|
R_ntc_pull vcc_rail n1 10000
|
||||||
R_ntc_pull n1 0 10000
|
R_ntc_ntc n1 0 11441.484668193882
|
||||||
R_ldr_ldr vcc_rail n0 400000
|
R_ldr_ldr vcc_rail n0 400000
|
||||||
R_ldr_pull n0 0 10000
|
R_ldr_pull n0 0 10000
|
||||||
.op
|
.op
|
||||||
|
|
@ -1446,8 +1446,8 @@ R_autopull_n5 n5 0 100Meg
|
||||||
exports[`netlist snapshot — circuits (41 examples) > ntc-temperature 1`] = `
|
exports[`netlist snapshot — circuits (41 examples) > ntc-temperature 1`] = `
|
||||||
"* Velxio circuit
|
"* Velxio circuit
|
||||||
V_VCC_RAIL vcc_rail 0 DC 5
|
V_VCC_RAIL vcc_rail 0 DC 5
|
||||||
R_ntc_ntc vcc_rail n0 10000
|
R_ntc_pull vcc_rail n0 10000
|
||||||
R_ntc_pull n0 0 10000
|
R_ntc_ntc n0 0 10000
|
||||||
.op
|
.op
|
||||||
.end"
|
.end"
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
||||||
addBoard,
|
addBoard,
|
||||||
components,
|
components,
|
||||||
running,
|
running,
|
||||||
|
sensorResetNonce,
|
||||||
pinManager,
|
pinManager,
|
||||||
initSimulator,
|
initSimulator,
|
||||||
updateComponentState,
|
updateComponentState,
|
||||||
|
|
@ -2490,14 +2491,15 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
||||||
key={sensorControlComponentId} forces a fresh mount when the user clicks a
|
key={sensorControlComponentId} forces a fresh mount when the user clicks a
|
||||||
different instance of the same sensor type (e.g. a second photoresistor); the
|
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
|
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 &&
|
{sensorControlComponentId &&
|
||||||
sensorControlMetadataId &&
|
sensorControlMetadataId &&
|
||||||
(() => {
|
(() => {
|
||||||
const meta = registry.getById(sensorControlMetadataId);
|
const meta = registry.getById(sensorControlMetadataId);
|
||||||
return (
|
return (
|
||||||
<SensorControlPanel
|
<SensorControlPanel
|
||||||
key={sensorControlComponentId}
|
key={`${sensorControlComponentId}:${sensorResetNonce}`}
|
||||||
componentId={sensorControlComponentId}
|
componentId={sensorControlComponentId}
|
||||||
metadataId={sensorControlMetadataId}
|
metadataId={sensorControlMetadataId}
|
||||||
sensorName={meta?.name ?? sensorControlMetadataId}
|
sensorName={meta?.name ?? sensorControlMetadataId}
|
||||||
|
|
|
||||||
|
|
@ -62,26 +62,29 @@ PartSimulationRegistry.register('tilt-switch', {
|
||||||
* NTC thermistor sensor — injects analog voltage representing temperature.
|
* NTC thermistor sensor — injects analog voltage representing temperature.
|
||||||
* Default 25°C → 2.5V. SensorControlPanel slider adjusts temperature.
|
* Default 25°C → 2.5V. SensorControlPanel slider adjusts temperature.
|
||||||
*
|
*
|
||||||
* Linear approximation: volts = clamp(2.5 - (temp - 25) * 0.02, 0, 5)
|
* The injected OUT voltage uses the SAME β-model voltage divider the circuit
|
||||||
* (25°C = 2.5V; lower temp = higher voltage, higher temp = lower voltage)
|
* (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', {
|
PartSimulationRegistry.register('ntc-temperature-sensor', {
|
||||||
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
attachEvents: (_element, simulator, getArduinoPinHelper, componentId) => {
|
||||||
const pin = getArduinoPinHelper('OUT');
|
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
|
// Room temperature default
|
||||||
if (pin !== null) setAdcVoltage(simulator, pin, tempToVolts(25));
|
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) => {
|
registerSensorUpdate(componentId, (values) => {
|
||||||
if ('temperature' in values) {
|
if ('temperature' in values) {
|
||||||
if (pin !== null) {
|
if (pin !== null) {
|
||||||
|
|
@ -94,7 +97,6 @@ PartSimulationRegistry.register('ntc-temperature-sensor', {
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener('input', onInput);
|
|
||||||
unregisterSensorUpdate(componentId);
|
unregisterSensorUpdate(componentId);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -700,9 +700,11 @@ const MAPPERS: Record<string, Mapper> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// NTC temperature sensor — 3-pin breakout module (VCC, GND, OUT).
|
// NTC temperature sensor — 3-pin breakout module (VCC, GND, OUT).
|
||||||
// Internal topology: NTC thermistor between VCC and OUT, plus an internal
|
// Internal topology: a 10k pull-up from VCC to OUT, with the NTC thermistor
|
||||||
// 10k pull-down from OUT to GND. Temperature up → R_ntc down → V_OUT up.
|
// from OUT to GND, so V_OUT = Vcc · R_ntc / (R_ntc + R_pull). Temperature up
|
||||||
// Matches the β-model math used in the ntc-temperature example.
|
// → 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) => {
|
'ntc-temperature-sensor': (comp, netLookup) => {
|
||||||
const vcc = netLookup('VCC');
|
const vcc = netLookup('VCC');
|
||||||
const gnd = netLookup('GND');
|
const gnd = netLookup('GND');
|
||||||
|
|
@ -719,7 +721,7 @@ const MAPPERS: Record<string, Mapper> = {
|
||||||
}
|
}
|
||||||
const Rpull = parseValueWithUnits(comp.properties.pullup, 10_000);
|
const Rpull = parseValueWithUnits(comp.properties.pullup, 10_000);
|
||||||
return {
|
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(),
|
modelsUsed: new Set(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import {
|
||||||
updateWires as icUpdateWires,
|
updateWires as icUpdateWires,
|
||||||
setInterconnectRuntime,
|
setInterconnectRuntime,
|
||||||
} from '../simulation/Interconnect';
|
} from '../simulation/Interconnect';
|
||||||
|
import { SENSOR_CONTROLS } from '../simulation/sensorControlConfig';
|
||||||
|
import { dispatchSensorUpdate } from '../simulation/SensorUpdateRegistry';
|
||||||
|
|
||||||
// ── Sensor pre-registration ──────────────────────────────────────────────────
|
// ── Sensor pre-registration ──────────────────────────────────────────────────
|
||||||
// Maps component metadataId → { sensorType, dataPinName, propertyKeys }
|
// Maps component metadataId → { sensorType, dataPinName, propertyKeys }
|
||||||
|
|
@ -817,6 +819,9 @@ interface SimulatorState {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
compiledHex: string | null;
|
compiledHex: string | null;
|
||||||
hexEpoch: number;
|
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;
|
serialOutput: string;
|
||||||
serialBaudRate: number;
|
serialBaudRate: number;
|
||||||
serialMonitorOpen: boolean;
|
serialMonitorOpen: boolean;
|
||||||
|
|
@ -1914,6 +1919,29 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
...(isActive ? { running: false, serialOutput: '', serialBaudRate: 0 } : {}),
|
...(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 ───────────────────────────────────────────
|
// ── Legacy single-board API ───────────────────────────────────────────
|
||||||
|
|
@ -1924,6 +1952,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
running: false,
|
running: false,
|
||||||
compiledHex: null,
|
compiledHex: null,
|
||||||
hexEpoch: 0,
|
hexEpoch: 0,
|
||||||
|
sensorResetNonce: 0,
|
||||||
serialOutput: '',
|
serialOutput: '',
|
||||||
serialBaudRate: 0,
|
serialBaudRate: 0,
|
||||||
serialMonitorOpen: false,
|
serialMonitorOpen: false,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue