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 eb0697d2..f24bd7e0 100644 --- a/frontend/src/__tests__/__snapshots__/examples-netlist-snapshot.test.ts.snap +++ b/frontend/src/__tests__/__snapshots__/examples-netlist-snapshot.test.ts.snap @@ -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`] = ` "* Velxio circuit 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-r1 n0 n2 220 V_night-led1_sense n2 night-led1_sense_mid DC 0 diff --git a/frontend/src/__tests__/component-to-spice.test.ts b/frontend/src/__tests__/component-to-spice.test.ts index ff75ce3a..6ec7d6e3 100644 --- a/frontend/src/__tests__/component-to-spice.test.ts +++ b/frontend/src/__tests__/component-to-spice.test.ts @@ -410,3 +410,36 @@ describe('componentToSpice — ngspice accepts every card', () => { }); } }); + +describe('photoresistor — dark resistance suffix', () => { + const mapOne = (properties: Record) => { + const nets: Record = { 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'); + }); +}); diff --git a/frontend/src/data/examples-100-days.ts b/frontend/src/data/examples-100-days.ts index 2a27b4d7..c008a63e 100644 --- a/frontend/src/data/examples-100-days.ts +++ b/frontend/src/data/examples-100-days.ts @@ -364,7 +364,7 @@ while True: ` }, ], 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"}], tags: ["100-days", "esp32", "ldr", "micropython"], }, diff --git a/frontend/src/simulation/spice/componentToSpice.ts b/frontend/src/simulation/spice/componentToSpice.ts index d4aff35e..aa4c8788 100644 --- a/frontend/src/simulation/spice/componentToSpice.ts +++ b/frontend/src/simulation/spice/componentToSpice.ts @@ -1206,7 +1206,24 @@ const MAPPERS: Record = { // The DO (digital threshold) pin is ignored for analog simulation. photoresistor: (comp, netLookup) => { 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 Rldr = Rdark / (1 + (k * lux) / 1000); const vcc = netLookup('VCC');