diff --git a/frontend/src/__tests__/board-pin-mapping.test.ts b/frontend/src/__tests__/board-pin-mapping.test.ts new file mode 100644 index 00000000..58a10268 --- /dev/null +++ b/frontend/src/__tests__/board-pin-mapping.test.ts @@ -0,0 +1,44 @@ +/** + * boardPinToNumber — the bridge between a board element's PAD NAMES and the + * GPIO numbers the simulators speak. + * + * A board whose kind has no branch here falls through to `return null`, and a + * null pin means every wire to that header silently connects to nothing: the + * part sits on the canvas, the sketch reads a dead pin, and nothing reports an + * error. That is what happened to the M5 Cardputer, whose EXT header and Grove + * Port A were unreachable even though the element drew them. + */ +import { describe, expect, it } from 'vitest'; +import { boardPinToNumber } from '../utils/boardPinMapping'; + +describe('boardPinToNumber — M5 Cardputer ADV header', () => { + // The pad names CardputerElement.ts renders on its wireable bottom row. + const GPIO_NAMES = ['3', '4', '5', '6', '8', '9', '13', '14', '15', '39', '40', '2', '1']; + + it('maps every GPIO pad to its own number', () => { + for (const name of GPIO_NAMES) { + expect(boardPinToNumber('cardputer-adv', name)).toBe(Number(name)); + } + }); + + it('leaves no pad unmapped', () => { + const unmapped = GPIO_NAMES.filter((n) => boardPinToNumber('cardputer-adv', n) === null); + expect(unmapped).toEqual([]); + }); + + it('maps the power pads to -1, not to a GPIO', () => { + expect(boardPinToNumber('cardputer-adv', '5V')).toBe(-1); + expect(boardPinToNumber('cardputer-adv', 'GND')).toBe(-1); + }); + + it('accepts G40, which the classic ESP32 range would have rejected', () => { + // The shared esp32 branch clamps at 39; the S3 has 48 GPIOs and this board + // exposes 40 on its header. + expect(boardPinToNumber('cardputer-adv', '40')).toBe(40); + }); + + it('rejects a pad that is not a pin', () => { + expect(boardPinToNumber('cardputer-adv', 'NOPE')).toBeNull(); + expect(boardPinToNumber('cardputer-adv', '99')).toBeNull(); + }); +}); diff --git a/frontend/src/utils/boardPinMapping.ts b/frontend/src/utils/boardPinMapping.ts index f6b0b027..d3497a16 100644 --- a/frontend/src/utils/boardPinMapping.ts +++ b/frontend/src/utils/boardPinMapping.ts @@ -350,6 +350,18 @@ export function boardPinToNumber(boardId: string, pinName: string): number | nul return null; } + // Overlay S3 boards whose pads are bare GPIO numbers but whose kind does not + // start with 'esp32' — the M5 Cardputer's EXT header and Grove Port A. Its + // pads go up to G40, above the classic ESP32's 39, so the shared branch below + // would reject the two highest ones even if the kind matched. + if (boardId === 'cardputer-adv') { + if (pinName.startsWith('GND') || pinName.startsWith('3V3') || pinName.startsWith('5V')) + return -1; + const num = parseInt(pinName, 10); + if (!isNaN(num) && num >= 0 && num <= 48) return num; // S3 has 48 GPIOs + return null; + } + // ESP32 / ESP32-S3 / ESP32-C3 — GPIO numbers used directly if (boardId === 'esp32' || boardId.startsWith('esp32')) { // Power / GND pins (GND, GND.1, 3V3, 3V3.1, 5V, 5V.1, etc.)