From 3ac00ae5ffd19f27340c56f94d1a7ec084dd3bda Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 20 Jul 2026 18:46:23 +0200 Subject: [PATCH] fix(trace): recognise runtime boards and same-hole junctions in pin tracing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An ESP32 clock built by the agent stayed dark while QEMU was verifiably emitting hundreds of GPIO edges per second (437/pin measured on the live websocket). Reload did not help — this was not the seating race. Two independent tracing bugs, reproduced from the real project circuit (fixture included) and each sufficient to kill the display: Boards added at runtime were invisible -------------------------------------- isBoardComponent matches static id prefixes ('arduino-uno', ...), which only covers the default board. Every board added at runtime gets a minted UUID id — the agent's add_board always does — so traceDetailed treated the board endpoint as an unknown component and resolved null, and SimulatorCanvas's direct-wire subscription path skipped it entirely. Every Uno project happened to work because they reuse the default board whose instance id IS the literal 'arduino-uno'. Both sites now consult the live boards list first, keeping isBoardComponent as the legacy-id fallback. Strip walking missed wires stacked on one hole ---------------------------------------------- The breadboard group walk continued the trace from every OTHER wired hole of the strip, excluding the arrival hole by name. But two wires may legitimately share one hole — the agent bridges strips straight into the seat hole (8 of this circuit's 9 bridges land exactly on a resistor's own hole), which is electrically identical to using a free hole of the strip. The name exclusion made those junctions dead ends. Exclusion is now by incoming WIRE id, so same-hole connections resolve; the depth bound already prevents ping-ponging between two wires of one net. With both fixes the exact saved circuit resolves every display pin to its GPIO (A..DP -> 32,33,25,26,27,14,12,13; DIG1..4 -> 15,2,4,5; COM -> GND) and the live project now shows 12:00 on the real QEMU simulation. traceDetailed is exported for the regression test, which drives the real store with the real circuit. Co-Authored-By: Claude Fable 5 --- .../esp32-breadboard-display.test.ts | 79 ++ .../fixtures/esp32-clock-circuit.json | 700 ++++++++++++++++++ frontend/src/components/DynamicComponent.tsx | 26 +- .../components/simulator/SimulatorCanvas.tsx | 8 +- 4 files changed, 805 insertions(+), 8 deletions(-) create mode 100644 frontend/src/__tests__/esp32-breadboard-display.test.ts create mode 100644 frontend/src/__tests__/fixtures/esp32-clock-circuit.json diff --git a/frontend/src/__tests__/esp32-breadboard-display.test.ts b/frontend/src/__tests__/esp32-breadboard-display.test.ts new file mode 100644 index 00000000..a6c154a4 --- /dev/null +++ b/frontend/src/__tests__/esp32-breadboard-display.test.ts @@ -0,0 +1,79 @@ +// @vitest-environment jsdom +/** + * Reproduction of the reported "ESP32 clock through a breadboard is dead" + * bug, using the exact circuit the agent built (fixture exported from the + * real project). QEMU was proven to emit every GPIO edge (437 events/pin on + * the live site) — the break is in the frontend resolution/subscription. + * + * These tests interrogate each stage separately so a failure names the + * broken stage rather than just "display dead". + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import { useSimulatorStore } from '../store/useSimulatorStore'; +import { traceDetailed } from '../components/DynamicComponent'; +import circuit from './fixtures/esp32-clock-circuit.json'; + +const BOARD = circuit.boardId; + +function loadCircuit() { + const s = useSimulatorStore.getState(); + s.setComponents(circuit.components.map((c) => ({ ...c, x: 0, y: 0 })) as never); + s.setWires(circuit.wires.map((w) => ({ + ...w, waypoints: [], color: '#000', + start: { ...w.start, x: 0, y: 0 }, end: { ...w.end, x: 0, y: 0 }, + })) as never); +} + +describe('ESP32 + breadboard-seated 7segment (real agent circuit)', () => { + beforeAll(() => { + const s = useSimulatorStore.getState(); + s.addBoard('esp32' as never, 0, 0, BOARD); + loadCircuit(); + }); + + it('traceDetailed resolves every segment pin to its GPIO through the breadboard', () => { + const state = useSimulatorStore.getState(); + const expected: Record = { + A: 32, B: 33, C: 25, D: 26, E: 27, F: 14, G: 12, DP: 13, + DIG1: 15, DIG2: 2, DIG3: 4, DIG4: 5, + }; + for (const [pin, gpio] of Object.entries(expected)) { + const r = traceDetailed(state as never, '7segment_1', pin, 0); + expect(r.arduinoPin, `7segment.${pin} should reach GPIO ${gpio}`).toBe(gpio); + } + }); + + it('COM resolves to ground (-1) through the rail', () => { + const state = useSimulatorStore.getState(); + const r = traceDetailed(state as never, '7segment_1', 'COM', 0); + expect(r.arduinoPin).toBe(-1); + }); + + it('the esp32 board exposes a simulator with a live pinManager', () => { + // DynamicComponent's attach effect reads store.simulator; the 7segment's + // attachEvents bails out entirely when simulator.pinManager is missing, + // which strands the display on the breadboard-blind onPinStateChange + // path (single-hop isBoardComponent — never true through a breadboard). + const s = useSimulatorStore.getState(); + s.setActiveBoardId(BOARD); + const after = useSimulatorStore.getState(); + expect(after.simulator, 'store.simulator for the esp32 board').toBeTruthy(); + const pm = (after.simulator as { pinManager?: unknown } | null)?.pinManager; + expect(pm, 'simulator.pinManager (attachEvents bails without it)').toBeTruthy(); + }); + + it('a GPIO edge from the bridge reaches a resolver subscribed via the pinManager', () => { + const s = useSimulatorStore.getState(); + const pm = (s.simulator as { pinManager?: { + onPinChange: (pin: number, cb: (pin: number, state: boolean) => void) => () => void; + triggerPinChange: (pin: number, state: boolean, source?: string) => void; + } } | null)?.pinManager; + expect(pm).toBeTruthy(); + let seen: boolean | null = null; + const un = pm!.onPinChange(32, (_p, st) => { seen = st; }); + // What Esp32Bridge.onPinChange does on a gpio_change frame: + pm!.triggerPinChange(32, true, 'mcu'); + un(); + expect(seen, 'edge on GPIO 32 must reach subscribers').toBe(true); + }); +}); diff --git a/frontend/src/__tests__/fixtures/esp32-clock-circuit.json b/frontend/src/__tests__/fixtures/esp32-clock-circuit.json new file mode 100644 index 00000000..79f69c83 --- /dev/null +++ b/frontend/src/__tests__/fixtures/esp32-clock-circuit.json @@ -0,0 +1,700 @@ +{ + "boardId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "components": [ + { + "id": "breadboard_1", + "metadataId": "breadboard", + "properties": {} + }, + { + "id": "7segment_1", + "metadataId": "7segment", + "properties": { + "digits": 4, + "colon": true + } + }, + { + "id": "resistor_220_1", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_2", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_3", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_4", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_5", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_6", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_7", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + }, + { + "id": "resistor_220_8", + "metadataId": "resistor-220", + "properties": { + "rotation": 90 + } + } + ], + "wires": [ + { + "id": "699bda65-cc5a-40", + "start": { + "componentId": "breadboard_1", + "pinName": "21t.d" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "35t.b" + }, + "bb": false + }, + { + "id": "ded18cb8-6804-4e", + "start": { + "componentId": "breadboard_1", + "pinName": "25t.d" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "37t.b" + }, + "bb": false + }, + { + "id": "210ac86a-dd84-42", + "start": { + "componentId": "breadboard_1", + "pinName": "23b.h" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "39t.b" + }, + "bb": false + }, + { + "id": "828e06ff-3b51-47", + "start": { + "componentId": "breadboard_1", + "pinName": "21b.h" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "41t.b" + }, + "bb": false + }, + { + "id": "020ff259-c6ad-47", + "start": { + "componentId": "breadboard_1", + "pinName": "20b.h" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "43t.b" + }, + "bb": false + }, + { + "id": "3d57ce29-1844-49", + "start": { + "componentId": "breadboard_1", + "pinName": "22t.d" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "45t.b" + }, + "bb": false + }, + { + "id": "c24471d2-8acf-42", + "start": { + "componentId": "breadboard_1", + "pinName": "24b.h" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "47t.b" + }, + "bb": false + }, + { + "id": "09dff781-b2a2-4c", + "start": { + "componentId": "breadboard_1", + "pinName": "22b.h" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "49t.b" + }, + "bb": false + }, + { + "id": "6c11a4bf-4271-4b", + "start": { + "componentId": "breadboard_1", + "pinName": "35b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "32" + }, + "bb": false + }, + { + "id": "56becf80-4217-49", + "start": { + "componentId": "breadboard_1", + "pinName": "37b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "33" + }, + "bb": false + }, + { + "id": "35148f93-0dfb-47", + "start": { + "componentId": "breadboard_1", + "pinName": "39b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "25" + }, + "bb": false + }, + { + "id": "8e68b903-6bc1-41", + "start": { + "componentId": "breadboard_1", + "pinName": "41b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "26" + }, + "bb": false + }, + { + "id": "74a77153-c2c4-4d", + "start": { + "componentId": "breadboard_1", + "pinName": "43b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "27" + }, + "bb": false + }, + { + "id": "2a0eb2fd-d583-4d", + "start": { + "componentId": "breadboard_1", + "pinName": "45b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "14" + }, + "bb": false + }, + { + "id": "b526bda1-01aa-47", + "start": { + "componentId": "breadboard_1", + "pinName": "47b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "12" + }, + "bb": false + }, + { + "id": "966f0a6e-bf67-4e", + "start": { + "componentId": "breadboard_1", + "pinName": "49b.g" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "13" + }, + "bb": false + }, + { + "id": "96d6e3a9-6ca0-44", + "start": { + "componentId": "breadboard_1", + "pinName": "20t.d" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "15" + }, + "bb": false + }, + { + "id": "7ebaeaa1-38ca-47", + "start": { + "componentId": "breadboard_1", + "pinName": "23t.d" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "2" + }, + "bb": false + }, + { + "id": "2fe1ff73-a9bd-49", + "start": { + "componentId": "breadboard_1", + "pinName": "24t.d" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "4" + }, + "bb": false + }, + { + "id": "88cad420-25f5-4a", + "start": { + "componentId": "breadboard_1", + "pinName": "25b.h" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "5" + }, + "bb": false + }, + { + "id": "26f601ef-fb7d-4d", + "start": { + "componentId": "breadboard_1", + "pinName": "26b.h" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "bn.1" + }, + "bb": false + }, + { + "id": "a86f8a48-6cca-4d", + "start": { + "componentId": "breadboard_1", + "pinName": "bn.1" + }, + "end": { + "componentId": "56b16fd2-8904-4a5e-b269-5c6071ec285c", + "pinName": "GND" + }, + "bb": false + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "A" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "21t.a" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "B" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "25t.a" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "C" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "23b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "D" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "21b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "E" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "20b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "F" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "22t.a" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "G" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "24b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "DP" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "22b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "DIG1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "20t.a" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "DIG2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "23t.a" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "DIG3" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "24t.a" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "DIG4" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "25b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "COM" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "26b.f" + }, + "bb": true + }, + { + "id": "bbwire-7segment_", + "start": { + "componentId": "7segment_1", + "pinName": "CLN" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "26t.a" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_1", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "35t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_1", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "35b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_2", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "37t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_2", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "37b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_3", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "39t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_3", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "39b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_4", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "41t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_4", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "41b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_5", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "43t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_5", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "43b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_6", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "45t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_6", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "45b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_7", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "47t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_7", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "47b.f" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_8", + "pinName": "1" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "49t.b" + }, + "bb": true + }, + { + "id": "bbwire-resistor_", + "start": { + "componentId": "resistor_220_8", + "pinName": "2" + }, + "end": { + "componentId": "breadboard_1", + "pinName": "49b.f" + }, + "bb": true + } + ] +} \ No newline at end of file diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index 5dbbc6d0..e322d07d 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -122,7 +122,7 @@ type TraceState = ReturnType; // Lifted to module scope (was inside getArduinoPin) so that getPinResolver // can call it too — the previous nested-scope version caused a runtime // ReferenceError "traceDetailed is not defined" on the simulator page. -function traceDetailed( +export function traceDetailed( state: TraceState, fromId: string, fromPin: string, @@ -146,10 +146,16 @@ function traceDetailed( w.start.componentId === fromId && w.start.pinName === fromPin ? w.start : w.end; const otherEp = selfEp === w.start ? w.end : w.start; - if (isBoardComponent(otherEp.componentId)) { - const boardKind = - state.boards.find((b) => b.id === otherEp.componentId)?.boardKind ?? - otherEp.componentId; + // A board endpoint is recognised by the LIVE boards list first. + // `isBoardComponent` matches static id prefixes ('arduino-uno', …), which + // only covers the default board — every board added at runtime (the agent + // mints UUID ids) failed the check, so tracing treated it as an unknown + // component and returned null. Symptom: an ESP32 clock whose QEMU was + // emitting hundreds of GPIO edges/second at a display that stayed dark, + // because no resolver ever attached. + const boardEp = state.boards.find((b) => b.id === otherEp.componentId); + if (boardEp || isBoardComponent(otherEp.componentId)) { + const boardKind = boardEp?.boardKind ?? otherEp.componentId; const pin = boardPinToNumber(boardKind, otherEp.pinName); if (pin !== null) return { arduinoPin: pin, crossedActiveDevice: activeSeen }; } else { @@ -176,14 +182,22 @@ function traceDetailed( // Breadboards join N holes per internal group (5-hole strip / power // rail), which the 2-terminal PASSIVE_PIN_PAIRS map can't express. // Continue the trace from every OTHER wired hole in the same group. + // + // Exclusion is by INCOMING WIRE, not by hole name: two wires may + // legitimately share one hole (a seated pin plus a jumper landing in + // that same hole — the agent bridges strips straight into the seat + // hole). Excluding the arrival hole made those stacked connections + // invisible: an ESP32 clock with QEMU firing hundreds of GPIO edges + // per second sat dark because every segment's bridge landed on its + // resistor's own seat hole and the trace dead-ended there. const bbGroup = comp && breadboardGroupKey(comp.metadataId, otherEp.pinName); if (bbGroup && comp) { const groupPins = new Set(); for (const gw of state.wires) { + if (gw.id === w.id) continue; // never bounce back on the same wire for (const ep of [gw.start, gw.end]) { if ( ep.componentId === comp.id && - ep.pinName !== otherEp.pinName && breadboardGroupKey(comp.metadataId, ep.pinName) === bbGroup ) { groupPins.add(ep.pinName); diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index da789076..04b43109 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -1153,11 +1153,15 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { const selfEndpoint = isStartSelf ? wire.start : wire.end; const otherEndpoint = isStartSelf ? wire.end : wire.start; - if (isBoardComponent(otherEndpoint.componentId)) { + // Recognise board endpoints from the LIVE boards list first — + // isBoardComponent only matches static id prefixes, so every + // runtime-added board (agent-minted UUID ids) failed it and its + // directly-wired components were never subscribed. + const boardInstance = boards.find((b) => b.id === otherEndpoint.componentId); + if (boardInstance || isBoardComponent(otherEndpoint.componentId)) { // Use the board's actual boardKind (not just its instance ID) so that // a board whose ID is 'arduino-uno' but whose kind is 'esp32' gets the // correct GPIO mapping ('GPIO4' → 4, not null). - const boardInstance = boards.find((b) => b.id === otherEndpoint.componentId); const lookupKey = boardInstance ? boardInstance.boardKind : otherEndpoint.componentId; const pin = boardPinToNumber(lookupKey, otherEndpoint.pinName); if (pin !== null && pin >= 0) {