fix(examples): night-light LDR shorted AO to VCC — "1M" is milliohms in SPICE

100d-auto-night-light shipped `dark: "1M"` meaning 1 megaohm. The value
parser follows the SPICE convention where M is MILLI and mega is `Meg`,
so the dark resistance became 0.001 ohm. With the divider's top leg a
short, AO sat at VCC: `ldr.read()` returned 4095 on every sample, the
sketch's `light < 500` branch never ran, and the lamp LED never lit.
Dragging the lux control changed nothing either, because every lux value
still divides a short (measured n1 = 3.299999996 V, which is exactly what
100k / (100k + 0.00011) predicts).

The netlist snapshot had frozen the bug in place:

  - R_night-ldr_ldr vcc_rail n1 0.00011111111111111112
  + R_night-ldr_ldr vcc_rail n1 111111.11111111111

Diagram now says "1Meg" (regenerated from test/test_100_days), and the
mapper clamps a sub-1k dark resistance to the documented 1Meg default
with a console warning naming the milli/mega trap — a photoresistor that
reads as a short is always this mistake, and it fails in the most
confusing way possible: the part looks alive, it just never moves.
This commit is contained in:
David Montero Crespo 2026-08-17 03:39:47 +02:00
parent 5c95ade35f
commit 9b0b4bc038
4 changed files with 53 additions and 3 deletions

View File

@ -22,7 +22,7 @@ R_autopull_n7 n7 0 100Meg
exports[`netlist snapshot — 100-days (49 examples) > 100d-auto-night-light-using-ldr-esp32-plus-micropython 1`] = ` exports[`netlist snapshot — 100-days (49 examples) > 100d-auto-night-light-using-ldr-esp32-plus-micropython 1`] = `
"* Velxio circuit "* Velxio circuit
V_VCC_RAIL vcc_rail 0 DC 5 V_VCC_RAIL vcc_rail 0 DC 5
R_night-ldr_ldr vcc_rail n1 0.00011111111111111112 R_night-ldr_ldr vcc_rail n1 111111.11111111111
R_night-ldr_pull n1 0 100000 R_night-ldr_pull n1 0 100000
R_night-r1 n0 n2 220 R_night-r1 n0 n2 220
V_night-led1_sense n2 night-led1_sense_mid DC 0 V_night-led1_sense n2 night-led1_sense_mid DC 0

View File

@ -410,3 +410,36 @@ describe('componentToSpice — ngspice accepts every card', () => {
}); });
} }
}); });
describe('photoresistor — dark resistance suffix', () => {
const mapOne = (properties: Record<string, unknown>) => {
const nets: Record<string, string> = { VCC: 'vcc', GND: '0', AO: 'n_ao' };
return componentToSpice(
{ id: 'ldr1', metadataId: 'photoresistor-sensor', properties },
(pin: string) => nets[pin] ?? null,
{} as never,
);
};
it('reads "1Meg" as one megaohm', () => {
const out = mapOne({ lux: 400, dark: '1Meg', k: 20, pullup: '100k' });
// R = 1e6 / (1 + 20*400/1000) = 111111.1 ohm
expect(out?.cards[0]).toMatch(/R_ldr1_ldr vcc n_ao 111111/);
});
it('does not short AO to VCC when dark is written "1M"', () => {
// Regression: 100d-auto-night-light shipped dark: "1M" meaning 1 megaohm.
// SPICE reads M as milli, so it parsed to 0.001 ohm — AO sat at VCC, the
// ADC read 4095 forever and dragging the lux control changed nothing
// (every lux value still divides a short). Clamp instead of emitting it.
const out = mapOne({ lux: 400, dark: '1M', k: 20, pullup: '100k' });
const ldrCard = out?.cards[0] ?? '';
const ohms = Number(ldrCard.split(/\s+/).pop());
expect(ohms).toBeGreaterThan(1_000);
});
it('still emits the pull-down leg to GND', () => {
const out = mapOne({ lux: 400, dark: '1Meg', k: 20, pullup: '100k' });
expect(out?.cards[1]).toBe('R_ldr1_pull n_ao 0 100000');
});
});

View File

@ -364,7 +364,7 @@ while True:
` }, ` },
], ],
code: '', code: '',
components: [{"type": "wokwi-photoresistor-sensor", "id": "night-ldr", "x": 430, "y": 300, "properties": {"lux": 400, "dark": "1M", "k": 20, "pullup": "100k"}}, {"type": "wokwi-resistor", "id": "night-r1", "x": 620, "y": 40, "properties": {"value": "220"}}, {"type": "wokwi-led", "id": "night-led1", "x": 620, "y": 140, "properties": {"color": "yellow", "label": "LAMP"}}], components: [{"type": "wokwi-photoresistor-sensor", "id": "night-ldr", "x": 430, "y": 300, "properties": {"lux": 400, "dark": "1Meg", "k": 20, "pullup": "100k"}}, {"type": "wokwi-resistor", "id": "night-r1", "x": 620, "y": 40, "properties": {"value": "220"}}, {"type": "wokwi-led", "id": "night-led1", "x": 620, "y": 140, "properties": {"color": "yellow", "label": "LAMP"}}],
wires: [{"id": "night-w1", "start": {"componentId": "arduino-uno", "pinName": "3V3"}, "end": {"componentId": "night-ldr", "pinName": "VCC"}, "color": "#ff4444"}, {"id": "night-w2", "start": {"componentId": "arduino-uno", "pinName": "GND"}, "end": {"componentId": "night-ldr", "pinName": "GND"}, "color": "#000000"}, {"id": "night-w3", "start": {"componentId": "night-ldr", "pinName": "AO"}, "end": {"componentId": "arduino-uno", "pinName": "34"}, "color": "#22aaff"}, {"id": "night-w4", "start": {"componentId": "arduino-uno", "pinName": "2"}, "end": {"componentId": "night-r1", "pinName": "1"}, "color": "#cc44cc"}, {"id": "night-w5", "start": {"componentId": "night-r1", "pinName": "2"}, "end": {"componentId": "night-led1", "pinName": "A"}, "color": "#cc44cc"}, {"id": "night-w6", "start": {"componentId": "night-led1", "pinName": "C"}, "end": {"componentId": "arduino-uno", "pinName": "GND"}, "color": "#000000"}], wires: [{"id": "night-w1", "start": {"componentId": "arduino-uno", "pinName": "3V3"}, "end": {"componentId": "night-ldr", "pinName": "VCC"}, "color": "#ff4444"}, {"id": "night-w2", "start": {"componentId": "arduino-uno", "pinName": "GND"}, "end": {"componentId": "night-ldr", "pinName": "GND"}, "color": "#000000"}, {"id": "night-w3", "start": {"componentId": "night-ldr", "pinName": "AO"}, "end": {"componentId": "arduino-uno", "pinName": "34"}, "color": "#22aaff"}, {"id": "night-w4", "start": {"componentId": "arduino-uno", "pinName": "2"}, "end": {"componentId": "night-r1", "pinName": "1"}, "color": "#cc44cc"}, {"id": "night-w5", "start": {"componentId": "night-r1", "pinName": "2"}, "end": {"componentId": "night-led1", "pinName": "A"}, "color": "#cc44cc"}, {"id": "night-w6", "start": {"componentId": "night-led1", "pinName": "C"}, "end": {"componentId": "arduino-uno", "pinName": "GND"}, "color": "#000000"}],
tags: ["100-days", "esp32", "ldr", "micropython"], tags: ["100-days", "esp32", "ldr", "micropython"],
}, },

View File

@ -1206,7 +1206,24 @@ const MAPPERS: Record<string, Mapper> = {
// The DO (digital threshold) pin is ignored for analog simulation. // The DO (digital threshold) pin is ignored for analog simulation.
photoresistor: (comp, netLookup) => { photoresistor: (comp, netLookup) => {
const lux = Number(comp.properties.lux ?? 500); const lux = Number(comp.properties.lux ?? 500);
const Rdark = parseValueWithUnits(comp.properties.dark, 1_000_000); // `dark` goes through the SPICE suffix convention, where M is MILLI and
// mega is `Meg`. A dark resistance below 1k is not a photoresistor — it
// is almost always "1M" written meaning 1 megaohm, which parses to
// 0.001 ohm and shorts AO straight to VCC. The symptom is nasty because
// it looks like the part works: the ADC just reads full scale forever and
// the lux control does nothing (every value is still a short). Clamp to
// the documented default and say so, rather than emitting a short.
const darkRaw = parseValueWithUnits(comp.properties.dark, 1_000_000);
let Rdark = darkRaw;
if (!(darkRaw >= 1_000)) {
Rdark = 1_000_000;
// eslint-disable-next-line no-console
console.warn(
`[spice] ${comp.id}: dark resistance ${JSON.stringify(comp.properties.dark)} ` +
`parsed as ${darkRaw} ohm — too low for a photoresistor. SPICE reads M as ` +
`milli; write "1Meg" for 1 megaohm. Using 1Meg.`,
);
}
const k = Number(comp.properties.k ?? 5); const k = Number(comp.properties.k ?? 5);
const Rldr = Rdark / (1 + (k * lux) / 1000); const Rldr = Rdark / (1 + (k * lux) / 1000);
const vcc = netLookup('VCC'); const vcc = netLookup('VCC');