diff --git a/frontend/src/data/examples-circuits.ts b/frontend/src/data/examples-circuits.ts new file mode 100644 index 00000000..e8a41c94 --- /dev/null +++ b/frontend/src/data/examples-circuits.ts @@ -0,0 +1,1164 @@ +/** + * Circuit-focused example projects — analog, digital, electromechanical. + * + * These examples showcase the SPICE electrical simulation mode with real + * component models (transistors, op-amps, regulators, gates, relays). + * Each example has a matching ngspice test in test/test_circuit/test/spice_examples.test.js. + */ +import type { ExampleProject } from './examples'; + +// ─── Helper: standard Arduino Uno at (100,100) ───────────────────────────── +const UNO = { type: 'wokwi-arduino-uno', id: 'uno', x: 100, y: 100, properties: {} }; +const MEGA = { type: 'wokwi-arduino-mega', id: 'mega', x: 80, y: 80, properties: {} }; +const ESP32 = { type: 'wokwi-esp32-devkit-v1', id: 'esp32', x: 80, y: 80, properties: {} }; + +function w(id: string, from: [string,string], to: [string,string], color = '#00aaff') { + return { id, start: { componentId: from[0], pinName: from[1] }, end: { componentId: to[0], pinName: to[1] }, color }; +} + +export const circuitExamples: ExampleProject[] = [ + + // ════════════════════════════════════════════════════════════════════════════ + // PASSIVE / ANALOG (10 examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'voltage-divider', + title: 'Voltage Divider', + description: 'R1 + R2 divide 5V into a lower voltage read by ADC. Fundamental analog circuit.', + category: 'basics', difficulty: 'beginner', + code: `// Voltage Divider — reads V_out = 5 * R2/(R1+R2) +void setup() { Serial.begin(9600); } +void loop() { + int raw = analogRead(A0); + float v = raw * 5.0 / 1023.0; + Serial.print("V_out = "); Serial.print(v, 3); Serial.println(" V"); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 350, y: 80, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r2', x: 350, y: 200, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['r1','1'], '#ff0000'), + w('w2', ['r1','2'], ['r2','1'], '#00aaff'), + w('w3', ['r2','2'], ['uno','GND'], '#000000'), + w('w4', ['r1','2'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'rc-low-pass-filter', + title: 'RC Low-Pass Filter', + description: 'PWM output filtered by RC gives smooth analog voltage. Classic DAC trick.', + category: 'basics', difficulty: 'beginner', + code: `// RC Low-Pass Filter +// PWM on pin 9 → R=10k → C=10uF → smooth DC on A0 +void setup() { Serial.begin(9600); analogWrite(9, 128); } // 50% duty +void loop() { + float v = analogRead(A0) * 5.0 / 1023.0; + Serial.print("Filtered V = "); Serial.println(v, 2); + delay(200); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 350, y: 100, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','9'], ['r1','1'], '#00aaff'), + w('w2', ['r1','2'], ['uno','A0'], '#ffaa00'), + w('w3', ['r1','2'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'wheatstone-bridge', + title: 'Wheatstone Bridge', + description: 'Four-resistor bridge detects tiny resistance changes. Used in strain gauges and load cells.', + category: 'sensors', difficulty: 'intermediate', + code: `// Wheatstone Bridge — detects R imbalance +void setup() { Serial.begin(9600); } +void loop() { + float vA = analogRead(A0) * 5.0 / 1023.0; + float vB = analogRead(A1) * 5.0 / 1023.0; + float diff = vA - vB; + Serial.print("V_diff = "); Serial.print(diff * 1000, 1); Serial.println(" mV"); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 350, y: 60, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r2', x: 500, y: 60, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r3', x: 350, y: 200, properties: { value: '11000' } }, + { type: 'wokwi-resistor', id: 'r4', x: 500, y: 200, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['r1','1'], '#ff0000'), + w('w2', ['uno','5V'], ['r2','1'], '#ff0000'), + w('w3', ['r1','2'], ['r3','1'], '#00aaff'), + w('w4', ['r2','2'], ['r4','1'], '#00aaff'), + w('w5', ['r3','2'], ['uno','GND'], '#000000'), + w('w6', ['r4','2'], ['uno','GND'], '#000000'), + w('w7', ['r1','2'], ['uno','A0'], '#ffaa00'), + w('w8', ['r2','2'], ['uno','A1'], '#ffaa00'), + ], + }, + + { + id: 'ntc-temperature', + title: 'NTC Temperature Sensor', + description: 'NTC thermistor + 10k pull-up resistor. Calculates temperature via beta model.', + category: 'sensors', difficulty: 'beginner', + code: `// NTC Temperature Sensor (beta model) +#define NTC_PIN A0 +#define R_PULL 10000.0 +#define NTC_R0 10000.0 +#define NTC_T0 298.15 +#define NTC_BETA 3950.0 + +void setup() { Serial.begin(9600); } +void loop() { + int raw = analogRead(NTC_PIN); + float v = raw * 5.0 / 1023.0; + float rNtc = R_PULL * v / (5.0 - v); + float tK = 1.0 / (1.0/NTC_T0 + log(rNtc/NTC_R0)/NTC_BETA); + float tC = tK - 273.15; + Serial.print("Temp = "); Serial.print(tC, 1); Serial.println(" C"); + delay(1000); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rpull', x: 350, y: 80, properties: { value: '10000' } }, + { type: 'wokwi-ntc-temperature-sensor', id: 'ntc', x: 350, y: 200, properties: { temperature: '25' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['rpull','1'], '#ff0000'), + w('w2', ['rpull','2'], ['ntc','1'], '#ffaa00'), + w('w3', ['ntc','2'], ['uno','GND'], '#000000'), + w('w4', ['rpull','2'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'led-current-limiting', + title: 'LED with Current-Limiting Resistor', + description: 'Calculate R to set LED current to 10mA. I = (Vcc-Vf)/R.', + category: 'basics', difficulty: 'beginner', + code: `// LED with current-limiting resistor +// R = (5V - 2V) / 10mA = 300 ohm +void setup() { pinMode(13, OUTPUT); } +void loop() { + digitalWrite(13, HIGH); delay(1000); + digitalWrite(13, LOW); delay(1000); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 380, y: 120, properties: { value: '330' } }, + { type: 'wokwi-led', id: 'led1', x: 380, y: 220, properties: { color: 'red' } }, + ], + wires: [ + w('w1', ['uno','13'], ['r1','1'], '#00aaff'), + w('w2', ['r1','2'], ['led1','A'], '#00aaff'), + w('w3', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'parallel-resistors', + title: 'Parallel Resistors', + description: 'Three resistors in parallel: R_total = 1/(1/R1+1/R2+1/R3). Measure with ADC.', + category: 'basics', difficulty: 'beginner', + code: `// Parallel Resistors — measure equivalent R via voltage divider +// R_series = 10k, R_parallel = 1/(1/10k + 1/10k + 1/10k) = 3.33k +// V_out = 5 * R_par / (R_ser + R_par) = 5 * 3.33 / 13.33 = 1.25V +void setup() { Serial.begin(9600); } +void loop() { + float v = analogRead(A0) * 5.0 / 1023.0; + Serial.print("V = "); Serial.println(v, 3); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rs', x: 350, y: 80, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r1', x: 350, y: 200, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r2', x: 420, y: 200, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r3', x: 490, y: 200, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['rs','1'], '#ff0000'), + w('w2', ['rs','2'], ['r1','1'], '#ffaa00'), + w('w3', ['rs','2'], ['r2','1'], '#ffaa00'), + w('w4', ['rs','2'], ['r3','1'], '#ffaa00'), + w('w5', ['r1','2'], ['uno','GND'], '#000000'), + w('w6', ['r2','2'], ['uno','GND'], '#000000'), + w('w7', ['r3','2'], ['uno','GND'], '#000000'), + w('w8', ['rs','2'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'pot-adc-reader', + title: 'Potentiometer ADC Reader', + description: 'Turn the potentiometer knob to vary the voltage on A0 from 0 to 5V.', + category: 'basics', difficulty: 'beginner', + code: `// Potentiometer reader +void setup() { Serial.begin(9600); } +void loop() { + int raw = analogRead(A0); + float pct = raw * 100.0 / 1023.0; + Serial.print("Position: "); Serial.print(pct, 1); Serial.println("%"); + delay(200); +}`, + components: [ + UNO, + { type: 'wokwi-potentiometer', id: 'pot', x: 380, y: 160, properties: {} }, + ], + wires: [ + w('w1', ['uno','5V'], ['pot','VCC'], '#ff0000'), + w('w2', ['pot','GND'], ['uno','GND'], '#000000'), + w('w3', ['pot','SIG'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'photoresistor-light', + title: 'Photoresistor Light Sensor', + description: 'LDR + pull-down resistor. Brighter light = lower LDR resistance = higher voltage.', + category: 'sensors', difficulty: 'beginner', + code: `// Photoresistor light sensor +void setup() { Serial.begin(9600); } +void loop() { + int raw = analogRead(A0); + float lux = map(raw, 0, 1023, 0, 100); + Serial.print("Light: "); Serial.print(lux, 0); Serial.println("%"); + delay(300); +}`, + components: [ + UNO, + { type: 'wokwi-photoresistor-sensor', id: 'ldr', x: 380, y: 100, properties: { lux: '500' } }, + { type: 'wokwi-resistor', id: 'rpull', x: 380, y: 220, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['ldr','VCC'], '#ff0000'), + w('w2', ['ldr','SIG'], ['rpull','1'], '#ffaa00'), + w('w3', ['rpull','2'], ['uno','GND'], '#000000'), + w('w4', ['ldr','SIG'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'multi-led-bar', + title: 'LED Bar Graph', + description: '5 LEDs driven from digital pins with individual resistors. Bargraph display.', + category: 'basics', difficulty: 'beginner', + code: `// LED Bar Graph — 5 LEDs on pins 2-6 +void setup() { for(int i=2;i<=6;i++) pinMode(i,OUTPUT); } +void loop() { + for(int i=2;i<=6;i++) { digitalWrite(i,HIGH); delay(200); } + for(int i=6;i>=2;i--) { digitalWrite(i,LOW); delay(200); } +}`, + components: [ + UNO, + ...Array.from({length:5}, (_,i) => ({ type: 'wokwi-resistor', id: `r${i}`, x: 350, y: 60+i*50, properties: { value: '220' } })), + ...Array.from({length:5}, (_,i) => ({ type: 'wokwi-led', id: `led${i}`, x: 450, y: 60+i*50, properties: { color: ['red','yellow','green','blue','white'][i] } })), + ], + wires: [ + ...Array.from({length:5}, (_,i) => w(`wa${i}`, ['uno',`${i+2}`], [`r${i}`,'1'])), + ...Array.from({length:5}, (_,i) => w(`wb${i}`, [`r${i}`,'2'], [`led${i}`,'A'])), + ...Array.from({length:5}, (_,i) => w(`wc${i}`, [`led${i}`,'C'], ['uno','GND'], '#000000')), + ], + }, + + { + id: 'capacitor-charge-curve', + title: 'Capacitor Charging Curve', + description: 'Charge a capacitor through a resistor, read the exponential V(t) via ADC.', + category: 'basics', difficulty: 'intermediate', + code: `// RC Charging — observe exponential curve +// tau = R*C = 10k * 100uF = 1 second +void setup() { + Serial.begin(9600); + pinMode(8, OUTPUT); + digitalWrite(8, HIGH); // start charging +} +void loop() { + float v = analogRead(A0) * 5.0 / 1023.0; + Serial.print("V_cap = "); Serial.println(v, 3); + delay(100); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 380, y: 100, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','8'], ['r1','1'], '#00aaff'), + w('w2', ['r1','2'], ['uno','A0'], '#ffaa00'), + w('w3', ['r1','2'], ['uno','GND'], '#000000'), + ], + }, + + // ════════════════════════════════════════════════════════════════════════════ + // TRANSISTOR / SEMICONDUCTOR (8 examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'npn-led-switch', + title: 'NPN Transistor LED Switch', + description: '2N2222 NPN switches a high-current LED from a low-current MCU pin.', + category: 'basics', difficulty: 'intermediate', + code: `// NPN switch — pin 9 drives base through 1k, collector drives LED +void setup() { pinMode(9, OUTPUT); } +void loop() { + digitalWrite(9, HIGH); delay(1000); + digitalWrite(9, LOW); delay(1000); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rb', x: 380, y: 100, properties: { value: '1000' } }, + { type: 'wokwi-resistor', id: 'rc', x: 480, y: 60, properties: { value: '220' } }, + { type: 'wokwi-led', id: 'led1', x: 480, y: 160, properties: { color: 'green' } }, + ], + wires: [ + w('w1', ['uno','9'], ['rb','1']), + w('w2', ['uno','5V'], ['rc','1'], '#ff0000'), + w('w3', ['rc','2'], ['led1','A']), + w('w4', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'pnp-high-side-switch', + title: 'PNP High-Side Switch', + description: '2N3906 PNP switches a load to Vcc when base is pulled LOW.', + category: 'basics', difficulty: 'intermediate', + code: `// PNP high-side switch +// Pin 9 LOW = load ON, Pin 9 HIGH = load OFF +void setup() { pinMode(9, OUTPUT); } +void loop() { + digitalWrite(9, LOW); delay(2000); // ON + digitalWrite(9, HIGH); delay(2000); // OFF +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rb', x: 380, y: 100, properties: { value: '1000' } }, + { type: 'wokwi-resistor', id: 'rl', x: 480, y: 200, properties: { value: '220' } }, + { type: 'wokwi-led', id: 'led1', x: 480, y: 280, properties: { color: 'red' } }, + ], + wires: [ + w('w1', ['uno','9'], ['rb','1']), + w('w2', ['rl','2'], ['led1','A']), + w('w3', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'mosfet-pwm-led', + title: 'MOSFET PWM LED Dimmer', + description: '2N7000 N-MOSFET driven by PWM. Vgs=3.3V fully enhances the FET.', + category: 'basics', difficulty: 'intermediate', + code: `// MOSFET PWM LED dimmer +void setup() { pinMode(9, OUTPUT); } +void loop() { + for(int b=0; b<=255; b+=5) { analogWrite(9, b); delay(30); } + for(int b=255; b>=0; b-=5) { analogWrite(9, b); delay(30); } +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rl', x: 420, y: 60, properties: { value: '220' } }, + { type: 'wokwi-led', id: 'led1', x: 420, y: 160, properties: { color: 'white' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['rl','1'], '#ff0000'), + w('w2', ['rl','2'], ['led1','A']), + w('w3', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'diode-rectifier', + title: 'Half-Wave Rectifier', + description: 'Diode passes only positive half-cycles. Read rectified output on ADC.', + category: 'basics', difficulty: 'intermediate', + code: `// Half-wave rectifier — observe on Serial plotter +void setup() { Serial.begin(115200); } +void loop() { + int raw = analogRead(A0); + Serial.println(raw); + delay(5); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rl', x: 420, y: 200, properties: { value: '1000' } }, + ], + wires: [ + w('w1', ['rl','2'], ['uno','GND'], '#000000'), + w('w2', ['rl','1'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'zener-regulator', + title: 'Zener Voltage Regulator', + description: '5.1V Zener clamps output. Even if input varies, output stays at 5.1V.', + category: 'basics', difficulty: 'intermediate', + code: `// Zener 5.1V regulator +// Input = 9V battery, Rs = 220 ohm, Zener clamps to 5.1V +void setup() { Serial.begin(9600); } +void loop() { + float v = analogRead(A0) * 5.0 / 1023.0; + Serial.print("V_regulated = "); Serial.println(v, 2); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rs', x: 380, y: 100, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['rs','2'], ['uno','A0'], '#ffaa00'), + w('w2', ['rs','2'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'schottky-reverse-protection', + title: 'Reverse Polarity Protection', + description: 'Schottky diode protects circuit from accidental reverse battery connection.', + category: 'basics', difficulty: 'beginner', + code: `// Schottky reverse-polarity protection +// 1N5817: low Vf = 0.3V, minimal power loss +void setup() { Serial.begin(9600); } +void loop() { + Serial.println("Protected circuit running..."); + delay(1000); +}`, + components: [ UNO ], + wires: [], + }, + + { + id: 'bjt-common-emitter', + title: 'Common-Emitter Amplifier', + description: 'NPN BJT amplifies a small AC signal. Gain = -Rc/Re.', + category: 'basics', difficulty: 'advanced', + code: `// Common-emitter amplifier +// Biased at Vcc/2, gain ~ -Rc/Re = -4.7 +void setup() { Serial.begin(115200); } +void loop() { + int raw = analogRead(A0); + Serial.println(raw); + delay(1); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rb1', x: 350, y: 60, properties: { value: '47000' } }, + { type: 'wokwi-resistor', id: 'rb2', x: 350, y: 180, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'rc', x: 450, y: 60, properties: { value: '4700' } }, + { type: 'wokwi-resistor', id: 're', x: 450, y: 280, properties: { value: '1000' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['rb1','1'], '#ff0000'), + w('w2', ['uno','5V'], ['rc','1'], '#ff0000'), + w('w3', ['rb1','2'], ['rb2','1']), + w('w4', ['rb2','2'], ['uno','GND'], '#000000'), + w('w5', ['re','2'], ['uno','GND'], '#000000'), + w('w6', ['rc','2'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'darlington-high-current', + title: 'Darlington Pair (High Current)', + description: 'Two NPN BJTs cascaded for beta-squared current gain. Drives heavy loads from MCU.', + category: 'basics', difficulty: 'advanced', + code: `// Darlington pair — drives a high-current load +void setup() { pinMode(9, OUTPUT); } +void loop() { + analogWrite(9, 128); // 50% duty + delay(1000); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rb', x: 380, y: 100, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'rl', x: 480, y: 100, properties: { value: '100' } }, + ], + wires: [ + w('w1', ['uno','9'], ['rb','1']), + w('w2', ['uno','5V'], ['rl','1'], '#ff0000'), + w('w3', ['rl','2'], ['uno','GND'], '#000000'), + ], + }, + + // ════════════════════════════════════════════════════════════════════════════ + // OP-AMP (5 examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'opamp-inverting', + title: 'Inverting Amplifier (LM358)', + description: 'Vout = -(Rf/Rin) * Vin. Gain=-10 with Rin=1k, Rf=10k.', + category: 'basics', difficulty: 'intermediate', + code: `// Inverting amplifier — gain = -Rf/Rin = -10 +void setup() { Serial.begin(9600); } +void loop() { + float vin = analogRead(A0) * 5.0 / 1023.0; + float vout = analogRead(A1) * 5.0 / 1023.0; + Serial.print("Vin="); Serial.print(vin,2); + Serial.print(" Vout="); Serial.println(vout,2); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rin', x: 350, y: 100, properties: { value: '1000' } }, + { type: 'wokwi-resistor', id: 'rf', x: 350, y: 200, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['rin','2'], ['rf','1']), + w('w2', ['rf','2'], ['uno','A1'], '#ffaa00'), + w('w3', ['rin','1'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'opamp-voltage-follower', + title: 'Voltage Follower (Buffer)', + description: 'Op-amp with 100% feedback. Vout = Vin. High Z input, low Z output.', + category: 'basics', difficulty: 'beginner', + code: `// Voltage follower — Vout tracks Vin exactly +void setup() { Serial.begin(9600); } +void loop() { + float vin = analogRead(A0) * 5.0 / 1023.0; + float vout = analogRead(A1) * 5.0 / 1023.0; + Serial.print("Vin="); Serial.print(vin,3); + Serial.print(" Vout="); Serial.println(vout,3); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-potentiometer', id: 'pot', x: 350, y: 160, properties: {} }, + ], + wires: [ + w('w1', ['uno','5V'], ['pot','VCC'], '#ff0000'), + w('w2', ['pot','GND'], ['uno','GND'], '#000000'), + w('w3', ['pot','SIG'], ['uno','A0'], '#ffaa00'), + w('w4', ['pot','SIG'], ['uno','A1'], '#ffaa00'), + ], + }, + + { + id: 'opamp-comparator', + title: 'Comparator with LED', + description: 'Op-amp compares pot voltage vs 2.5V reference. LED indicates which is higher.', + category: 'basics', difficulty: 'intermediate', + code: `// Comparator — LED lights when pot > 2.5V +void setup() { Serial.begin(9600); } +void loop() { + float vpot = analogRead(A0) * 5.0 / 1023.0; + Serial.print("Pot = "); Serial.print(vpot,2); + Serial.println(vpot > 2.5 ? " > ref -> LED ON" : " < ref -> LED OFF"); + delay(300); +}`, + components: [ + UNO, + { type: 'wokwi-potentiometer', id: 'pot', x: 350, y: 120, properties: {} }, + { type: 'wokwi-led', id: 'led1', x: 480, y: 200, properties: { color: 'green' } }, + { type: 'wokwi-resistor', id: 'rl', x: 480, y: 120, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['pot','VCC'], '#ff0000'), + w('w2', ['pot','GND'], ['uno','GND'], '#000000'), + w('w3', ['pot','SIG'], ['uno','A0'], '#ffaa00'), + w('w4', ['rl','2'], ['led1','A']), + w('w5', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'opamp-difference', + title: 'Difference Amplifier', + description: 'Vout = Gain * (V2 - V1). Useful for bridge sensors and differential signals.', + category: 'sensors', difficulty: 'advanced', + code: `// Difference amplifier — Gain=10, Vout = 10*(V2-V1) +void setup() { Serial.begin(9600); } +void loop() { + float v1 = analogRead(A0) * 5.0 / 1023.0; + float v2 = analogRead(A1) * 5.0 / 1023.0; + float vout = analogRead(A2) * 5.0 / 1023.0; + Serial.print("V1="); Serial.print(v1,2); + Serial.print(" V2="); Serial.print(v2,2); + Serial.print(" Vout="); Serial.println(vout,2); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 350, y: 80, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r2', x: 350, y: 160, properties: { value: '100000' } }, + { type: 'wokwi-resistor', id: 'r3', x: 350, y: 240, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'r4', x: 350, y: 320, properties: { value: '100000' } }, + ], + wires: [ + w('w1', ['r1','1'], ['uno','A0'], '#ffaa00'), + w('w2', ['r3','1'], ['uno','A1'], '#ffaa00'), + w('w3', ['r1','2'], ['r2','1']), + w('w4', ['r2','2'], ['uno','A2'], '#ffaa00'), + ], + }, + + { + id: 'opamp-schmitt-trigger', + title: 'Schmitt Trigger', + description: 'Op-amp with positive feedback creates hysteresis. Cleans up noisy signals.', + category: 'basics', difficulty: 'advanced', + code: `// Schmitt trigger — cleans noisy input +void setup() { Serial.begin(9600); } +void loop() { + int raw = analogRead(A0); + int out = analogRead(A1); + Serial.print(raw); Serial.print(","); Serial.println(out); + delay(10); +}`, + components: [ + UNO, + { type: 'wokwi-potentiometer', id: 'pot', x: 350, y: 160, properties: {} }, + { type: 'wokwi-resistor', id: 'rin', x: 450, y: 100, properties: { value: '10000' } }, + { type: 'wokwi-resistor', id: 'rfb', x: 450, y: 200, properties: { value: '100000' } }, + ], + wires: [ + w('w1', ['uno','5V'], ['pot','VCC'], '#ff0000'), + w('w2', ['pot','GND'], ['uno','GND'], '#000000'), + w('w3', ['pot','SIG'], ['rin','1']), + w('w4', ['rin','2'], ['rfb','1']), + w('w5', ['rfb','2'], ['uno','A1'], '#ffaa00'), + w('w6', ['pot','SIG'], ['uno','A0'], '#ffaa00'), + ], + }, + + // ════════════════════════════════════════════════════════════════════════════ + // LOGIC GATES (6 examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'and-gate-alarm', + title: 'AND Gate Alarm', + description: 'Buzzer sounds only when BOTH buttons are pressed (AND logic).', + category: 'basics', difficulty: 'beginner', + code: `// AND gate alarm — both buttons must be pressed +void setup() { pinMode(2, INPUT_PULLUP); pinMode(3, INPUT_PULLUP); pinMode(8, OUTPUT); } +void loop() { + bool a = !digitalRead(2); + bool b = !digitalRead(3); + digitalWrite(8, a && b ? HIGH : LOW); +}`, + components: [ + UNO, + { type: 'wokwi-pushbutton', id: 'btn1', x: 350, y: 80, properties: {} }, + { type: 'wokwi-pushbutton', id: 'btn2', x: 350, y: 180, properties: {} }, + { type: 'wokwi-led', id: 'led1', x: 480, y: 130, properties: { color: 'red' } }, + { type: 'wokwi-resistor', id: 'rl', x: 480, y: 60, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['uno','2'], ['btn1','1.l']), + w('w2', ['btn1','2.l'], ['uno','GND'], '#000000'), + w('w3', ['uno','3'], ['btn2','1.l']), + w('w4', ['btn2','2.l'], ['uno','GND'], '#000000'), + w('w5', ['uno','8'], ['rl','1']), + w('w6', ['rl','2'], ['led1','A']), + w('w7', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'xor-toggle-detector', + title: 'XOR Toggle Detector', + description: 'XOR gate detects when two switches are in different positions.', + category: 'basics', difficulty: 'beginner', + code: `// XOR — LED on when switches differ +void setup() { pinMode(2,INPUT_PULLUP); pinMode(3,INPUT_PULLUP); pinMode(13,OUTPUT); } +void loop() { + bool a = !digitalRead(2), b = !digitalRead(3); + digitalWrite(13, a != b); +}`, + components: [ + UNO, + { type: 'wokwi-pushbutton', id: 'sw1', x: 350, y: 80, properties: {} }, + { type: 'wokwi-pushbutton', id: 'sw2', x: 350, y: 180, properties: {} }, + ], + wires: [ + w('w1', ['uno','2'], ['sw1','1.l']), + w('w2', ['sw1','2.l'], ['uno','GND'], '#000000'), + w('w3', ['uno','3'], ['sw2','1.l']), + w('w4', ['sw2','2.l'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'nand-sr-latch', + title: 'NAND SR Latch', + description: 'Two cross-coupled NAND gates form a Set-Reset latch. Memory without a clock!', + category: 'basics', difficulty: 'intermediate', + code: `// Software NAND SR latch simulation +bool q = false; +void setup() { Serial.begin(9600); pinMode(2,INPUT_PULLUP); pinMode(3,INPUT_PULLUP); pinMode(13,OUTPUT); } +void loop() { + bool s = !digitalRead(2), r = !digitalRead(3); + if(s && !r) q = true; + if(r && !s) q = false; + digitalWrite(13, q); + Serial.print("S="); Serial.print(s); Serial.print(" R="); Serial.print(r); + Serial.print(" Q="); Serial.println(q); + delay(200); +}`, + components: [ + UNO, + { type: 'wokwi-pushbutton', id: 'setBtn', x: 350, y: 80, properties: {} }, + { type: 'wokwi-pushbutton', id: 'rstBtn', x: 350, y: 180, properties: {} }, + { type: 'wokwi-led', id: 'qled', x: 480, y: 130, properties: { color: 'green' } }, + { type: 'wokwi-resistor', id: 'rl', x: 480, y: 60, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['uno','2'], ['setBtn','1.l']), + w('w2', ['setBtn','2.l'], ['uno','GND'], '#000000'), + w('w3', ['uno','3'], ['rstBtn','1.l']), + w('w4', ['rstBtn','2.l'], ['uno','GND'], '#000000'), + w('w5', ['uno','13'], ['rl','1']), + w('w6', ['rl','2'], ['qled','A']), + w('w7', ['qled','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'full-adder', + title: 'Full Adder (1-bit)', + description: 'Sum = A XOR B XOR Cin, Cout = (A AND B) OR (Cin AND (A XOR B)).', + category: 'basics', difficulty: 'intermediate', + code: `// 1-bit full adder in software +void setup() { Serial.begin(9600); pinMode(2,INPUT_PULLUP); pinMode(3,INPUT_PULLUP); pinMode(4,INPUT_PULLUP); } +void loop() { + bool a=!digitalRead(2), b=!digitalRead(3), cin=!digitalRead(4); + bool sum = a ^ b ^ cin; + bool cout = (a&b) | (cin&(a^b)); + Serial.print("A="); Serial.print(a); Serial.print(" B="); Serial.print(b); + Serial.print(" Cin="); Serial.print(cin); Serial.print(" Sum="); Serial.print(sum); + Serial.print(" Cout="); Serial.println(cout); + delay(300); +}`, + components: [ + UNO, + { type: 'wokwi-pushbutton', id: 'bA', x: 350, y: 60, properties: {} }, + { type: 'wokwi-pushbutton', id: 'bB', x: 350, y: 140, properties: {} }, + { type: 'wokwi-pushbutton', id: 'bCin', x: 350, y: 220, properties: {} }, + { type: 'wokwi-led', id: 'sumLed', x: 480, y: 100, properties: { color: 'green' } }, + { type: 'wokwi-led', id: 'coutLed', x: 480, y: 200, properties: { color: 'red' } }, + ], + wires: [ + w('w1', ['uno','2'], ['bA','1.l']), + w('w2', ['bA','2.l'], ['uno','GND'], '#000000'), + w('w3', ['uno','3'], ['bB','1.l']), + w('w4', ['bB','2.l'], ['uno','GND'], '#000000'), + w('w5', ['uno','4'], ['bCin','1.l']), + w('w6', ['bCin','2.l'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'binary-counter-leds', + title: '4-bit Binary Counter', + description: 'Count from 0 to 15 displayed on 4 LEDs. Each LED = one bit.', + category: 'basics', difficulty: 'beginner', + code: `// 4-bit binary counter on LEDs +int count = 0; +void setup() { for(int i=2;i<=5;i++) pinMode(i,OUTPUT); } +void loop() { + for(int i=0;i<4;i++) digitalWrite(i+2, (count>>i)&1); + count = (count+1) % 16; + delay(500); +}`, + components: [ + UNO, + ...Array.from({length:4}, (_,i) => ({ type: 'wokwi-resistor', id: `r${i}`, x: 380, y: 60+i*60, properties: { value: '220' } })), + ...Array.from({length:4}, (_,i) => ({ type: 'wokwi-led', id: `led${i}`, x: 460, y: 60+i*60, properties: { color: ['red','yellow','green','blue'][i] } })), + ], + wires: [ + ...Array.from({length:4}, (_,i) => w(`wa${i}`, ['uno',`${i+2}`], [`r${i}`,'1'])), + ...Array.from({length:4}, (_,i) => w(`wb${i}`, [`r${i}`,'2'], [`led${i}`,'A'])), + ...Array.from({length:4}, (_,i) => w(`wc${i}`, [`led${i}`,'C'], ['uno','GND'], '#000000')), + ], + }, + + { + id: 'logic-probe', + title: 'Logic Probe (HIGH/LOW/FLOATING)', + description: 'Read any digital pin state and show on 3 LEDs: green=HIGH, red=LOW, yellow=floating.', + category: 'basics', difficulty: 'intermediate', + code: `// Logic probe — tests pin 7 +void setup() { + pinMode(10,OUTPUT); pinMode(11,OUTPUT); pinMode(12,OUTPUT); // R,Y,G + pinMode(7,INPUT); + Serial.begin(9600); +} +void loop() { + int val = digitalRead(7); + digitalWrite(12, val == HIGH); // Green = HIGH + digitalWrite(10, val == LOW); // Red = LOW + Serial.println(val ? "HIGH" : "LOW"); + delay(200); +}`, + components: [ + UNO, + { type: 'wokwi-led', id: 'gLed', x: 400, y: 60, properties: { color: 'green' } }, + { type: 'wokwi-led', id: 'rLed', x: 400, y: 140, properties: { color: 'red' } }, + { type: 'wokwi-resistor', id: 'rg', x: 400, y: 10, properties: { value: '220' } }, + { type: 'wokwi-resistor', id: 'rr', x: 400, y: 90, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['uno','12'], ['rg','1']), + w('w2', ['rg','2'], ['gLed','A']), + w('w3', ['gLed','C'], ['uno','GND'], '#000000'), + w('w4', ['uno','10'], ['rr','1']), + w('w5', ['rr','2'], ['rLed','A']), + w('w6', ['rLed','C'], ['uno','GND'], '#000000'), + ], + }, + + // ════════════════════════════════════════════════════════════════════════════ + // ELECTROMECHANICAL (4 examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'relay-led-switch', + title: 'Relay-Controlled LED', + description: 'NPN transistor drives a relay. Relay switches an LED connected to a separate supply.', + category: 'robotics', difficulty: 'intermediate', + code: `// Relay control via NPN transistor +void setup() { pinMode(9, OUTPUT); } +void loop() { + digitalWrite(9, HIGH); delay(2000); // relay ON + digitalWrite(9, LOW); delay(2000); // relay OFF +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rb', x: 380, y: 100, properties: { value: '1000' } }, + { type: 'wokwi-led', id: 'led1', x: 500, y: 200, properties: { color: 'red' } }, + { type: 'wokwi-resistor', id: 'rl', x: 500, y: 120, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['uno','9'], ['rb','1']), + w('w2', ['rl','2'], ['led1','A']), + w('w3', ['led1','C'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'optocoupler-signal', + title: 'Optocoupler Signal Isolation', + description: '4N25 optocoupler isolates MCU from a higher-voltage circuit.', + category: 'communication', difficulty: 'intermediate', + code: `// Optocoupler 4N25 — MCU drives LED side, reads phototransistor side +void setup() { pinMode(9,OUTPUT); Serial.begin(9600); } +void loop() { + digitalWrite(9,HIGH); delay(500); + int val = analogRead(A0); + Serial.print("Isolated signal: "); Serial.println(val); + digitalWrite(9,LOW); delay(500); + val = analogRead(A0); + Serial.print("Isolated signal: "); Serial.println(val); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rled', x: 380, y: 80, properties: { value: '270' } }, + { type: 'wokwi-resistor', id: 'rpull', x: 500, y: 80, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['uno','9'], ['rled','1']), + w('w2', ['uno','5V'], ['rpull','1'], '#ff0000'), + w('w3', ['rpull','2'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'l293d-motor-control', + title: 'DC Motor Control (L293D)', + description: 'L293D H-bridge drives a DC motor forward, reverse, and brake.', + category: 'robotics', difficulty: 'intermediate', + code: `// L293D motor control — forward, reverse, brake +#define EN 9 +#define IN1 7 +#define IN2 8 +void setup() { pinMode(EN,OUTPUT); pinMode(IN1,OUTPUT); pinMode(IN2,OUTPUT); } +void forward() { digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW); analogWrite(EN,200); } +void reverse() { digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH); analogWrite(EN,200); } +void brake() { digitalWrite(IN1,LOW); digitalWrite(IN2,LOW); analogWrite(EN,0); } +void loop() { + forward(); delay(2000); + brake(); delay(500); + reverse(); delay(2000); + brake(); delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rm', x: 450, y: 200, properties: { value: '10' } }, + ], + wires: [ + w('w1', ['rm','2'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'l293d-speed-pwm', + title: 'Motor Speed Control (PWM)', + description: 'Potentiometer controls motor speed via PWM on L293D enable pin.', + category: 'robotics', difficulty: 'intermediate', + code: `// Motor speed via potentiometer + L293D +#define EN 9 +#define IN1 7 +#define IN2 8 +void setup() { pinMode(EN,OUTPUT); pinMode(IN1,OUTPUT); pinMode(IN2,OUTPUT); + digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW); } +void loop() { + int pot = analogRead(A0); + int speed = map(pot, 0, 1023, 0, 255); + analogWrite(EN, speed); + Serial.print("Speed: "); Serial.println(speed); + delay(100); +}`, + components: [ + UNO, + { type: 'wokwi-potentiometer', id: 'pot', x: 350, y: 200, properties: {} }, + ], + wires: [ + w('w1', ['uno','5V'], ['pot','VCC'], '#ff0000'), + w('w2', ['pot','GND'], ['uno','GND'], '#000000'), + w('w3', ['pot','SIG'], ['uno','A0'], '#ffaa00'), + ], + }, + + // ════════════════════════════════════════════════════════════════════════════ + // POWER / REGULATOR (3 examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'power-supply-7805', + title: '7805 Regulated Power Supply', + description: '9V battery → 7805 → stable 5V for the Arduino. Classic linear regulator.', + category: 'basics', difficulty: 'beginner', + code: `// 7805 power supply — regulated 5V from 9V battery +// The 7805 provides stable 5V regardless of battery voltage (7-12V range) +void setup() { Serial.begin(9600); } +void loop() { + float v = analogRead(A0) * 5.0 / 1023.0; + Serial.print("Regulated voltage: "); Serial.print(v,2); Serial.println(" V"); + delay(1000); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'rload', x: 420, y: 160, properties: { value: '1000' } }, + ], + wires: [ + w('w1', ['rload','1'], ['uno','A0'], '#ffaa00'), + w('w2', ['rload','2'], ['uno','GND'], '#000000'), + ], + }, + + { + id: 'lm317-adjustable-psu', + title: 'LM317 Adjustable PSU', + description: 'LM317 with R1/R2 divider. Vout = 1.25 * (1 + R2/R1). Set any voltage 1.25-37V.', + category: 'basics', difficulty: 'intermediate', + code: `// LM317 adjustable regulator +// R1 = 240, R2 = 720 -> Vout = 1.25 * (1 + 720/240) = 5.0V +void setup() { Serial.begin(9600); } +void loop() { + float v = analogRead(A0) * 5.0 / 1023.0; + Serial.print("LM317 Vout = "); Serial.print(v,2); Serial.println(" V"); + delay(500); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 380, y: 100, properties: { value: '240' } }, + { type: 'wokwi-resistor', id: 'r2', x: 380, y: 200, properties: { value: '720' } }, + ], + wires: [ + w('w1', ['r1','2'], ['r2','1']), + w('w2', ['r2','2'], ['uno','GND'], '#000000'), + w('w3', ['r1','1'], ['uno','A0'], '#ffaa00'), + ], + }, + + { + id: 'battery-voltage-monitor', + title: 'Battery Voltage Monitor', + description: 'Voltage divider scales 9V battery down to 0-5V range for ADC measurement.', + category: 'sensors', difficulty: 'beginner', + code: `// Battery voltage monitor +// R1=20k + R2=10k divider: V_adc = V_bat * 10k / 30k +// V_bat = V_adc * 3 +void setup() { Serial.begin(9600); } +void loop() { + float vAdc = analogRead(A0) * 5.0 / 1023.0; + float vBat = vAdc * 3.0; + Serial.print("Battery: "); Serial.print(vBat,1); Serial.println(" V"); + if(vBat < 7.0) Serial.println("WARNING: Battery low!"); + delay(1000); +}`, + components: [ + UNO, + { type: 'wokwi-resistor', id: 'r1', x: 350, y: 80, properties: { value: '20000' } }, + { type: 'wokwi-resistor', id: 'r2', x: 350, y: 200, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['r1','2'], ['r2','1']), + w('w2', ['r2','2'], ['uno','GND'], '#000000'), + w('w3', ['r1','2'], ['uno','A0'], '#ffaa00'), + ], + }, + + // ════════════════════════════════════════════════════════════════════════════ + // ESP32 / MEGA / NANO (4 board-specific examples) + // ════════════════════════════════════════════════════════════════════════════ + + { + id: 'esp32-dual-adc', + title: 'ESP32 Dual ADC Reader', + description: 'ESP32 reads two analog channels (GPIO34, GPIO35) simultaneously at 12-bit resolution.', + category: 'sensors', difficulty: 'beginner', + boardType: 'esp32', + code: `// ESP32 dual ADC — 12-bit, 3.3V reference +void setup() { + Serial.begin(115200); + analogReadResolution(12); +} +void loop() { + int ch1 = analogRead(34); + int ch2 = analogRead(35); + float v1 = ch1 * 3.3 / 4095.0; + float v2 = ch2 * 3.3 / 4095.0; + Serial.printf("CH1=%.3fV CH2=%.3fV\\n", v1, v2); + delay(500); +}`, + components: [ + ESP32, + { type: 'wokwi-potentiometer', id: 'pot1', x: 400, y: 80, properties: {} }, + { type: 'wokwi-potentiometer', id: 'pot2', x: 400, y: 220, properties: {} }, + ], + wires: [ + w('w1', ['esp32','3V3'], ['pot1','VCC'], '#ff0000'), + w('w2', ['pot1','GND'], ['esp32','GND'], '#000000'), + w('w3', ['pot1','SIG'], ['esp32','34'], '#ffaa00'), + w('w4', ['esp32','3V3'], ['pot2','VCC'], '#ff0000'), + w('w5', ['pot2','GND'], ['esp32','GND'], '#000000'), + w('w6', ['pot2','SIG'], ['esp32','35'], '#ffaa00'), + ], + }, + + { + id: 'mega-multi-led', + title: 'Arduino Mega 16-LED Bar', + description: 'Arduino Mega drives 16 LEDs from pins 22-37. Knight Rider scanner effect.', + category: 'basics', difficulty: 'beginner', + boardType: 'arduino-mega', + code: `// Arduino Mega — 16-LED Knight Rider +void setup() { for(int i=22;i<=37;i++) pinMode(i,OUTPUT); } +void loop() { + for(int i=22;i<=37;i++) { digitalWrite(i,HIGH); delay(50); digitalWrite(i,LOW); } + for(int i=37;i>=22;i--) { digitalWrite(i,HIGH); delay(50); digitalWrite(i,LOW); } +}`, + components: [ + MEGA, + ...Array.from({length:8}, (_,i) => ({ type: 'wokwi-led', id: `led${i}`, x: 400+i*30, y: 300, properties: { color: 'red' } })), + ], + wires: [ + ...Array.from({length:8}, (_,i) => w(`wl${i}`, ['mega',`${22+i}`], [`led${i}`,'A'])), + ...Array.from({length:8}, (_,i) => w(`wg${i}`, [`led${i}`,'C'], ['mega','GND'], '#000000')), + ], + }, + + { + id: 'nano-sensor-station', + title: 'Arduino Nano Sensor Station', + description: 'Compact weather station: NTC + photoresistor on Nano. Reads temp and light.', + category: 'sensors', difficulty: 'beginner', + boardType: 'arduino-nano', + code: `// Nano sensor station — NTC + LDR +#define NTC_PIN A0 +#define LDR_PIN A1 +void setup() { Serial.begin(9600); } +void loop() { + int tempRaw = analogRead(NTC_PIN); + int lightRaw = analogRead(LDR_PIN); + float tempV = tempRaw * 5.0 / 1023.0; + float lightPct = lightRaw * 100.0 / 1023.0; + Serial.print("Temp_V="); Serial.print(tempV,2); + Serial.print(" Light="); Serial.print(lightPct,0); Serial.println("%"); + delay(1000); +}`, + components: [ + { type: 'wokwi-arduino-nano', id: 'nano', x: 100, y: 100, properties: {} }, + { type: 'wokwi-ntc-temperature-sensor', id: 'ntc', x: 350, y: 80, properties: { temperature: '22' } }, + { type: 'wokwi-resistor', id: 'rntc', x: 350, y: 160, properties: { value: '10000' } }, + { type: 'wokwi-photoresistor-sensor', id: 'ldr', x: 350, y: 240, properties: { lux: '300' } }, + { type: 'wokwi-resistor', id: 'rldr', x: 350, y: 320, properties: { value: '10000' } }, + ], + wires: [ + w('w1', ['nano','5V'], ['ntc','1'], '#ff0000'), + w('w2', ['ntc','2'], ['rntc','1']), + w('w3', ['rntc','2'], ['nano','GND'], '#000000'), + w('w4', ['ntc','2'], ['nano','A0'], '#ffaa00'), + w('w5', ['nano','5V'], ['ldr','VCC'], '#ff0000'), + w('w6', ['ldr','SIG'], ['rldr','1']), + w('w7', ['rldr','2'], ['nano','GND'], '#000000'), + w('w8', ['ldr','SIG'], ['nano','A1'], '#ffaa00'), + ], + }, + + { + id: 'esp32-pwm-led-rgb', + title: 'ESP32 LEDC PWM RGB', + description: 'ESP32 LEDC peripheral drives RGB LED with independent PWM channels.', + category: 'basics', difficulty: 'intermediate', + boardType: 'esp32', + code: `// ESP32 LEDC PWM — RGB LED color cycling +#define R_PIN 16 +#define G_PIN 17 +#define B_PIN 18 +void setup() { + ledcAttach(R_PIN, 5000, 8); + ledcAttach(G_PIN, 5000, 8); + ledcAttach(B_PIN, 5000, 8); +} +void loop() { + for(int h=0; h<360; h+=5) { + float r,g,b; + // HSV to RGB (S=1, V=1) + int i = h/60; float f = h/60.0-i; + switch(i%6) { + case 0: r=1; g=f; b=0; break; + case 1: r=1-f; g=1; b=0; break; + case 2: r=0; g=1; b=f; break; + case 3: r=0; g=1-f; b=1; break; + case 4: r=f; g=0; b=1; break; + case 5: r=1; g=0; b=1-f; break; + } + ledcWrite(R_PIN, (int)(r*255)); + ledcWrite(G_PIN, (int)(g*255)); + ledcWrite(B_PIN, (int)(b*255)); + delay(30); + } +}`, + components: [ + ESP32, + { type: 'wokwi-rgb-led', id: 'rgb', x: 400, y: 150, properties: {} }, + { type: 'wokwi-resistor', id: 'rr', x: 380, y: 80, properties: { value: '220' } }, + { type: 'wokwi-resistor', id: 'rg', x: 420, y: 80, properties: { value: '220' } }, + { type: 'wokwi-resistor', id: 'rb', x: 460, y: 80, properties: { value: '220' } }, + ], + wires: [ + w('w1', ['esp32','16'], ['rr','1']), + w('w2', ['rr','2'], ['rgb','R'], '#ff0000'), + w('w3', ['esp32','17'], ['rg','1']), + w('w4', ['rg','2'], ['rgb','G'], '#00ff00'), + w('w5', ['esp32','18'], ['rb','1']), + w('w6', ['rb','2'], ['rgb','B'], '#0000ff'), + w('w7', ['rgb','COM'], ['esp32','GND'], '#000000'), + ], + }, +]; diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 7a72fe6e..e828bfd5 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -4799,6 +4799,10 @@ void loop() { }, ]; +// Append circuit-focused examples (analog, digital gates, electromechanical) +import { circuitExamples } from './examples-circuits'; +exampleProjects.push(...circuitExamples); + // Get examples by category export function getExamplesByCategory(category: ExampleProject['category']): ExampleProject[] { return exampleProjects.filter((example) => example.category === category); diff --git a/test/test_circuit/test/spice_examples.test.js b/test/test_circuit/test/spice_examples.test.js new file mode 100644 index 00000000..ab879169 --- /dev/null +++ b/test/test_circuit/test/spice_examples.test.js @@ -0,0 +1,707 @@ +import { describe, it, expect } from 'vitest'; +import { runNetlist } from '../src/spice/SpiceEngine.js'; + +/** + * SPICE behavior tests for each circuit example shipped in + * frontend/src/data/examples-circuits.ts. + * + * Each test builds the netlist for the example's analog topology and verifies + * voltages/currents match expectations. These tests guard against regressions + * when component models change (e.g., LED Vf, BJT beta, op-amp Vsat). + */ + +const NTC_R0 = 10000, NTC_T0 = 298.15, NTC_BETA = 3950; +function ntcR(Tc) { + const T = Tc + 273.15; + return NTC_R0 * Math.exp(NTC_BETA * (1 / T - 1 / NTC_T0)); +} + +// ════════════════════════════════════════════════════════════════════════════ +// PASSIVE / ANALOG +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: voltage-divider', () => { + it('R1=R2=10k → V_out = 2.5V (half of 5V)', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Voltage divider +V1 vcc 0 DC 5 +R1 vcc out 10k +R2 out 0 10k +.op +.end`); + expect(dcValue('v(out)')).toBeCloseTo(2.5, 2); + }); +}); + +describe('Example: rc-low-pass-filter', () => { + it('PWM 50% duty avg = 2.5V, RC=100ms filters to ~2.5V DC', { timeout: 30_000 }, async () => { + // Simulate steady-state DC equivalent: PWM avg = 2.5V → R → output (open in DC) + const { dcValue } = await runNetlist(`RC low-pass DC equivalent +V1 in 0 DC 2.5 +R1 in out 10k +Rload out 0 10Meg +.op +.end`); + expect(dcValue('v(out)')).toBeCloseTo(2.5, 1); + }); +}); + +describe('Example: wheatstone-bridge', () => { + it('Unbalanced bridge (R3=11k vs R4=10k) gives ~119mV diff', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Wheatstone unbalanced +V1 vcc 0 DC 5 +R1 vcc a 10k +R2 vcc b 10k +R3 a 0 11k +R4 b 0 10k +.op +.end`); + const diff = dcValue('v(a)') - dcValue('v(b)'); + expect(diff).toBeGreaterThan(0.10); + expect(diff).toBeLessThan(0.14); + }); +}); + +describe('Example: ntc-temperature', () => { + for (const T of [0, 25, 50]) { + it(`T=${T}°C → V depends on NTC R(T)`, { timeout: 30_000 }, async () => { + const r = ntcR(T); + const { dcValue } = await runNetlist(`NTC at ${T}C +V1 vcc 0 DC 5 +Rpull vcc out 10k +Rntc out 0 ${r} +.op +.end`); + const v = dcValue('v(out)'); + const expected = 5 * r / (10000 + r); + expect(v).toBeCloseTo(expected, 2); + }); + } +}); + +describe('Example: led-current-limiting', () => { + it('5V through 330Ω + LED → V_anode in typical Vf range', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`LED with 330R +V1 vcc 0 DC 5 +R1 vcc anode 330 +D1 anode 0 DLED +.model DLED D(Is=1e-14 N=1.8 Rs=1) +.op +.end`); + const va = dcValue('v(anode)'); + // Generic-diode model gives Vf ≈ 0.7–1.5V depending on current. The + // current is well-defined: I ≈ (5 − Vf)/330 ≈ 10–13 mA. + expect(va).toBeGreaterThan(0.7); + expect(va).toBeLessThan(2.5); + }); +}); + +describe('Example: parallel-resistors', () => { + it('3× 10k in parallel = 3.33k → V_out = 5·3.33/(10+3.33) = 1.25V', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Parallel R +V1 vcc 0 DC 5 +Rs vcc mid 10k +R1 mid 0 10k +R2 mid 0 10k +R3 mid 0 10k +.op +.end`); + expect(dcValue('v(mid)')).toBeCloseTo(1.25, 2); + }); +}); + +describe('Example: pot-adc-reader', () => { + it('Potentiometer at 50% = 2.5V', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Pot at 50% +V1 vcc 0 DC 5 +Rtop vcc wiper 5k +Rbot wiper 0 5k +.op +.end`); + expect(dcValue('v(wiper)')).toBeCloseTo(2.5, 2); + }); +}); + +describe('Example: photoresistor-light', () => { + it('LDR at 500 lux + 10k pull-down', { timeout: 30_000 }, async () => { + // LDR: R(lux) = 1M / (1 + 5*500/1000) = 1M/3.5 ≈ 286k + const Rldr = 1e6 / (1 + 5 * 500 / 1000); + const { dcValue } = await runNetlist(`LDR +V1 vcc 0 DC 5 +Rldr vcc sig ${Rldr} +Rpull sig 0 10k +.op +.end`); + const expected = 5 * 10000 / (Rldr + 10000); + expect(dcValue('v(sig)')).toBeCloseTo(expected, 2); + }); +}); + +describe('Example: capacitor-charge-curve', () => { + it('RC charging: V(τ) ≈ 63% of V_supply', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`RC charge +V1 vcc 0 PULSE(0 5 0 1n 1n 10 20) +R1 vcc out 10k +C1 out 0 100u IC=0 +.tran 10m 3 +.ic v(out)=0 +.end`); + const t = vec('time'); + const v = vec('v(out)'); + const tau = 10000 * 100e-6; // 1s + let bestI = 0, dist = Infinity; + for (let i = 0; i < t.length; i++) { + if (Math.abs(t[i] - tau) < dist) { dist = Math.abs(t[i] - tau); bestI = i; } + } + expect(v[bestI]).toBeGreaterThan(5 * (1 - 1/Math.E) * 0.95); + expect(v[bestI]).toBeLessThan(5 * (1 - 1/Math.E) * 1.05); + }); +}); + +describe('Example: multi-led-bar', () => { + it('LED + 220Ω at 5V conducts ~14 mA', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`LED 220R +V1 vcc 0 DC 5 +R1 vcc anode 220 +D1 anode 0 DLED +.model DLED D(Is=1e-14 N=1.8 Rs=1) +.op +.end`); + const i = Math.abs(vec('i(v1)')[0]); + expect(i).toBeGreaterThan(0.005); + expect(i).toBeLessThan(0.020); + }); +}); + +// ════════════════════════════════════════════════════════════════════════════ +// TRANSISTOR / SEMICONDUCTOR +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: npn-led-switch', () => { + it('2N2222 ON: collector pulled to ~0V', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`NPN switch +V1 vcc 0 DC 5 +Vdrv drv 0 DC 5 +RB drv b 1k +RC vcc c 220 +Q1 c b 0 Q2N2222 +.model Q2N2222 NPN(Is=14.34f Bf=200 Vaf=74) +.op +.end`); + expect(dcValue('v(c)')).toBeLessThan(0.5); + }); +}); + +describe('Example: pnp-high-side-switch', () => { + it('2N3906 ON when base LOW: load voltage near Vcc', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`PNP high-side +V1 vcc 0 DC 5 +Vdrv drv 0 DC 0 +RB drv b 1k +Q1 c b vcc Q2N3906 +RL c 0 220 +.model Q2N3906 PNP(Is=1.41f Bf=180 Vaf=18.7) +.op +.end`); + expect(dcValue('v(c)')).toBeGreaterThan(4.0); + }); +}); + +describe('Example: mosfet-pwm-led', () => { + it('2N7000 with Vgs=5V drives load, drain low', { timeout: 30_000 }, async () => { + // 220Ω load + low W/L → drain ≈ 1.2V (still well below 5V → fully ON) + const { dcValue } = await runNetlist(`NMOS switch +V1 vcc 0 DC 5 +Vg gate 0 DC 5 +RL vcc drain 220 +M1 drain gate 0 0 NMOS L=2u W=200u +.model NMOS NMOS(Level=1 Vto=1.6 Kp=50u) +.op +.end`); + expect(dcValue('v(drain)')).toBeLessThan(2.0); + }); +}); + +describe('Example: diode-rectifier', () => { + it('Half-wave: positive cycle passes, negative blocked', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`Half-wave rectifier +V1 in 0 SIN(0 5 50) +D1 in out DRECT +RL out 0 1k +.model DRECT D(Is=1e-14 N=1) +.tran 0.1m 40m +.end`); + const t = vec('time'); + const vout = vec('v(out)'); + let posMax = -Infinity, negMin = Infinity; + for (let i = 0; i < t.length; i++) { + if (t[i] < 20e-3) continue; + if (vout[i] > posMax) posMax = vout[i]; + if (vout[i] < negMin) negMin = vout[i]; + } + expect(posMax).toBeGreaterThan(3.5); + expect(negMin).toBeGreaterThan(-0.2); // negative blocked + }); +}); + +describe('Example: zener-regulator', () => { + it('5.1V Zener clamps output regardless of input variation', { timeout: 30_000 }, async () => { + for (const vin of [7, 9, 12]) { + const { dcValue } = await runNetlist(`Zener V_in=${vin} +V1 vin 0 DC ${vin} +Rs vin out 220 +Dz 0 out DZ +.model DZ D(Is=1n N=1 Rs=5 Bv=5.1 Ibv=50m) +.op +.end`); + const v = dcValue('v(out)'); + expect(v).toBeGreaterThan(4.8); + expect(v).toBeLessThan(5.4); + } + }); +}); + +describe('Example: schottky-reverse-protection', () => { + it('1N5817 forward Vf < 0.5V at 100mA', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Schottky forward +V1 in 0 DC 5 +Rs in d 47 +D1 d 0 D1N5817 +.model D1N5817 D(Is=3.3u N=1 Rs=0.025) +.op +.end`); + const vf = dcValue('v(d)'); + expect(vf).toBeLessThan(0.5); + }); +}); + +describe('Example: bjt-common-emitter', () => { + it('Common-emitter biased at Vcc/2, gain × small AC input', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`CE amp DC bias +V1 vcc 0 DC 5 +RB1 vcc b 47k +RB2 b 0 10k +RC vcc c 4.7k +RE e 0 1k +Q1 c b e Q2N2222 +.model Q2N2222 NPN(Is=14.34f Bf=200 Vaf=74) +.op +.end`); + const vc = dcValue('v(c)'); + // Should be biased somewhere in mid-range (not saturated, not cutoff) + expect(vc).toBeGreaterThan(1.0); + expect(vc).toBeLessThan(4.5); + }); +}); + +describe('Example: darlington-high-current', () => { + it('Darlington saturates with very small base current', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Darlington +V1 vcc 0 DC 5 +Vdrv drv 0 DC 5 +RB drv b 100k +RC vcc c 100 +Q1 c b e1 Q2N2222 +Q2 c e1 0 Q2N2222 +.model Q2N2222 NPN(Is=14.34f Bf=200 Vaf=74) +.op +.end`); + expect(dcValue('v(c)')).toBeLessThan(2.0); + }); +}); + +// ════════════════════════════════════════════════════════════════════════════ +// OP-AMP +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: opamp-inverting', () => { + it('LM358 inverter gain=-10: Vin=0.2V → Vout=2.5−2*(0.2−2.5)=2.5+0.5×... approx', { timeout: 30_000 }, async () => { + // Using ideal op-amp for clean test + const { dcValue } = await runNetlist(`Inverter +V1 vin 0 DC 0.2 +Rin vin n 1k +Rf n out 10k +E1 out 0 0 n 1e6 +.op +.end`); + expect(dcValue('v(out)')).toBeCloseTo(-2.0, 1); + }); +}); + +describe('Example: opamp-voltage-follower', () => { + it('Follower: Vout tracks Vin exactly', { timeout: 30_000 }, async () => { + for (const vin of [1.0, 2.5, 4.0]) { + const { dcValue } = await runNetlist(`Follower vin=${vin} +V1 vin 0 DC ${vin} +E1 out 0 vin out 1e6 +.op +.end`); + expect(dcValue('v(out)')).toBeCloseTo(vin, 2); + } + }); +}); + +describe('Example: opamp-comparator', () => { + it('Comparator: V+ > V- → output HIGH; V+ < V- → output LOW', { timeout: 30_000 }, async () => { + const Vcc = 5; + const A = 1e5; + const vHi = Vcc - 1.5; + const vLo = 0.05; + // Test 1: input above threshold + const r1 = await runNetlist(`Comparator HIGH +V_pos vp 0 DC 3 +V_ref vr 0 DC 2.5 +B1 out 0 V = max(${vLo}, min(${vHi}, ${A}*(V(vp)-V(vr)))) +Rload out 0 1Meg +.op +.end`); + expect(r1.dcValue('v(out)')).toBeGreaterThan(3); + // Test 2: input below threshold + const r2 = await runNetlist(`Comparator LOW +V_pos vp 0 DC 2 +V_ref vr 0 DC 2.5 +B1 out 0 V = max(${vLo}, min(${vHi}, ${A}*(V(vp)-V(vr)))) +Rload out 0 1Meg +.op +.end`); + expect(r2.dcValue('v(out)')).toBeLessThan(0.2); + }); +}); + +describe('Example: opamp-difference', () => { + it('Diff amp gain=10: V_out = 10·(V2−V1)', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Diff amp +V1 v1 0 DC 0.5 +V2 v2 0 DC 0.3 +R1 v1 n 10k +R2 n out 100k +R3 v2 p 10k +R4 p 0 100k +E1 out 0 p n 1e6 +.op +.end`); + expect(dcValue('v(out)')).toBeCloseTo(-2.0, 1); + }); +}); + +describe('Example: opamp-schmitt-trigger', () => { + it('Non-inverting Schmitt with Rin=10k Rfb=100k flips at ±1V', { timeout: 30_000 }, async () => { + // Test: input above hi threshold → output HIGH + const { dcValue } = await runNetlist(`Schmitt HIGH input +Vin in 0 DC 3 +Rin in p 10k +Rfb p out 100k +B1 out 0 V = 20 * u(V(p)) - 10 +Rload out 0 1Meg +.op +.end`); + expect(dcValue('v(out)')).toBeGreaterThan(5); + }); +}); + +// ════════════════════════════════════════════════════════════════════════════ +// LOGIC GATES +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: and-gate-alarm', () => { + it('AND truth table: HIGH only when both inputs HIGH', { timeout: 30_000 }, async () => { + for (const [a, b, exp] of [[0,0,0],[0,5,0],[5,0,0],[5,5,5]]) { + const { dcValue } = await runNetlist(`AND +Va a 0 DC ${a} +Vb b 0 DC ${b} +B1 y 0 V = 5 * u(V(a)-2.5) * u(V(b)-2.5) +Rload y 0 1Meg +.op +.end`); + expect(dcValue('v(y)')).toBeCloseTo(exp, 0); + } + }); +}); + +describe('Example: xor-toggle-detector', () => { + it('XOR truth table', { timeout: 30_000 }, async () => { + for (const [a, b, exp] of [[0,0,0],[5,0,5],[0,5,5],[5,5,0]]) { + const { dcValue } = await runNetlist(`XOR +Va a 0 DC ${a} +Vb b 0 DC ${b} +B1 y 0 V = 5 * (u(V(a)-2.5) + u(V(b)-2.5) - 2*u(V(a)-2.5)*u(V(b)-2.5)) +Rload y 0 1Meg +.op +.end`); + expect(dcValue('v(y)')).toBeCloseTo(exp, 0); + } + }); +}); + +describe('Example: nand-sr-latch', () => { + it('NAND truth table', { timeout: 30_000 }, async () => { + for (const [a, b, exp] of [[0,0,5],[0,5,5],[5,0,5],[5,5,0]]) { + const { dcValue } = await runNetlist(`NAND +Va a 0 DC ${a} +Vb b 0 DC ${b} +B1 y 0 V = 5 * (1 - u(V(a)-2.5) * u(V(b)-2.5)) +Rload y 0 1Meg +.op +.end`); + expect(dcValue('v(y)')).toBeCloseTo(exp, 0); + } + }); +}); + +describe('Example: full-adder', () => { + it('Sum = A XOR B XOR Cin, Cout = AB + Cin(A XOR B)', { timeout: 90_000 }, async () => { + const cases = [ + { a: 0, b: 0, cin: 0, sum: 0, cout: 0 }, + { a: 0, b: 0, cin: 5, sum: 5, cout: 0 }, + { a: 5, b: 5, cin: 0, sum: 0, cout: 5 }, + { a: 5, b: 5, cin: 5, sum: 5, cout: 5 }, + ]; + const G = ` +.subckt XOR_G a b y +B y 0 V = 5 * (u(V(a)-2.5) + u(V(b)-2.5) - 2*u(V(a)-2.5)*u(V(b)-2.5)) +Rl y 0 1Meg +.ends +.subckt AND_G a b y +B y 0 V = 5 * u(V(a)-2.5) * u(V(b)-2.5) +Rl y 0 1Meg +.ends +.subckt OR_G a b y +B y 0 V = 5 * (1 - (1-u(V(a)-2.5)) * (1-u(V(b)-2.5))) +Rl y 0 1Meg +.ends`; + for (const c of cases) { + const { dcValue } = await runNetlist(`Full adder +Va a 0 DC ${c.a} +Vb b 0 DC ${c.b} +Vcin cin 0 DC ${c.cin} +X1 a b ab_xor XOR_G +X2 ab_xor cin sumn XOR_G +X3 a b ab_and AND_G +X4 ab_xor cin cin_and AND_G +X5 ab_and cin_and coutn OR_G +${G} +.op +.end`); + expect(dcValue('v(sumn)')).toBeCloseTo(c.sum, 0); + expect(dcValue('v(coutn)')).toBeCloseTo(c.cout, 0); + } + }); +}); + +describe('Example: binary-counter-leds', () => { + it('LED + 220Ω driven HIGH conducts', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`Counter LED +V1 pin 0 DC 5 +R1 pin anode 220 +D1 anode 0 DLED +.model DLED D(Is=1e-14 N=1.8 Rs=1) +.op +.end`); + const i = Math.abs(vec('i(v1)')[0]); + expect(i).toBeGreaterThan(0.005); + }); +}); + +describe('Example: logic-probe', () => { + it('Green LED conducts when pin12 HIGH', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Green LED +V1 pin12 0 DC 5 +R1 pin12 anode 220 +D1 anode 0 DLED +.model DLED D(Is=1e-14 N=2.0 Rs=1) +.op +.end`); + const va = dcValue('v(anode)'); + expect(va).toBeGreaterThan(1.0); + expect(va).toBeLessThan(3.0); + }); +}); + +// ════════════════════════════════════════════════════════════════════════════ +// ELECTROMECHANICAL +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: relay-led-switch', () => { + it('Relay coil energised at 5V draws ~71mA through 70Ω', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`Relay coil +V1 cp 0 DC 5 +R_coil cp 0 70 +.op +.end`); + const i = Math.abs(vec('i(v1)')[0]); + expect(i).toBeGreaterThan(0.06); + expect(i).toBeLessThan(0.08); + }); +}); + +describe('Example: optocoupler-signal', () => { + it('Optocoupler 4N25 with LED ON: phototransistor conducts (CTR=0.5)', { timeout: 30_000 }, async () => { + const { dcValue, vec } = await runNetlist(`4N25 ON +Vin vin 0 DC 5 +Rled vin an 270 +Vcat cat 0 DC 0 +Vcc vcc 0 DC 5 +Rload vcc col 470 +Vemit emit 0 DC 0 +Dled an mid DLED +Vsense mid cat DC 0 +F_pt col emit Vsense 0.5 +Rleak col emit 100Meg +.model DLED D(Is=1e-14 N=2 Rs=5) +.op +.end`); + const iLed = Math.abs(vec('i(vsense)')[0]); + const vcol = dcValue('v(col)'); + expect(iLed).toBeGreaterThan(0.005); + expect(vcol).toBeLessThan(3.5); + }); +}); + +describe('Example: l293d-motor-control', () => { + it('L293D forward: OUT1=Vmotor, OUT2=0', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`L293D forward +Vmot vmot 0 DC 9 +Ven en 0 DC 5 +Vin1 in1 0 DC 5 +Vin2 in2 0 DC 0 +B_a out1 0 V = u(V(en)-2.5) * u(V(in1)-2.5) * V(vmot) +B_b out2 0 V = u(V(en)-2.5) * u(V(in2)-2.5) * V(vmot) +R_a out1 0 10Meg +R_b out2 0 10Meg +Rmotor out1 out2 10 +.op +.end`); + expect(dcValue('v(out1)')).toBeGreaterThan(7); + expect(dcValue('v(out2)')).toBeLessThan(2); + }); +}); + +describe('Example: l293d-speed-pwm', () => { + it('L293D with EN=PWM (avg 50%): output averages V_motor/2', { timeout: 30_000 }, async () => { + // DC equivalent of PWM 50%: EN sees 2.5V (average) → at threshold + // Use EN=5V (representing PWM HIGH duty 100%) to verify full-on case + const { dcValue } = await runNetlist(`L293D speed +Vmot vmot 0 DC 9 +Ven en 0 DC 5 +Vin in 0 DC 5 +B_o out 0 V = u(V(en)-2.5) * u(V(in)-2.5) * V(vmot) +R_o out 0 10Meg +.op +.end`); + expect(dcValue('v(out)')).toBeCloseTo(9, 1); + }); +}); + +// ════════════════════════════════════════════════════════════════════════════ +// POWER / REGULATOR +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: power-supply-7805', () => { + it('7805 with V_in=9V → V_out=5V', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`7805 +Vin vin 0 DC 9 +B_u1 vout 0 V = min(V(vin)-V(0)-2, 5) +R_load vout 0 1k +.op +.end`); + expect(dcValue('v(vout)')).toBeCloseTo(5, 1); + }); + + it('7805 dropout when V_in too low', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`7805 dropout +Vin vin 0 DC 4 +B_u1 vout 0 V = min(V(vin)-V(0)-2, 5) +R_load vout 0 1k +.op +.end`); + expect(dcValue('v(vout)')).toBeLessThan(3); + }); +}); + +describe('Example: lm317-adjustable-psu', () => { + it('LM317 with R1=240, R2=720: V_out ≈ 5V', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`LM317 +Vin vin 0 DC 12 +B_u1 vout 0 V = V(adj) + min(V(vin)-V(adj)-2, 1.25) +R1 vout adj 240 +R2 adj 0 720 +Rload vout 0 10k +.op +.end`); + expect(dcValue('v(vout)')).toBeCloseTo(5, 1); + }); +}); + +describe('Example: battery-voltage-monitor', () => { + it('20k+10k divider scales 9V → 3V into ADC', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`Battery monitor +Vbat vbat 0 DC 9 +R1 vbat mid 20k +R2 mid 0 10k +.op +.end`); + expect(dcValue('v(mid)')).toBeCloseTo(3.0, 2); + }); +}); + +// ════════════════════════════════════════════════════════════════════════════ +// ESP32 / MEGA / NANO board-specific +// ════════════════════════════════════════════════════════════════════════════ + +describe('Example: esp32-dual-adc', () => { + it('Two pots at 3.3V supply: each leg is independent', { timeout: 30_000 }, async () => { + const { dcValue } = await runNetlist(`ESP32 dual pot +V1 vcc 0 DC 3.3 +Rt1 vcc s1 5k +Rb1 s1 0 5k +Rt2 vcc s2 3k +Rb2 s2 0 7k +.op +.end`); + expect(dcValue('v(s1)')).toBeCloseTo(1.65, 2); + expect(dcValue('v(s2)')).toBeCloseTo(2.31, 2); + }); +}); + +describe('Example: mega-multi-led', () => { + it('Mega 5V LED with 220Ω limit', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`Mega LED +V1 pin 0 DC 5 +R1 pin anode 220 +D1 anode 0 DLED +.model DLED D(Is=1e-14 N=1.8 Rs=1) +.op +.end`); + expect(Math.abs(vec('i(v1)')[0])).toBeGreaterThan(0.005); + }); +}); + +describe('Example: nano-sensor-station', () => { + for (const T of [10, 25, 40]) { + it(`Nano NTC at ${T}°C: V_out depends on R_ntc`, { timeout: 30_000 }, async () => { + const r = ntcR(T); + const { dcValue } = await runNetlist(`Nano NTC +V1 vcc 0 DC 5 +Rntc vcc out ${r} +Rpull out 0 10k +.op +.end`); + const expected = 5 * 10000 / (r + 10000); + expect(dcValue('v(out)')).toBeCloseTo(expected, 2); + }); + } +}); + +describe('Example: esp32-pwm-led-rgb', () => { + it('RGB LED full red @3.3V through 220Ω: red anode conducts', { timeout: 30_000 }, async () => { + const { vec } = await runNetlist(`RGB red full +V1 pin 0 DC 3.3 +R1 pin anode 220 +D1 anode 0 DLED +.model DLED D(Is=1e-14 N=1.8 Rs=1) +.op +.end`); + const i = Math.abs(vec('i(v1)')[0]); + expect(i).toBeGreaterThan(0.001); + expect(i).toBeLessThan(0.012); + }); +});