From bc31ee76c107c7dd90cde7b0d246268fd09499fb Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 16 Mar 2026 10:31:02 -0300 Subject: [PATCH] feat: enhance oscilloscope and simulator functionality - Added `onPinStateChange` method to `ChipParts` for handling pin state changes in 7-segment displays. - Updated `useOscilloscopeStore` to allow independent monitoring of multiple boards by adding `boardId` to channels and modifying `addChannel` method. - Modified `getOscilloscopeCallback` in `useSimulatorStore` to filter channels based on `boardId` and pin number. - Adjusted component and board position calculations in `useSimulatorStore` to account for wrapper offsets. - Updated submodule references for `qemu-lcgamboa`, `rp2040js`, and `wokwi-elements` to indicate dirty states. --- .../components-wokwi/Esp32Element.ts | 40 +-- .../src/components/simulator/Oscilloscope.tsx | 297 ++++++++++++------ .../components/simulator/SimulatorCanvas.tsx | 4 +- frontend/src/data/examples.ts | 120 +++++-- frontend/src/simulation/parts/ChipParts.ts | 5 + frontend/src/store/useOscilloscopeStore.ts | 24 +- frontend/src/store/useSimulatorStore.ts | 26 +- 7 files changed, 344 insertions(+), 172 deletions(-) diff --git a/frontend/src/components/components-wokwi/Esp32Element.ts b/frontend/src/components/components-wokwi/Esp32Element.ts index 2b1e8a3..0d9eb5e 100644 --- a/frontend/src/components/components-wokwi/Esp32Element.ts +++ b/frontend/src/components/components-wokwi/Esp32Element.ts @@ -29,33 +29,33 @@ const PINS_ESP32 = [ { name: 'EN', x: 6, y: 29 }, { name: 'VN', x: 6, y: 42 }, { name: 'VP', x: 6, y: 54 }, - { name: 'D34', x: 6, y: 67 }, - { name: 'D35', x: 6, y: 80 }, - { name: 'D32', x: 6, y: 93 }, - { name: 'D33', x: 6, y: 105 }, - { name: 'D25', x: 6, y: 118 }, - { name: 'D26', x: 6, y: 131 }, - { name: 'D27', x: 6, y: 143 }, - { name: 'D14', x: 6, y: 156 }, - { name: 'D12', x: 6, y: 169 }, - { name: 'D13', x: 6, y: 181 }, + { name: '34', x: 6, y: 67 }, + { name: '35', x: 6, y: 80 }, + { name: '32', x: 6, y: 93 }, + { name: '33', x: 6, y: 105 }, + { name: '25', x: 6, y: 118 }, + { name: '26', x: 6, y: 131 }, + { name: '27', x: 6, y: 143 }, + { name: '14', x: 6, y: 156 }, + { name: '12', x: 6, y: 169 }, + { name: '13', x: 6, y: 181 }, { name: 'GND', x: 6, y: 194 }, { name: 'VIN', x: 6, y: 207 }, { name: '3V3', x: 134, y: 207 }, - { name: 'GND', x: 134, y: 194 }, - { name: 'D15', x: 134, y: 181 }, - { name: 'D2', x: 134, y: 169 }, - { name: 'D4', x: 134, y: 156 }, + { name: 'GND2', x: 134, y: 194 }, + { name: '15', x: 134, y: 181 }, + { name: '2', x: 134, y: 169 }, + { name: '4', x: 134, y: 156 }, { name: 'RX2', x: 134, y: 143 }, { name: 'TX2', x: 134, y: 131 }, - { name: 'D5', x: 134, y: 118 }, - { name: 'D18', x: 134, y: 105 }, - { name: 'D19', x: 134, y: 93 }, - { name: 'D21', x: 134, y: 80 }, + { name: '5', x: 134, y: 118 }, + { name: '18', x: 134, y: 105 }, + { name: '19', x: 134, y: 93 }, + { name: '21', x: 134, y: 80 }, { name: 'RX0', x: 134, y: 67 }, { name: 'TX0', x: 134, y: 54 }, - { name: 'D22', x: 134, y: 42 }, - { name: 'D23', x: 134, y: 29 }, + { name: '22', x: 134, y: 42 }, + { name: '23', x: 134, y: 29 }, ]; // ESP32-S3 DevKitC-1: 25.527 mm × 70.057 mm → 128 × 350 px diff --git a/frontend/src/components/simulator/Oscilloscope.tsx b/frontend/src/components/simulator/Oscilloscope.tsx index 9c6bbdd..7a82685 100644 --- a/frontend/src/components/simulator/Oscilloscope.tsx +++ b/frontend/src/components/simulator/Oscilloscope.tsx @@ -1,11 +1,11 @@ /** * Oscilloscope / Logic Analyzer panel. * - * Captures digital pin HIGH/LOW transitions (with timestamps from the CPU - * cycle counter) and renders them as step waveforms on a . + * Supports multiple boards: each channel is tied to a specific (boardId, pin) + * pair, so D13 on board A and D13 on board B are tracked independently. * * Usage: - * - Click "+ Add Channel" to pick pins to monitor. + * - Click "+ Add Channel" → choose a board → choose a pin. * - Adjust Time/div to zoom in or out. * - Click Run / Pause to freeze the display without stopping the simulation. * - Click Clear to wipe all captured samples. @@ -18,8 +18,11 @@ import React, { useCallback, useLayoutEffect, } from 'react'; +import ReactDOM from 'react-dom'; import { useOscilloscopeStore, type OscChannel, type OscSample } from '../../store/useOscilloscopeStore'; import { useSimulatorStore } from '../../store/useSimulatorStore'; +import { BOARD_KIND_LABELS } from '../../types/board'; +import type { BoardKind } from '../../types/board'; import './Oscilloscope.css'; // Horizontal divisions shown at once @@ -37,39 +40,47 @@ const TIME_DIV_OPTIONS: { label: string; ms: number }[] = [ { label: '500 ms', ms: 500 }, ]; -/** Possible digital pins to monitor (Uno/Nano layout) */ -const AVAILABLE_PINS = [ - { pin: 0, label: 'D0' }, - { pin: 1, label: 'D1' }, - { pin: 2, label: 'D2' }, - { pin: 3, label: 'D3' }, - { pin: 4, label: 'D4' }, - { pin: 5, label: 'D5' }, - { pin: 6, label: 'D6' }, - { pin: 7, label: 'D7' }, - { pin: 8, label: 'D8' }, - { pin: 9, label: 'D9' }, - { pin: 10, label: 'D10' }, - { pin: 11, label: 'D11' }, - { pin: 12, label: 'D12' }, - { pin: 13, label: 'D13' }, - { pin: 14, label: 'A0' }, - { pin: 15, label: 'A1' }, - { pin: 16, label: 'A2' }, - { pin: 17, label: 'A3' }, - { pin: 18, label: 'A4' }, - { pin: 19, label: 'A5' }, -]; +/** Return the list of monitorable pins for a given board kind */ +function getPinsForBoardKind(boardKind: BoardKind): { pin: number; label: string }[] { + switch (boardKind) { + case 'arduino-mega': + return [ + ...Array.from({ length: 54 }, (_, i) => ({ pin: i, label: `D${i}` })), + ...Array.from({ length: 16 }, (_, i) => ({ pin: 54 + i, label: `A${i}` })), + ]; + case 'attiny85': + return Array.from({ length: 6 }, (_, i) => ({ pin: i, label: `D${i}` })); + case 'raspberry-pi-pico': + case 'pi-pico-w': + return Array.from({ length: 29 }, (_, i) => ({ pin: i, label: `GP${i}` })); + case 'esp32': + case 'esp32-devkit-c-v4': + case 'esp32-cam': + case 'wemos-lolin32-lite': + return Array.from({ length: 40 }, (_, i) => ({ pin: i, label: `GPIO${i}` })); + case 'esp32-s3': + case 'xiao-esp32-s3': + case 'arduino-nano-esp32': + return Array.from({ length: 45 }, (_, i) => ({ pin: i, label: `GPIO${i}` })); + case 'esp32-c3': + case 'xiao-esp32-c3': + case 'aitewinrobot-esp32c3-supermini': + return Array.from({ length: 22 }, (_, i) => ({ pin: i, label: `GPIO${i}` })); + case 'riscv-generic': + return Array.from({ length: 20 }, (_, i) => ({ pin: i, label: `D${i}` })); + case 'raspberry-pi-3': + return Array.from({ length: 28 }, (_, i) => ({ pin: i, label: `GPIO${i}` })); + default: + // arduino-uno, arduino-nano + return [ + ...Array.from({ length: 14 }, (_, i) => ({ pin: i, label: `D${i}` })), + ...Array.from({ length: 6 }, (_, i) => ({ pin: 14 + i, label: `A${i}` })), + ]; + } +} // ── Canvas rendering helpers ──────────────────────────────────────────────── -/** - * Draw a single channel's waveform onto `canvas`. - * @param samples The sample ring-buffer for this channel. - * @param color Stroke color (CSS string). - * @param windowEndMs Right edge of the time window. - * @param windowMs Total time window width (NUM_DIVS * timeDivMs). - */ function drawWaveform( canvas: HTMLCanvasElement, samples: OscSample[], @@ -105,10 +116,7 @@ function drawWaveform( if (samples.length === 0) return; const windowStartMs = windowEndMs - windowMs; - - // Convert timeMs → canvas x pixel const toX = (t: number) => ((t - windowStartMs) / windowMs) * width; - const HIGH_Y = Math.round(height * 0.15); const LOW_Y = Math.round(height * 0.85); @@ -116,7 +124,6 @@ function drawWaveform( ctx.lineWidth = 1.5; ctx.beginPath(); - // Find the last sample before the window to establish the initial state let initState = false; for (let i = samples.length - 1; i >= 0; i--) { if (samples[i].timeMs <= windowStartMs) { @@ -134,27 +141,20 @@ function drawWaveform( const x = Math.max(0, Math.min(width, toX(s.timeMs))); const nextY = s.state ? HIGH_Y : LOW_Y; - - // Vertical step ctx.lineTo(x, currentY); ctx.lineTo(x, nextY); currentY = nextY; } - // Extend to the right edge ctx.lineTo(width, currentY); ctx.stroke(); - // HIGH / LOW labels at the right margin ctx.fillStyle = color; ctx.font = '9px monospace'; ctx.fillText('H', width - 12, HIGH_Y + 3); ctx.fillText('L', width - 12, LOW_Y + 3); } -/** - * Draw the time-axis ruler below all the channels. - */ function drawRuler( canvas: HTMLCanvasElement, windowEndMs: number, @@ -182,7 +182,6 @@ function drawRuler( ctx.lineTo(x, 5); ctx.stroke(); - // Format the label const absMs = Math.abs(timeAtDiv); const label = absMs >= 1000 ? `${(timeAtDiv / 1000).toFixed(1)}s` @@ -194,7 +193,7 @@ function drawRuler( } } -// ── Channel canvas hook ───────────────────────────────────────────────────── +// ── Channel canvas ────────────────────────────────────────────────────────── interface ChannelCanvasProps { channel: OscChannel; @@ -204,15 +203,11 @@ interface ChannelCanvasProps { } const ChannelCanvas: React.FC = ({ - channel, - samples, - windowEndMs, - windowMs, + channel, samples, windowEndMs, windowMs, }) => { const canvasRef = useRef(null); const wrapRef = useRef(null); - // Re-draw whenever data or sizing changes useLayoutEffect(() => { const canvas = canvasRef.current; const wrap = wrapRef.current; @@ -221,13 +216,11 @@ const ChannelCanvas: React.FC = ({ const { width, height } = wrap.getBoundingClientRect(); if (width === 0 || height === 0) return; - canvas.width = Math.floor(width) * window.devicePixelRatio; + canvas.width = Math.floor(width) * window.devicePixelRatio; canvas.height = Math.floor(height) * window.devicePixelRatio; const ctx = canvas.getContext('2d'); if (ctx) ctx.scale(window.devicePixelRatio, window.devicePixelRatio); drawWaveform(canvas, samples, channel.color, windowEndMs, windowMs); - // Intentionally exclude canvasRef/wrapRef (stable refs) and the - // module-level drawWaveform function from the dependency array. // eslint-disable-next-line react-hooks/exhaustive-deps }, [samples, channel.color, windowEndMs, windowMs]); @@ -258,12 +251,11 @@ const RulerCanvas: React.FC = ({ windowEndMs, windowMs, timeDi const { width, height } = wrap.getBoundingClientRect(); if (width === 0 || height === 0) return; - canvas.width = Math.floor(width) * window.devicePixelRatio; + canvas.width = Math.floor(width) * window.devicePixelRatio; canvas.height = Math.floor(height) * window.devicePixelRatio; const ctx = canvas.getContext('2d'); if (ctx) ctx.scale(window.devicePixelRatio, window.devicePixelRatio); drawRuler(canvas, windowEndMs, windowMs, timeDivMs); - // Intentionally exclude stable canvasRef/wrapRef and module-level drawRuler. // eslint-disable-next-line react-hooks/exhaustive-deps }, [windowEndMs, windowMs, timeDivMs]); @@ -274,6 +266,93 @@ const RulerCanvas: React.FC = ({ windowEndMs, windowMs, timeDi ); }; +// ── Channel picker (two-step: board → pin) ─────────────────────────────────── + +interface ChannelPickerProps { + onAdd: (boardId: string, pin: number, pinLabel: string) => void; + activeChannels: OscChannel[]; + onClose: () => void; + anchorRect: DOMRect; + dropdownRef: React.RefObject; +} + +const ChannelPicker: React.FC = ({ + onAdd, activeChannels, onClose, anchorRect, dropdownRef, +}) => { + const boards = useSimulatorStore((s) => s.boards); + const activeBoardId = useSimulatorStore((s) => s.activeBoardId); + const [selectedBoardId, setSelectedBoardId] = useState( + activeBoardId ?? boards[0]?.id ?? '', + ); + + const selectedBoard = boards.find((b) => b.id === selectedBoardId) ?? boards[0]; + const pins = selectedBoard ? getPinsForBoardKind(selectedBoard.boardKind) : []; + + const activePinsForBoard = new Set( + activeChannels.filter((c) => c.boardId === selectedBoardId).map((c) => c.pin), + ); + + // Open upward from the anchor button, fixed in the viewport + const style: React.CSSProperties = { + position: 'fixed', + left: anchorRect.left, + bottom: window.innerHeight - anchorRect.top + 4, + zIndex: 9999, + }; + + return ReactDOM.createPortal( +
+ {/* Board tabs */} +
+ {boards.map((b) => ( + + ))} +
+ + {/* Board label */} + {selectedBoard && ( +
+ {BOARD_KIND_LABELS[selectedBoard.boardKind]} +
+ )} + + {/* Pin grid */} +
+ {pins.map(({ pin, label }) => { + const added = activePinsForBoard.has(pin); + return ( + + ); + })} +
+
, + document.body, + ); +}; + // ── Main component ───────────────────────────────────────────────────────── export const Oscilloscope: React.FC = () => { @@ -289,34 +368,47 @@ export const Oscilloscope: React.FC = () => { clearSamples, } = useOscilloscopeStore(); - const simRunning = useSimulatorStore((s) => s.running); + // Any board running → oscilloscope can capture + const anyRunning = useSimulatorStore((s) => s.boards.some((b) => b.running)); const [showPicker, setShowPicker] = useState(false); - const pickerRef = useRef(null); + const [pickerAnchor, setPickerAnchor] = useState(null); + const addBtnRef = useRef(null); + const dropdownRef = useRef(null); - // Close picker when clicking outside + const handleTogglePicker = () => { + if (showPicker) { + setShowPicker(false); + setPickerAnchor(null); + } else { + const rect = addBtnRef.current?.getBoundingClientRect() ?? null; + setPickerAnchor(rect); + setShowPicker(true); + } + }; + + // Close picker on outside click (checks both the button and the portal dropdown) useEffect(() => { if (!showPicker) return; const handler = (e: MouseEvent) => { - if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) { + const target = e.target as Node; + const inBtn = addBtnRef.current?.contains(target); + const inDropdown = dropdownRef.current?.contains(target); + if (!inBtn && !inDropdown) { setShowPicker(false); + setPickerAnchor(null); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [showPicker]); - // ── Compute the display window ────────────────────────────────────────── - // - // We show the last (NUM_DIVS * timeDivMs) ms of captured data. - // windowEndMs = latest sample time across all channels (or 0 if none). - + // ── Display window ────────────────────────────────────────────────────── const [, forceRedraw] = useState(0); - - // Poll at 60 fps while simulation + capture are running const rafRef = useRef(null); + useEffect(() => { - if (simRunning && capturing) { + if (anyRunning && capturing) { const tick = () => { forceRedraw((n) => n + 1); rafRef.current = requestAnimationFrame(tick); @@ -326,11 +418,10 @@ export const Oscilloscope: React.FC = () => { if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); }; } - }, [simRunning, capturing]); + }, [anyRunning, capturing]); const windowMs = NUM_DIVS * timeDivMs; - // Find the latest sample time to anchor the right edge of the window let windowEndMs = 0; for (const ch of channels) { const buf = samples[ch.id] ?? []; @@ -338,15 +429,23 @@ export const Oscilloscope: React.FC = () => { windowEndMs = Math.max(windowEndMs, buf[buf.length - 1].timeMs); } } - // Always advance at least one window ahead so the display isn't stale windowEndMs = Math.max(windowEndMs, windowMs); - const handleAddChannel = useCallback((pin: number) => { - addChannel(pin); - setShowPicker(false); + const handleAddChannel = useCallback((boardId: string, pin: number, pinLabel: string) => { + addChannel(boardId, pin, pinLabel); }, [addChannel]); - const activePins = new Set(channels.map((c) => c.pin)); + // Short display name for a board id — strip leading "arduino-", "raspberry-pi-", etc. + const boardShortName = (boardId: string) => { + const parts = boardId.split('-'); + // If numeric suffix like "arduino-uno-2", keep the suffix + const last = parts[parts.length - 1]; + const isNum = /^\d+$/.test(last); + if (isNum && parts.length >= 2) { + return `${parts[parts.length - 2]}-${last}`; + } + return last; + }; return (
@@ -354,31 +453,25 @@ export const Oscilloscope: React.FC = () => {
Oscilloscope - {/* Add Channel button + dropdown */} -
- + {/* Add Channel button + portal picker */} + - {showPicker && ( -
- {AVAILABLE_PINS.map(({ pin, label }) => ( - - ))} -
- )} -
+ {showPicker && pickerAnchor && ( + { setShowPicker(false); setPickerAnchor(null); }} + anchorRect={pickerAnchor} + dropdownRef={dropdownRef} + /> + )} {/* Time / div */} Time/div: @@ -422,8 +515,10 @@ export const Oscilloscope: React.FC = () => {
{channels.map((ch) => (
- {/* Label + remove button */}
+ + {boardShortName(ch.boardId)} + {ch.label} @@ -436,7 +531,6 @@ export const Oscilloscope: React.FC = () => {
- {/* Waveform canvas */} { ))}
- {/* Time ruler */} { subscribeComponentToPin(component, component.properties.pin as number, 'A'); } else { // 2. Subscribe by finding wires connected to arduino - const connectedWires = useSimulatorStore.getState().wires.filter( + const connectedWires = wires.filter( w => w.start.componentId === component.id || w.end.componentId === component.id ); @@ -502,7 +502,7 @@ export const SimulatorCanvas = () => { return () => { unsubscribers.forEach(unsub => unsub()); }; - }, [components, pinManager, updateComponentState]); + }, [components, wires, boards, pinManager, updateComponentState]); // Handle keyboard delete useEffect(() => { diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 4811ebb..391ac0f 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -170,6 +170,9 @@ void loop() { end: { componentId: 'led-green', pinName: 'A' }, color: '#00ff00', }, + { id: 'wire-red-gnd', start: { componentId: 'led-red', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-yellow-gnd', start: { componentId: 'led-yellow', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-green-gnd', start: { componentId: 'led-green', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -234,6 +237,8 @@ void loop() { end: { componentId: 'led-1', pinName: 'A' }, color: '#ff0000', }, + { id: 'wire-led-gnd', start: { componentId: 'led-1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-button-gnd', start: { componentId: 'button-1', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -288,6 +293,7 @@ void loop() { end: { componentId: 'led-1', pinName: 'A' }, color: '#0000ff', }, + { id: 'wire-led-gnd', start: { componentId: 'led-1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -411,6 +417,7 @@ void loop() { end: { componentId: 'rgb-led-1', pinName: 'B' }, color: '#0000ff', }, + { id: 'wire-rgb-gnd', start: { componentId: 'rgb-led-1', pinName: 'COM' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -614,6 +621,14 @@ void loop() { end: { componentId: 'button-yellow', pinName: '1.l' }, color: '#00aaff', }, + { id: 'wire-led-red-gnd', start: { componentId: 'led-red', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-led-green-gnd', start: { componentId: 'led-green', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-led-blue-gnd', start: { componentId: 'led-blue', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-led-yellow-gnd', start: { componentId: 'led-yellow', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-btn-red-gnd', start: { componentId: 'button-red', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-btn-green-gnd', start: { componentId: 'button-green', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-btn-blue-gnd', start: { componentId: 'button-blue', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'wire-btn-yellow-gnd', start: { componentId: 'button-yellow', pinName: '2.l' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -911,6 +926,7 @@ void loop() { ], wires: [ { id: 'w-led', start: { componentId: 'arduino-uno', pinName: '13' }, end: { componentId: 'led-1', pinName: 'A' }, color: '#00cc00' }, + { id: 'w-led-gnd', start: { componentId: 'led-1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -1411,6 +1427,7 @@ void loop() { wires: [ { id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-blink', pinName: 'A' }, color: '#00cc00' }, { id: 'w2', start: { componentId: 'led-blink', pinName: 'C' }, end: { componentId: 'r1', pinName: '1' }, color: '#999999' }, + { id: 'w3', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1456,6 +1473,7 @@ void loop() { ], wires: [ { id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'TX' }, end: { componentId: 'led-rx', pinName: 'A' }, color: '#ff8800' }, + { id: 'w2', start: { componentId: 'led-rx', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1510,6 +1528,7 @@ void loop() { wires: [ { id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-status', pinName: 'A' }, color: '#00cc00' }, { id: 'w2', start: { componentId: 'led-status', pinName: 'C' }, end: { componentId: 'r1', pinName: '1' }, color: '#999999' }, + { id: 'w3', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1575,6 +1594,8 @@ void loop() { wires: [ { id: 'w1', start: { componentId: 'nano-rp2040', pinName: 'D12' }, end: { componentId: 'led-scan', pinName: 'A' }, color: '#4488ff' }, { id: 'w2', start: { componentId: 'nano-rp2040', pinName: 'D10' }, end: { componentId: 'led-found', pinName: 'A' }, color: '#00cc00' }, + { id: 'w3', start: { componentId: 'led-scan', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w4', start: { componentId: 'led-found', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1645,6 +1666,8 @@ void loop() { wires: [ { id: 'w-sda', start: { componentId: 'nano-rp2040', pinName: 'D12' }, end: { componentId: 'led-i2c', pinName: 'A' }, color: '#4488ff' }, { id: 'w-scl', start: { componentId: 'nano-rp2040', pinName: 'D10' }, end: { componentId: 'led-rtc', pinName: 'A' }, color: '#ffaa00' }, + { id: 'w-sda-gnd', start: { componentId: 'led-i2c', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-scl-gnd', start: { componentId: 'led-rtc', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1733,6 +1756,8 @@ void loop() { wires: [ { id: 'w-sda', start: { componentId: 'nano-rp2040', pinName: 'D12' }, end: { componentId: 'led-write', pinName: 'A' }, color: '#ff4444' }, { id: 'w-scl', start: { componentId: 'nano-rp2040', pinName: 'D10' }, end: { componentId: 'led-read', pinName: 'A' }, color: '#00cc00' }, + { id: 'w-write-gnd', start: { componentId: 'led-write', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-read-gnd', start: { componentId: 'led-read', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1794,6 +1819,9 @@ void loop() { { id: 'w-mosi', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'led-mosi', pinName: 'A' }, color: '#ff4444' }, { id: 'w-miso', start: { componentId: 'nano-rp2040', pinName: 'D4' }, end: { componentId: 'led-miso', pinName: 'A' }, color: '#00cc00' }, { id: 'w-sck', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'led-sck', pinName: 'A' }, color: '#ffaa00' }, + { id: 'w-mosi-gnd', start: { componentId: 'led-mosi', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-miso-gnd', start: { componentId: 'led-miso', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-sck-gnd', start: { componentId: 'led-sck', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1846,6 +1874,11 @@ void loop() { { id: 'w-a0', start: { componentId: 'nano-rp2040', pinName: 'A0' }, end: { componentId: 'pot-a0', pinName: 'SIG' }, color: '#4488ff' }, { id: 'w-a1', start: { componentId: 'nano-rp2040', pinName: 'A1' }, end: { componentId: 'pot-a1', pinName: 'SIG' }, color: '#44cc44' }, { id: 'w-temp', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-temp', pinName: 'A' }, color: '#ff4444' }, + { id: 'w-pot-a0-vcc', start: { componentId: 'nano-rp2040', pinName: '3V3' }, end: { componentId: 'pot-a0', pinName: 'VCC' }, color: '#ff0000' }, + { id: 'w-pot-a0-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pot-a0', pinName: 'GND' }, color: '#000000' }, + { id: 'w-pot-a1-vcc', start: { componentId: 'nano-rp2040', pinName: '3V3' }, end: { componentId: 'pot-a1', pinName: 'VCC' }, color: '#ff0000' }, + { id: 'w-pot-a1-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pot-a1', pinName: 'GND' }, color: '#000000' }, + { id: 'w-temp-gnd', start: { componentId: 'led-temp', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, @@ -1976,6 +2009,11 @@ void loop() { { id: 'w-spi', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'led-spi', pinName: 'A' }, color: '#ffaa00' }, { id: 'w-adc', start: { componentId: 'nano-rp2040', pinName: 'A0' }, end: { componentId: 'pot-adc', pinName: 'SIG' }, color: '#cc44cc' }, { id: 'w-gpio', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'led-gpio', pinName: 'A' }, color: '#00cc00' }, + { id: 'w-i2c-gnd', start: { componentId: 'led-i2c', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-spi-gnd', start: { componentId: 'led-spi', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-gpio-gnd', start: { componentId: 'led-gpio', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'w-pot-adc-vcc', start: { componentId: 'nano-rp2040', pinName: '3V3' }, end: { componentId: 'pot-adc', pinName: 'VCC' }, color: '#ff0000' }, + { id: 'w-pot-adc-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pot-adc', pinName: 'GND' }, color: '#000000' }, ], }, // ─── ESP32 Examples ─────────────────────────────────────────────────────── @@ -2015,10 +2053,10 @@ void loop() { { type: 'wokwi-led', id: 'led-ext', x: 460, y: 190, properties: { color: 'red' } }, ], wires: [ - // GPIO4 → LED anode (direct — subscription system needs board→component wire) - { id: 'w-gpio4-led', start: { componentId: 'arduino-uno', pinName: 'GPIO4' }, end: { componentId: 'led-ext', pinName: 'A' }, color: '#e74c3c' }, + // GPIO4 → LED anode + { id: 'w-gpio4-led', start: { componentId: 'esp32', pinName: '4' }, end: { componentId: 'led-ext', pinName: 'A' }, color: '#e74c3c' }, // LED cathode → GND - { id: 'w-gnd', start: { componentId: 'led-ext', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#2c3e50' }, + { id: 'w-gnd', start: { componentId: 'led-ext', pinName: 'C' }, end: { componentId: 'esp32', pinName: 'GND' }, color: '#2c3e50' }, ], }, { @@ -2294,6 +2332,8 @@ void loop() { wires: [ { id: 'w-btn', start: { componentId: 'arduino-uno', pinName: '2' }, end: { componentId: 'btn1', pinName: '1a' }, color: '#00aaff' }, { id: 'w-led', start: { componentId: 'arduino-uno', pinName: '13' }, end: { componentId: 'led1', pinName: 'A' }, color: '#ff4444' }, + { id: 'w-led-gnd', start: { componentId: 'led1', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w-btn-gnd', start: { componentId: 'btn1', pinName: '1b' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2333,6 +2373,7 @@ void loop() { wires: [ { id: 'w-fade', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'led-fade', pinName: 'A' }, color: '#2244ff' }, { id: 'w-fade-r', start: { componentId: 'led-fade', pinName: 'C' }, end: { componentId: 'r-fade', pinName: '1' }, color: '#888888' }, + { id: 'w-fade-gnd', start: { componentId: 'r-fade', pinName: '2' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, @@ -2452,6 +2493,14 @@ void loop() { { id: 'w7', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'led7', pinName: 'A' }, color: '#aa44ff' }, { id: 'w8', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'led8', pinName: 'A' }, color: '#ffffff' }, { id: 'w9', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'led9', pinName: 'A' }, color: '#ff2222' }, + { id: 'w2-gnd', start: { componentId: 'led2', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w3-gnd', start: { componentId: 'led3', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w4-gnd', start: { componentId: 'led4', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w5-gnd', start: { componentId: 'led5', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w6-gnd', start: { componentId: 'led6', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w7-gnd', start: { componentId: 'led7', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w8-gnd', start: { componentId: 'led8', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'w9-gnd', start: { componentId: 'led9', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2517,6 +2566,14 @@ void loop() { { id: 'mw7', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'mled7', pinName: 'A' }, color: '#aa44ff' }, { id: 'mw8', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'mled8', pinName: 'A' }, color: '#ffffff' }, { id: 'mw9', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'mled9', pinName: 'A' }, color: '#ff2222' }, + { id: 'mw2-gnd', start: { componentId: 'mled2', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw3-gnd', start: { componentId: 'mled3', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw4-gnd', start: { componentId: 'mled4', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw5-gnd', start: { componentId: 'mled5', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw6-gnd', start: { componentId: 'mled6', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw7-gnd', start: { componentId: 'mled7', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw8-gnd', start: { componentId: 'mled8', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, + { id: 'mw9-gnd', start: { componentId: 'mled9', pinName: 'C' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, @@ -2555,6 +2612,7 @@ void loop() { wires: [ { id: 'c3w1', start: { componentId: 'esp32-c3', pinName: '8' }, end: { componentId: 'c3-led1', pinName: 'A' }, color: '#22cc22' }, { id: 'c3w2', start: { componentId: 'c3-led1', pinName: 'C' }, end: { componentId: 'c3-r1', pinName: '1' }, color: '#888888' }, + { id: 'c3w3', start: { componentId: 'c3-r1', pinName: '2' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2636,6 +2694,7 @@ void loop() { { id: 'c3-rw1', start: { componentId: 'esp32-c3', pinName: '6' }, end: { componentId: 'c3-rgb1', pinName: 'R' }, color: '#ff2222' }, { id: 'c3-rw2', start: { componentId: 'esp32-c3', pinName: '7' }, end: { componentId: 'c3-rgb1', pinName: 'G' }, color: '#22cc22' }, { id: 'c3-rw3', start: { componentId: 'esp32-c3', pinName: '8' }, end: { componentId: 'c3-rgb1', pinName: 'B' }, color: '#2244ff' }, + { id: 'c3-rw4', start: { componentId: 'c3-rgb1', pinName: 'COM' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2673,6 +2732,8 @@ void loop() { wires: [ { id: 'c3-bw1', start: { componentId: 'esp32-c3', pinName: '9' }, end: { componentId: 'c3-btn1', pinName: '1a' }, color: '#00aaff' }, { id: 'c3-bw2', start: { componentId: 'esp32-c3', pinName: '8' }, end: { componentId: 'c3-led-btn', pinName: 'A' }, color: '#2244ff' }, + { id: 'c3-bw3', start: { componentId: 'c3-led-btn', pinName: 'C' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' }, + { id: 'c3-bw4', start: { componentId: 'c3-btn1', pinName: '1b' }, end: { componentId: 'esp32-c3', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2765,6 +2826,7 @@ void loop() { { id: 'seg-e', start: { componentId: 'arduino-uno', pinName: '6' }, end: { componentId: 'seg1', pinName: 'E' }, color: '#4488ff' }, { id: 'seg-f', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'seg1', pinName: 'F' }, color: '#aa44ff' }, { id: 'seg-g', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'seg1', pinName: 'G' }, color: '#ffffff' }, + { id: 'seg-gnd', start: { componentId: 'seg1', pinName: 'COM' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2816,27 +2878,28 @@ void loop() { { type: 'wokwi-7segment', id: 'pico-seg1', x: 440, y: 140, properties: { common: 'cathode', color: 'green' } }, ], wires: [ - { id: 'ps-a', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'pico-seg1', pinName: 'A' }, color: '#ff4444' }, - { id: 'ps-b', start: { componentId: 'nano-rp2040', pinName: 'D3' }, end: { componentId: 'pico-seg1', pinName: 'B' }, color: '#ff8800' }, - { id: 'ps-c', start: { componentId: 'nano-rp2040', pinName: 'D4' }, end: { componentId: 'pico-seg1', pinName: 'C' }, color: '#ffcc00' }, - { id: 'ps-d', start: { componentId: 'nano-rp2040', pinName: 'D5' }, end: { componentId: 'pico-seg1', pinName: 'D' }, color: '#44cc44' }, - { id: 'ps-e', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'pico-seg1', pinName: 'E' }, color: '#4488ff' }, - { id: 'ps-f', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'pico-seg1', pinName: 'F' }, color: '#aa44ff' }, - { id: 'ps-g', start: { componentId: 'nano-rp2040', pinName: 'D8' }, end: { componentId: 'pico-seg1', pinName: 'G' }, color: '#ffffff' }, + { id: 'ps-a', start: { componentId: 'nano-rp2040', pinName: 'GP2' }, end: { componentId: 'pico-seg1', pinName: 'A' }, color: '#ff4444' }, + { id: 'ps-b', start: { componentId: 'nano-rp2040', pinName: 'GP3' }, end: { componentId: 'pico-seg1', pinName: 'B' }, color: '#ff8800' }, + { id: 'ps-c', start: { componentId: 'nano-rp2040', pinName: 'GP4' }, end: { componentId: 'pico-seg1', pinName: 'C' }, color: '#ffcc00' }, + { id: 'ps-d', start: { componentId: 'nano-rp2040', pinName: 'GP5' }, end: { componentId: 'pico-seg1', pinName: 'D' }, color: '#44cc44' }, + { id: 'ps-e', start: { componentId: 'nano-rp2040', pinName: 'GP6' }, end: { componentId: 'pico-seg1', pinName: 'E' }, color: '#4488ff' }, + { id: 'ps-f', start: { componentId: 'nano-rp2040', pinName: 'GP7' }, end: { componentId: 'pico-seg1', pinName: 'F' }, color: '#aa44ff' }, + { id: 'ps-g', start: { componentId: 'nano-rp2040', pinName: 'GP8' }, end: { componentId: 'pico-seg1', pinName: 'G' }, color: '#ffffff' }, + { id: 'ps-gnd', start: { componentId: 'pico-seg1', pinName: 'COM' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, { id: 'esp32-7segment', title: 'ESP32: 7-Segment Counter', - description: 'Count 0–9 on a 7-segment display driven from GPIO 12–18 on the ESP32.', + description: 'Count 0–9 on a 7-segment display driven from GPIO 12, 13, 14, 22, 25, 26, 27 on the ESP32.', category: 'displays', difficulty: 'beginner', boardType: 'esp32', boardFilter: 'esp32', code: `// ESP32 — 7-Segment Display Counter 0-9 -// Segments: a=12, b=13, c=14, d=15, e=16, f=17, g=18 +// Segments: a=12, b=13, c=14, d=25, e=26, f=27, g=22 -const int SEG[7] = {12, 13, 14, 15, 16, 17, 18}; +const int SEG[7] = {12, 13, 14, 25, 26, 27, 22}; const bool DIGITS[10][7] = { {1,1,1,1,1,1,0}, // 0 @@ -2873,13 +2936,14 @@ void loop() { { type: 'wokwi-7segment', id: 'esp-seg1', x: 440, y: 140, properties: { common: 'cathode', color: 'orange' } }, ], wires: [ - { id: 'es-a', start: { componentId: 'arduino-uno', pinName: 'GPIO12' }, end: { componentId: 'esp-seg1', pinName: 'A' }, color: '#ff4444' }, - { id: 'es-b', start: { componentId: 'arduino-uno', pinName: 'GPIO13' }, end: { componentId: 'esp-seg1', pinName: 'B' }, color: '#ff8800' }, - { id: 'es-c', start: { componentId: 'arduino-uno', pinName: 'GPIO14' }, end: { componentId: 'esp-seg1', pinName: 'C' }, color: '#ffcc00' }, - { id: 'es-d', start: { componentId: 'arduino-uno', pinName: 'GPIO15' }, end: { componentId: 'esp-seg1', pinName: 'D' }, color: '#44cc44' }, - { id: 'es-e', start: { componentId: 'arduino-uno', pinName: 'GPIO16' }, end: { componentId: 'esp-seg1', pinName: 'E' }, color: '#4488ff' }, - { id: 'es-f', start: { componentId: 'arduino-uno', pinName: 'GPIO17' }, end: { componentId: 'esp-seg1', pinName: 'F' }, color: '#aa44ff' }, - { id: 'es-g', start: { componentId: 'arduino-uno', pinName: 'GPIO18' }, end: { componentId: 'esp-seg1', pinName: 'G' }, color: '#ffffff' }, + { id: 'es-a', start: { componentId: 'esp32', pinName: '12' }, end: { componentId: 'esp-seg1', pinName: 'A' }, color: '#ff4444' }, + { id: 'es-b', start: { componentId: 'esp32', pinName: '13' }, end: { componentId: 'esp-seg1', pinName: 'B' }, color: '#ff8800' }, + { id: 'es-c', start: { componentId: 'esp32', pinName: '14' }, end: { componentId: 'esp-seg1', pinName: 'C' }, color: '#ffcc00' }, + { id: 'es-d', start: { componentId: 'esp32', pinName: '25' }, end: { componentId: 'esp-seg1', pinName: 'D' }, color: '#44cc44' }, + { id: 'es-e', start: { componentId: 'esp32', pinName: '26' }, end: { componentId: 'esp-seg1', pinName: 'E' }, color: '#4488ff' }, + { id: 'es-f', start: { componentId: 'esp32', pinName: '27' }, end: { componentId: 'esp-seg1', pinName: 'F' }, color: '#aa44ff' }, + { id: 'es-g', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'esp-seg1', pinName: 'G' }, color: '#ffffff' }, + { id: 'es-gnd', start: { componentId: 'esp-seg1', pinName: 'COM' }, end: { componentId: 'esp32', pinName: 'GND' }, color: '#000000' }, ], }, @@ -2920,6 +2984,8 @@ void loop() { ], wires: [ { id: 'w-pot-sig', start: { componentId: 'arduino-uno', pinName: 'A0' }, end: { componentId: 'pot1', pinName: 'SIG' }, color: '#aa44ff' }, + { id: 'w-pot-vcc', start: { componentId: 'arduino-uno', pinName: '5V' }, end: { componentId: 'pot1', pinName: 'VCC' }, color: '#ff0000' }, + { id: 'w-pot-gnd', start: { componentId: 'arduino-uno', pinName: 'GND' }, end: { componentId: 'pot1', pinName: 'GND' }, color: '#000000' }, ], }, { @@ -2963,6 +3029,7 @@ void loop() { { id: 'w-r', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'rgb1', pinName: 'R' }, color: '#ff2222' }, { id: 'w-g', start: { componentId: 'arduino-uno', pinName: '10' }, end: { componentId: 'rgb1', pinName: 'G' }, color: '#22cc22' }, { id: 'w-b', start: { componentId: 'arduino-uno', pinName: '11' }, end: { componentId: 'rgb1', pinName: 'B' }, color: '#2244ff' }, + { id: 'w-rgb-gnd', start: { componentId: 'rgb1', pinName: 'COM' }, end: { componentId: 'arduino-uno', pinName: 'GND' }, color: '#000000' }, ], }, @@ -2997,8 +3064,10 @@ void loop() { { type: 'wokwi-led', id: 'pico-led-btn', x: 440, y: 260, properties: { color: 'yellow' } }, ], wires: [ - { id: 'pb-btn', start: { componentId: 'nano-rp2040', pinName: 'D2' }, end: { componentId: 'pico-btn1', pinName: '1a' }, color: '#00aaff' }, - { id: 'pb-led', start: { componentId: 'nano-rp2040', pinName: 'D3' }, end: { componentId: 'pico-led-btn', pinName: 'A' }, color: '#ffcc00' }, + { id: 'pb-btn', start: { componentId: 'nano-rp2040', pinName: 'GP2' }, end: { componentId: 'pico-btn1', pinName: '1a' }, color: '#00aaff' }, + { id: 'pb-led', start: { componentId: 'nano-rp2040', pinName: 'GP3' }, end: { componentId: 'pico-led-btn', pinName: 'A' }, color: '#ffcc00' }, + { id: 'pb-led-gnd', start: { componentId: 'pico-led-btn', pinName: 'C' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, + { id: 'pb-btn-gnd', start: { componentId: 'pico-btn1', pinName: '1b' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, { @@ -3042,9 +3111,10 @@ void loop() { { type: 'wokwi-rgb-led', id: 'pico-rgb1', x: 440, y: 160, properties: {} }, ], wires: [ - { id: 'pr-r', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'pico-rgb1', pinName: 'R' }, color: '#ff2222' }, - { id: 'pr-g', start: { componentId: 'nano-rp2040', pinName: 'D7' }, end: { componentId: 'pico-rgb1', pinName: 'G' }, color: '#22cc22' }, - { id: 'pr-b', start: { componentId: 'nano-rp2040', pinName: 'D8' }, end: { componentId: 'pico-rgb1', pinName: 'B' }, color: '#2244ff' }, + { id: 'pr-r', start: { componentId: 'nano-rp2040', pinName: 'GP6' }, end: { componentId: 'pico-rgb1', pinName: 'R' }, color: '#ff2222' }, + { id: 'pr-g', start: { componentId: 'nano-rp2040', pinName: 'GP7' }, end: { componentId: 'pico-rgb1', pinName: 'G' }, color: '#22cc22' }, + { id: 'pr-b', start: { componentId: 'nano-rp2040', pinName: 'GP8' }, end: { componentId: 'pico-rgb1', pinName: 'B' }, color: '#2244ff' }, + { id: 'pr-gnd', start: { componentId: 'pico-rgb1', pinName: 'COM' }, end: { componentId: 'nano-rp2040', pinName: 'GND.1' }, color: '#000000' }, ], }, ]; diff --git a/frontend/src/simulation/parts/ChipParts.ts b/frontend/src/simulation/parts/ChipParts.ts index 4307595..ebdc423 100644 --- a/frontend/src/simulation/parts/ChipParts.ts +++ b/frontend/src/simulation/parts/ChipParts.ts @@ -207,4 +207,9 @@ PartSimulationRegistry.register('7segment', { return () => unsubscribers.forEach(u => u()); }, + // Called by SimulatorCanvas for boards without a local simulator (e.g. ESP32 via QEMU backend). + // pinName is the segment identifier (A, B, C, D, E, F, G, DP). + onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => { + set7SegPin(element, pinName, state); + }, }); diff --git a/frontend/src/store/useOscilloscopeStore.ts b/frontend/src/store/useOscilloscopeStore.ts index 9f46d83..fd3cf06 100644 --- a/frontend/src/store/useOscilloscopeStore.ts +++ b/frontend/src/store/useOscilloscopeStore.ts @@ -2,7 +2,10 @@ * Oscilloscope / Logic Analyzer store. * * Captures pin HIGH/LOW transitions with microsecond-level timestamps - * derived from the AVR CPU cycle counter and renders them as waveforms. + * derived from the CPU cycle counter and renders them as waveforms. + * + * Channels are keyed by (boardId, pin) so multiple boards with the same + * logical pin number can be monitored independently. */ import { create } from 'zustand'; @@ -23,6 +26,8 @@ export const CHANNEL_COLORS = [ export interface OscChannel { id: string; + /** Board that owns this channel */ + boardId: string; pin: number; label: string; color: string; @@ -51,7 +56,7 @@ interface OscilloscopeState { toggleOscilloscope: () => void; setCapturing: (running: boolean) => void; setTimeDivMs: (ms: number) => void; - addChannel: (pin: number) => void; + addChannel: (boardId: string, pin: number, pinLabel: string) => void; removeChannel: (id: string) => void; /** Push one sample; drops the oldest if the buffer is full */ pushSample: (channelId: string, timeMs: number, state: boolean) => void; @@ -71,18 +76,16 @@ export const useOscilloscopeStore = create((set, get) => ({ setTimeDivMs: (ms) => set({ timeDivMs: ms }), - addChannel: (pin: number) => { + addChannel: (boardId: string, pin: number, pinLabel: string) => { const { channels } = get(); - if (channels.some((c) => c.pin === pin)) return; // already added + // Deduplicate by (boardId, pin) + if (channels.some((c) => c.boardId === boardId && c.pin === pin)) return; - const isAnalog = pin >= 14 && pin <= 19; - const pinLabel = isAnalog ? `A${pin - 14}` : `D${pin}`; - - const id = `osc-ch-${pin}`; + const id = `osc-ch-${boardId}-${pin}`; const color = CHANNEL_COLORS[channels.length % CHANNEL_COLORS.length]; set((s) => ({ - channels: [...s.channels, { id, pin, label: pinLabel, color }], + channels: [...s.channels, { id, boardId, pin, label: pinLabel, color }], samples: { ...s.samples, [id]: [] }, })); }, @@ -103,9 +106,8 @@ export const useOscilloscopeStore = create((set, get) => ({ const buf = s.samples[channelId]; if (!buf) return s; - // Efficient copy: one allocation instead of two (avoids spread + slice). const next = buf.slice(); - if (next.length >= MAX_SAMPLES) next.shift(); // drop oldest + if (next.length >= MAX_SAMPLES) next.shift(); next.push({ timeMs, state }); return { samples: { ...s.samples, [channelId]: next } }; }); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 28a2394..838b150 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -213,11 +213,11 @@ export const useSimulatorStore = create((set, get) => { const initialPm = new PinManager(); pinManagerMap.set(INITIAL_BOARD_ID, initialPm); - function getOscilloscopeCallback() { + function getOscilloscopeCallback(boardId: string) { return (pin: number, state: boolean, timeMs: number) => { const { channels, pushSample } = useOscilloscopeStore.getState(); for (const ch of channels) { - if (ch.pin === pin) pushSample(ch.id, timeMs, state); + if (ch.boardId === boardId && ch.pin === pin) pushSample(ch.id, timeMs, state); } }; } @@ -243,7 +243,7 @@ export const useSimulatorStore = create((set, get) => { return { boards, ...(isActive ? { serialBaudRate: baud } : {}) }; }); }, - getOscilloscopeCallback(), + getOscilloscopeCallback(INITIAL_BOARD_ID), ); // Cross-board serial bridge for the initial board: AVR TX → Pi bridges RX const initialOrigSerial = initialSim.onSerialData; @@ -341,7 +341,7 @@ export const useSimulatorStore = create((set, get) => { return { boards, ...(isActive ? { serialBaudRate: baud } : {}) }; }); }, - getOscilloscopeCallback(), + getOscilloscopeCallback(id), ); // Cross-board serial bridge: AVR TX → all Pi bridges RX const origSerial = sim.onSerialData; @@ -948,8 +948,10 @@ export const useSimulatorStore = create((set, get) => { const component = state.components.find((c) => c.id === componentId); // Check if this componentId matches a board id const board = state.boards.find((b) => b.id === componentId); - const compX = component ? component.x : (board ? board.x : state.boardPosition.x); - const compY = component ? component.y : (board ? board.y : state.boardPosition.y); + // Components have a DynamicComponent wrapper with border:2px + padding:4px → offset (4,6) + // Boards are rendered directly without a wrapper, so no offset. + const compX = component ? component.x + 4 : (board ? board.x : state.boardPosition.x); + const compY = component ? component.y + 6 : (board ? board.y : state.boardPosition.y); const updatedWires = state.wires.map((wire) => { const updated = { ...wire }; @@ -972,21 +974,21 @@ export const useSimulatorStore = create((set, get) => { const updatedWires = state.wires.map((wire) => { const updated = { ...wire }; - // Resolve start + // Resolve start — components have wrapper offset (4,6), boards do not const startComp = state.components.find((c) => c.id === wire.start.componentId); const startBoard = state.boards.find((b) => b.id === wire.start.componentId); - const startX = startComp ? startComp.x : (startBoard ? startBoard.x : state.boardPosition.x); - const startY = startComp ? startComp.y : (startBoard ? startBoard.y : state.boardPosition.y); + const startX = startComp ? startComp.x + 4 : (startBoard ? startBoard.x : state.boardPosition.x); + const startY = startComp ? startComp.y + 6 : (startBoard ? startBoard.y : state.boardPosition.y); const startPos = calculatePinPosition(wire.start.componentId, wire.start.pinName, startX, startY); updated.start = startPos ? { ...wire.start, x: startPos.x, y: startPos.y } : { ...wire.start, x: startX, y: startY }; - // Resolve end + // Resolve end — components have wrapper offset (4,6), boards do not const endComp = state.components.find((c) => c.id === wire.end.componentId); const endBoard = state.boards.find((b) => b.id === wire.end.componentId); - const endX = endComp ? endComp.x : (endBoard ? endBoard.x : state.boardPosition.x); - const endY = endComp ? endComp.y : (endBoard ? endBoard.y : state.boardPosition.y); + const endX = endComp ? endComp.x + 4 : (endBoard ? endBoard.x : state.boardPosition.x); + const endY = endComp ? endComp.y + 6 : (endBoard ? endBoard.y : state.boardPosition.y); const endPos = calculatePinPosition(wire.end.componentId, wire.end.pinName, endX, endY); updated.end = endPos ? { ...wire.end, x: endPos.x, y: endPos.y }