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.
pull/47/head
David Montero Crespo 2026-03-16 10:31:02 -03:00
parent 5e6c5d451b
commit bc31ee76c1
7 changed files with 344 additions and 172 deletions

View File

@ -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

View File

@ -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 <canvas>.
* 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<ChannelCanvasProps> = ({
channel,
samples,
windowEndMs,
windowMs,
channel, samples, windowEndMs, windowMs,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const wrapRef = useRef<HTMLDivElement>(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<ChannelCanvasProps> = ({
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<RulerCanvasProps> = ({ 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<RulerCanvasProps> = ({ 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<HTMLDivElement>;
}
const ChannelPicker: React.FC<ChannelPickerProps> = ({
onAdd, activeChannels, onClose, anchorRect, dropdownRef,
}) => {
const boards = useSimulatorStore((s) => s.boards);
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
const [selectedBoardId, setSelectedBoardId] = useState<string>(
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(
<div
ref={dropdownRef}
className="osc-picker-dropdown osc-picker-multiboard"
style={style}
>
{/* Board tabs */}
<div className="osc-picker-board-tabs">
{boards.map((b) => (
<button
key={b.id}
className={`osc-picker-board-tab${b.id === selectedBoard?.id ? ' active' : ''}`}
onClick={() => setSelectedBoardId(b.id)}
title={BOARD_KIND_LABELS[b.boardKind]}
>
{b.id}
</button>
))}
</div>
{/* Board label */}
{selectedBoard && (
<div className="osc-picker-board-label">
{BOARD_KIND_LABELS[selectedBoard.boardKind]}
</div>
)}
{/* Pin grid */}
<div className="osc-picker-pins">
{pins.map(({ pin, label }) => {
const added = activePinsForBoard.has(pin);
return (
<button
key={pin}
className={`osc-pin-btn${added ? ' osc-pin-btn-active' : ''}`}
onClick={() => {
if (!added) {
onAdd(selectedBoardId, pin, label);
onClose();
}
}}
title={added ? 'Already added' : `Monitor ${label}`}
>
{label}
</button>
);
})}
</div>
</div>,
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<HTMLDivElement>(null);
const [pickerAnchor, setPickerAnchor] = useState<DOMRect | null>(null);
const addBtnRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(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<number | null>(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 (
<div className="osc-container">
@ -354,31 +453,25 @@ export const Oscilloscope: React.FC = () => {
<div className="osc-header">
<span className="osc-title">Oscilloscope</span>
{/* Add Channel button + dropdown */}
<div className="osc-picker-wrap" ref={pickerRef}>
<button
className="osc-btn"
onClick={() => setShowPicker((v) => !v)}
title="Add a pin channel"
>
+ Add Channel
</button>
{/* Add Channel button + portal picker */}
<button
ref={addBtnRef}
className="osc-btn"
onClick={handleTogglePicker}
title="Add a pin channel"
>
+ Add Channel
</button>
{showPicker && (
<div className="osc-picker-dropdown">
{AVAILABLE_PINS.map(({ pin, label }) => (
<button
key={pin}
className={`osc-pin-btn${activePins.has(pin) ? ' osc-pin-btn-active' : ''}`}
onClick={() => !activePins.has(pin) && handleAddChannel(pin)}
title={activePins.has(pin) ? 'Already added' : `Monitor ${label}`}
>
{label}
</button>
))}
</div>
)}
</div>
{showPicker && pickerAnchor && (
<ChannelPicker
onAdd={handleAddChannel}
activeChannels={channels}
onClose={() => { setShowPicker(false); setPickerAnchor(null); }}
anchorRect={pickerAnchor}
dropdownRef={dropdownRef}
/>
)}
{/* Time / div */}
<span className="osc-label">Time/div:</span>
@ -422,8 +515,10 @@ export const Oscilloscope: React.FC = () => {
<div className="osc-waveforms">
{channels.map((ch) => (
<div key={ch.id} className="osc-channel-row">
{/* Label + remove button */}
<div className="osc-channel-label">
<span className="osc-channel-board" title={ch.boardId}>
{boardShortName(ch.boardId)}
</span>
<span className="osc-channel-name" style={{ color: ch.color }}>
{ch.label}
</span>
@ -436,7 +531,6 @@ export const Oscilloscope: React.FC = () => {
</button>
</div>
{/* Waveform canvas */}
<ChannelCanvas
channel={ch}
samples={samples[ch.id] ?? []}
@ -447,7 +541,6 @@ export const Oscilloscope: React.FC = () => {
))}
</div>
{/* Time ruler */}
<RulerCanvas
windowEndMs={windowEndMs}
windowMs={windowMs}

View File

@ -469,7 +469,7 @@ export const SimulatorCanvas = () => {
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(() => {

View File

@ -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 09 on a 7-segment display driven from GPIO 1218 on the ESP32.',
description: 'Count 09 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' },
],
},
];

View File

@ -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);
},
});

View File

@ -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<OscilloscopeState>((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<OscilloscopeState>((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 } };
});

View File

@ -213,11 +213,11 @@ export const useSimulatorStore = create<SimulatorState>((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<SimulatorState>((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<SimulatorState>((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<SimulatorState>((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<SimulatorState>((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 }