From 55b3dd29e5b7e48cdce4a63b304e41d320406615 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 18 May 2026 22:07:42 -0300 Subject: [PATCH 1/2] fix(DynamicComponent): pass componentId through to PinTracer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PinTracer` signature is `(componentId, componentPinName) => number | null` but the local `getArduinoPin` lambda only accepted one arg and used the closure-captured `id`. When `createDefaultPinResolver` passed both args (per the typed signature), JS bound the FIRST arg (the componentId) into the lambda's single `componentPinName` parameter. `traceDetailed` then looked up a pin literally named "rgb-led-1" on component "rgb-led-1", returned null, and the resolver locked itself into 'FLOATING' state — its onChange path never subscribed and the wokwi-rgb-led element's ledRed/ledGreen/ledBlue stayed at 0 forever even as the SPICE side correctly cycled through R, G, B, Y, C, M, W via analogWrite(). Same bug latent for any multi-pin component that goes through the PinResolver path (multi-pin LEDs, RGB strips, 7-seg drivers, anything that calls `getPinResolver()` for several pin names). Fix: lambda now accepts both shapes — `getArduinoPin(pinName)` (legacy single-arg used by every PartSimulationRegistry handler) AND `getArduinoPin(componentId, pinName)` (PinTracer 2-arg form used by createDefaultPinResolver / createSpiceResolvedPinResolver). Picks the right componentId in either case. Verified via the rgb-led example: ledRed/ledGreen/ledBlue now cycle 0→255→0 in sync with the SPICE node voltages on pins 9/10/11. Co-Authored-By: Claude Opus 4.7 --- frontend/src/components/DynamicComponent.tsx | 26 +++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index 19d0952f..7c6e8e3c 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -369,11 +369,29 @@ export const DynamicComponent: React.FC = ({ // Helper to find Arduino pin connected to a component pin. // Traces through electrically-transparent passive components so that a // circuit like LED-cathode → resistor → GND returns -1 (GND) instead - // of null. Delegates to the module-level `traceDetailed` (shared with - // getPinResolver). - const getArduinoPin = (componentPinName: string): number | null => { + // of null. Delegates to the module-level `traceDetailed`. + // + // Two call shapes are supported because this same function is passed + // BOTH to PartSimulationRegistry handlers (which call it as + // `getArduinoPin(componentPinName)`) AND to `createDefaultPinResolver` + // as a `PinTracer` (which calls it as `tracePin(componentId, + // componentPinName)`). When the second arg is present we treat the + // first as a componentId override; otherwise we use the closure- + // captured component id. The previous single-arg signature silently + // matched the PinTracer 2-arg call as `(componentId, undefined)` — + // traceDetailed then looked up a pin literally named "rgb-led-1" on + // component "rgb-led-1", got null, and the PinResolver reported + // FLOATING forever (the canonical "wokwi-rgb-led never lights up + // even though SPICE is driving R/G/B" symptom). + const getArduinoPin = ( + componentIdOrPin: string, + maybePinName?: string, + ): number | null => { const state = useSimulatorStore.getState(); - return traceDetailed(state, id, componentPinName, 0).arduinoPin; + const componentId = maybePinName !== undefined ? componentIdOrPin : id; + const componentPinName = + maybePinName !== undefined ? maybePinName : componentIdOrPin; + return traceDetailed(state, componentId, componentPinName, 0).arduinoPin; }; // PinResolver factory — Phase 0 of the mixed-mode simulator project From d898f122ece8f4e56a4d686f6c36eca51a2bbc60 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 18 May 2026 22:39:01 -0300 Subject: [PATCH 2/2] test(visual-led): add RGB + 7-segment leafCheck assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new harness modes that would have caught the PinTracer signature bug fixed in 55b3dd2: - `leafCheck: 'rgbLed'` — samples wokwi-rgb-led.ledRed/Green/Blue 16 times across a fade cycle and asserts each channel takes ≥2 distinct values. The buggy version stayed at {0} for every channel because the resolver locked itself to FLOATING and onChange never fired. - `leafCheck: 'sevenSegment'` — samples wokwi-7segment.values 12 times and asserts ≥4 distinct segment patterns. Counter sketches naturally hit 10+ patterns when working; ≤1 means the segment subscribers never saw an edge. Both checks are now in the default suite alongside Blink, Button, Traffic-Light, Fade. Result with current main: 6/6 pass. Co-Authored-By: Claude Opus 4.7 --- scripts/visual-led-test.mjs | 63 +++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/scripts/visual-led-test.mjs b/scripts/visual-led-test.mjs index 7987d8ad..f2e84ed8 100644 --- a/scripts/visual-led-test.mjs +++ b/scripts/visual-led-test.mjs @@ -48,7 +48,11 @@ const DEFAULT_SUITE = [ expectGradient: true, note: 'Brightness must hit 3+ distinct non-zero values (smooth fade)' }, { slug: 'rgb-led', label: 'RGB 3-PWM driven', simulateMs: 12000, - leafCheck: 'pwmActive' }, + leafCheck: 'rgbLed', + note: 'wokwi-rgb-led ledRed/Green/Blue MUST cycle — catches PinTracer signature bug' }, + { slug: 'uno-7segment', label: '7-segment counter', simulateMs: 8000, + leafCheck: 'sevenSegment', + note: 'wokwi-7segment.values MUST hit ≥4 distinct digit patterns — catches the PinResolver-floating bug for any handler that subscribes per pin' }, ]; // ── CDP plumbing ────────────────────────────────────────────────────────── @@ -181,6 +185,57 @@ async function runOne(cdp, ex) { return result; } + if (ex.leafCheck === 'rgbLed') { + // wokwi-rgb-led exposes ledRed/Green/Blue (0-255). Visual correctness + // requires each channel to take ≥2 distinct values during the cycle. + // Without that, the SPICE side may be driving the pins but the visual + // is stuck (the canonical PinTracer-signature bug). + const rgbSamples = await cdp.eval(` + (async () => { + const out = []; + for (let i = 0; i < 16; i++) { + const el = document.querySelector('wokwi-rgb-led'); + out.push({ R: el?.ledRed ?? null, G: el?.ledGreen ?? null, B: el?.ledBlue ?? null }); + await new Promise(r => setTimeout(r, 400)); + } + return out; + })() + `, { awaitPromise: true }); + if (!rgbSamples || rgbSamples[0].R === null) { + result.fail = 'no wokwi-rgb-led element found'; + return result; + } + const rs = new Set(rgbSamples.map(s => s.R)); + const gs = new Set(rgbSamples.map(s => s.G)); + const bs = new Set(rgbSamples.map(s => s.B)); + result.distinctR = rs.size; result.distinctG = gs.size; result.distinctB = bs.size; + if (rs.size < 2 || gs.size < 2 || bs.size < 2) { + result.fail = `RGB channel(s) stuck — distinct R=${rs.size} G=${gs.size} B=${bs.size} (each MUST be ≥2)`; + } + return result; + } + + if (ex.leafCheck === 'sevenSegment') { + // wokwi-7segment exposes `values` (length-8 array, segments a..g + dp). + // A working counter must hit ≥4 distinct patterns during the run. + const patterns = await cdp.eval(` + (async () => { + const out = new Set(); + for (let i = 0; i < 12; i++) { + const el = document.querySelector('wokwi-7segment'); + if (el?.values) out.add(Array.from(el.values).join(',')); + await new Promise(r => setTimeout(r, 500)); + } + return [...out]; + })() + `, { awaitPromise: true }); + result.distinctPatterns = patterns?.length ?? 0; + if (!patterns || patterns.length < 4) { + result.fail = `7-seg only ${patterns?.length ?? 0} distinct pattern(s) — display stuck or not driving segments`; + } + return result; + } + // Standard LED assertions. if (samples[0].leds.length === 0) { // No wokwi-led on canvas — only validate SPICE pin activity. @@ -271,7 +326,11 @@ async function main() { failed++; } else { let detail; - if (r.outputPinCount != null && r.maxBrightness == null) { + if (r.distinctR != null) { + detail = `RGB channels distinct R=${r.distinctR} G=${r.distinctG} B=${r.distinctB}`; + } else if (r.distinctPatterns != null) { + detail = `7-seg patterns=${r.distinctPatterns}`; + } else if (r.outputPinCount != null && r.maxBrightness == null) { detail = `no canvas LED, ${r.outputPinCount} pin(s) driven`; } else if (r.outputPinCount != null) { detail = `${r.outputPinCount} pin(s) driven, max=${r.maxBrightness} min=${r.minBrightness}`;