feat: enhance simulation accuracy and component interactions across various modules

This commit is contained in:
David Montero Crespo 2026-03-20 17:11:12 -03:00
parent f6fdae6b0e
commit dc5dfb8635
11 changed files with 201 additions and 75 deletions

View File

@ -1,6 +1,6 @@
{ {
"version": "1.0.0", "version": "1.0.0",
"generatedAt": "2026-03-19T00:06:03.160Z", "generatedAt": "2026-03-20T17:33:38.631Z",
"components": [ "components": [
{ {
"id": "arduino-mega", "id": "arduino-mega",

View File

@ -197,7 +197,8 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
if (logic && logic.attachEvents && simulator) { if (logic && logic.attachEvents && simulator) {
// Helper to find Arduino pin connected to a component pin // Helper to find Arduino pin connected to a component pin
const getArduinoPin = (componentPinName: string): number | null => { const getArduinoPin = (componentPinName: string): number | null => {
const wires = useSimulatorStore.getState().wires.filter( const state = useSimulatorStore.getState();
const wires = state.wires.filter(
w => (w.start.componentId === id && w.start.pinName === componentPinName) || w => (w.start.componentId === id && w.start.pinName === componentPinName) ||
(w.end.componentId === id && w.end.pinName === componentPinName) (w.end.componentId === id && w.end.pinName === componentPinName)
); );
@ -207,7 +208,11 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
const boardEndpoint = isBoardComponent(w.start.componentId) ? w.start : const boardEndpoint = isBoardComponent(w.start.componentId) ? w.start :
isBoardComponent(w.end.componentId) ? w.end : null; isBoardComponent(w.end.componentId) ? w.end : null;
if (boardEndpoint) { if (boardEndpoint) {
const pin = boardPinToNumber(boardEndpoint.componentId, boardEndpoint.pinName); // Use the board's actual kind for pin mapping (instance ID may differ from kind,
// e.g. board ID 'arduino-uno' after switching to 'raspberry-pi-pico')
const boardKind = state.boards.find((b) => b.id === boardEndpoint.componentId)?.boardKind
?? boardEndpoint.componentId;
const pin = boardPinToNumber(boardKind, boardEndpoint.pinName);
if (pin !== null) return pin; if (pin !== null) return pin;
} }
} }

View File

@ -454,34 +454,41 @@ export const SimulatorCanvas = () => {
// Helper to add subscription // Helper to add subscription
const subscribeComponentToPin = (component: any, pin: number, componentPinName?: string) => { const subscribeComponentToPin = (component: any, pin: number, componentPinName?: string) => {
// Components with attachEvents in PartSimulationRegistry manage their own
// visual state (e.g. servo, buzzer). Skip generic digital/PWM updates for them
// to avoid flickering from raw PWM pulses being misinterpreted as on/off state.
const logic = PartSimulationRegistry.get(component.metadataId);
const hasSelfManagedVisuals = !!(logic && logic.attachEvents);
const unsubscribe = pinManager.onPinChange( const unsubscribe = pinManager.onPinChange(
pin, pin,
(_pin, state) => { (_pin, state) => {
// 1. Update React state for standard properties if (!hasSelfManagedVisuals) {
updateComponentState(component.id, state); // 1. Update React state for standard properties (LEDs, buttons, etc.)
updateComponentState(component.id, state);
}
// 2. Delegate to PartSimulationRegistry for custom visual updates // 2. Delegate to PartSimulationRegistry for custom visual updates
const logic = PartSimulationRegistry.get(component.metadataId);
if (logic && logic.onPinStateChange) { if (logic && logic.onPinStateChange) {
const el = document.getElementById(component.id); const el = document.getElementById(component.id);
if (el) { if (el) {
logic.onPinStateChange(componentPinName || 'A', state, el); logic.onPinStateChange(componentPinName || 'A', state, el);
} }
} }
console.log(`Component ${component.id} on pin ${pin}: ${state ? 'HIGH' : 'LOW'}`);
} }
); );
unsubscribers.push(unsubscribe); unsubscribers.push(unsubscribe);
// PWM subscription: update LED opacity when the pin receives a LEDC duty cycle. // PWM subscription: update LED opacity when the pin receives a PWM duty cycle.
// duty=0 means no PWM / analogWrite(0) — clear the inline style so the // Skip for self-managed components (servo, buzzer) — their duty cycle is a
// component keeps its default visibility instead of becoming invisible. // control signal, not a brightness value, so setting opacity would cause flicker.
const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => { if (!hasSelfManagedVisuals) {
const el = document.getElementById(component.id); const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => {
if (el) el.style.opacity = duty > 0 ? String(duty) : ''; const el = document.getElementById(component.id);
}); if (el) el.style.opacity = duty > 0 ? String(duty) : '';
unsubscribers.push(pwmUnsub); });
unsubscribers.push(pwmUnsub);
}
}; };
components.forEach((component) => { components.forEach((component) => {

View File

@ -309,6 +309,11 @@ export class AVRSimulator {
return this.adc; return this.adc;
} }
/** Returns the CPU clock frequency in Hz (16 MHz for AVR). */
getClockHz(): number {
return 16_000_000;
}
/** /**
* Returns the current CPU cycle count. * Returns the current CPU cycle count.
* Used by timing-sensitive peripherals to schedule future pin changes. * Used by timing-sensitive peripherals to schedule future pin changes.

View File

@ -73,8 +73,6 @@ export class PinManager {
if (callbacks) { if (callbacks) {
callbacks.forEach(cb => cb(arduinoPin, newState)); callbacks.forEach(cb => cb(arduinoPin, newState));
} }
console.log(`Pin ${arduinoPin} (${portName}${bit}): ${oldState ? 'HIGH' : 'LOW'}${newState ? 'HIGH' : 'LOW'}`);
} }
} }
} }

View File

@ -49,6 +49,8 @@ export class RP2040Simulator {
private speed = 1.0; private speed = 1.0;
private gpioUnsubscribers: Array<() => void> = []; private gpioUnsubscribers: Array<() => void> = [];
private flashCopy: Uint8Array | null = null; private flashCopy: Uint8Array | null = null;
private totalCycles = 0;
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
/** Serial output callback — fires for each byte the Pico sends on UART0 */ /** Serial output callback — fires for each byte the Pico sends on UART0 */
public onSerialData: ((char: string) => void) | null = null; public onSerialData: ((char: string) => void) | null = null;
@ -269,7 +271,10 @@ export class RP2040Simulator {
const jump: number = clock.nanosToNextAlarm; const jump: number = clock.nanosToNextAlarm;
if (jump <= 0) break; // no pending alarms if (jump <= 0) break; // no pending alarms
clock.tick(jump); clock.tick(jump);
cyclesDone += Math.ceil(jump / CYCLE_NANOS); const jumped = Math.ceil(jump / CYCLE_NANOS);
cyclesDone += jumped;
this.totalCycles += jumped;
this.flushScheduledPinChanges();
} else { } else {
break; break;
} }
@ -277,6 +282,8 @@ export class RP2040Simulator {
const cycles: number = core.executeInstruction(); const cycles: number = core.executeInstruction();
if (clock) clock.tick(cycles * CYCLE_NANOS); if (clock) clock.tick(cycles * CYCLE_NANOS);
cyclesDone += cycles; cyclesDone += cycles;
this.totalCycles += cycles;
this.flushScheduledPinChanges();
} }
} }
@ -308,6 +315,8 @@ export class RP2040Simulator {
reset(): void { reset(): void {
this.stop(); this.stop();
this.totalCycles = 0;
this.scheduledPinChanges = [];
if (this.rp2040 && this.flashCopy) { if (this.rp2040 && this.flashCopy) {
this.initMCU(this.flashCopy); this.initMCU(this.flashCopy);
// Re-register any previously added I2C devices // Re-register any previously added I2C devices
@ -328,6 +337,34 @@ export class RP2040Simulator {
return this.speed; return this.speed;
} }
/** Returns the CPU clock frequency in Hz. */
getClockHz(): number {
return F_CPU;
}
/** Returns total CPU cycles executed since last reset/load. */
getCurrentCycles(): number {
return this.totalCycles;
}
/**
* Schedule a GPIO pin state change at a specific future cycle count.
* Enables cycle-accurate protocol simulation (e.g. HC-SR04 echo timing).
*/
schedulePinChange(pin: number, state: boolean, atCycle: number): void {
let i = this.scheduledPinChanges.length;
while (i > 0 && this.scheduledPinChanges[i - 1].cycle > atCycle) i--;
this.scheduledPinChanges.splice(i, 0, { cycle: atCycle, pin, state });
}
private flushScheduledPinChanges(): void {
if (this.scheduledPinChanges.length === 0) return;
while (this.scheduledPinChanges.length > 0 && this.scheduledPinChanges[0].cycle <= this.totalCycles) {
const { pin, state } = this.scheduledPinChanges.shift()!;
this.setPinState(pin, state);
}
}
/** /**
* Drive a GPIO pin externally (e.g. from a button or slider). * Drive a GPIO pin externally (e.g. from a button or slider).
* GPIO n = Arduino D(n) for Raspberry Pi Pico. * GPIO n = Arduino D(n) for Raspberry Pi Pico.

View File

@ -184,23 +184,29 @@ PartSimulationRegistry.register('photoresistor-sensor', {
*/ */
PartSimulationRegistry.register('analog-joystick', { PartSimulationRegistry.register('analog-joystick', {
attachEvents: (element, avrSimulator, getArduinoPinHelper, componentId) => { attachEvents: (element, avrSimulator, getArduinoPinHelper, componentId) => {
const pinX = getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT'); // wokwi-analog-joystick uses VERT/HORZ/SEL pin names
const pinY = getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT'); const pinX = getArduinoPinHelper('VERT') ?? getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT');
const pinSW = getArduinoPinHelper('SW'); const pinY = getArduinoPinHelper('HORZ') ?? getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT');
const pinSW = getArduinoPinHelper('SEL') ?? getArduinoPinHelper('SW');
const el = element as any; const el = element as any;
// Center position is mid-range (~2.5V) // RP2040 uses 3.3V reference; AVR uses 5V
if (pinX !== null) setAdcVoltage(avrSimulator, pinX, 2.5); const vcc = avrSimulator instanceof RP2040Simulator ? 3.3 : 5.0;
if (pinY !== null) setAdcVoltage(avrSimulator, pinY, 2.5); const centerV = vcc / 2;
// Initialize to center position and button not pressed
if (pinX !== null) setAdcVoltage(avrSimulator, pinX, centerV);
if (pinY !== null) setAdcVoltage(avrSimulator, pinY, centerV);
if (pinSW !== null) avrSimulator.setPinState(pinSW, true); // HIGH = not pressed
const onMove = () => { const onMove = () => {
// xValue / yValue are 0-1023 // xValue / yValue are 0-1023
if (pinX !== null) { if (pinX !== null) {
const vx = ((el.xValue ?? 512) / 1023.0) * 5.0; const vx = ((el.xValue ?? 512) / 1023.0) * vcc;
setAdcVoltage(avrSimulator, pinX, vx); setAdcVoltage(avrSimulator, pinX, vx);
} }
if (pinY !== null) { if (pinY !== null) {
const vy = ((el.yValue ?? 512) / 1023.0) * 5.0; const vy = ((el.yValue ?? 512) / 1023.0) * vcc;
setAdcVoltage(avrSimulator, pinY, vy); setAdcVoltage(avrSimulator, pinY, vy);
} }
}; };
@ -219,13 +225,13 @@ PartSimulationRegistry.register('analog-joystick', {
element.addEventListener('button-press', onPress); element.addEventListener('button-press', onPress);
element.addEventListener('button-release', onRelease); element.addEventListener('button-release', onRelease);
// SensorControlPanel: xAxis/yAxis -512..512 → voltage 05V (center = 2.5V) // SensorControlPanel: xAxis/yAxis -512..512 → voltage 0VCC (center = VCC/2)
registerSensorUpdate(componentId, (values) => { registerSensorUpdate(componentId, (values) => {
if ('xAxis' in values && pinX !== null) { if ('xAxis' in values && pinX !== null) {
setAdcVoltage(avrSimulator, pinX, ((values.xAxis as number + 512) / 1023) * 5.0); setAdcVoltage(avrSimulator, pinX, ((values.xAxis as number + 512) / 1023) * vcc);
} }
if ('yAxis' in values && pinY !== null) { if ('yAxis' in values && pinY !== null) {
setAdcVoltage(avrSimulator, pinY, ((values.yAxis as number + 512) / 1023) * 5.0); setAdcVoltage(avrSimulator, pinY, ((values.yAxis as number + 512) / 1023) * vcc);
} }
}); });
@ -242,34 +248,80 @@ PartSimulationRegistry.register('analog-joystick', {
// ─── Servo ─────────────────────────────────────────────────────────────────── // ─── Servo ───────────────────────────────────────────────────────────────────
/** /**
* Servo motor reads OCR1A and ICR1 to calculate pulse width and angle. * Servo motor measures actual PWM pulse width from pin state changes.
* *
* Standard RC servo protocol: * Standard RC servo protocol:
* - 50 Hz signal (20 ms period) * - 50 Hz signal (20 ms period)
* - Pulse width 1 ms 0°, 1.5 ms 90°, 2 ms 180° * - Pulse width 544 µs 0°, 1472 µs 90°, 2400 µs 180°
* (Arduino Servo.h uses 5442400 µs, NOT the generic 10002000 µs range)
* *
* With Timer1, prescaler=8, F_CPU=16MHz: * Approach: subscribe to the servo's PWM pin state changes, record the CPU
* - ICR1 = 20000 for 50Hz * cycle count at the rising edge, then compute pulse width on the falling edge.
* - OCR1A = 1000 0°, 1500 90°, 2000 180° * avr8js re-schedules Timer1 every 8 CPU cycles (prescaler=8), so each HIGH
* and LOW transition fires in a separate count() call with a distinct cpu.cycles
* value the measurement is cycle-accurate.
* *
* We poll these registers every animation frame via a requestAnimationFrame loop. * Fallback: if no wire is connected (pinSIG === null), poll OCR1A/ICR1 registers
* via requestAnimationFrame (less accurate but still functional).
*/ */
PartSimulationRegistry.register('servo', { PartSimulationRegistry.register('servo', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => { attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1'); const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1');
const el = element as any; const el = element as any;
// OCR1A low byte = 0x88, OCR1A high byte = 0x89 // Arduino Servo.h actual pulse range (544µs = 0°, 2400µs = 180°)
const MIN_PULSE_US = 544;
const MAX_PULSE_US = 2400;
const CPU_HZ = 16_000_000;
// ── Primary: cycle-accurate pulse width measurement ────────────────
if (pinSIG !== null) {
const pinManager = (avrSimulator as any).pinManager as import('../PinManager').PinManager | undefined;
if (pinManager) {
let riseTime = -1; // cpu.cycles at last rising edge
const unsubscribe = pinManager.onPinChange(pinSIG, (_pin, state) => {
const cpu = (avrSimulator as any).cpu;
if (!cpu) return;
if (state) {
// Rising edge — record cycle count
riseTime = cpu.cycles;
} else if (riseTime >= 0) {
// Falling edge — compute pulse width in µs
const pulseCycles = cpu.cycles - riseTime;
const pulseUs = (pulseCycles / CPU_HZ) * 1_000_000;
riseTime = -1;
// Only update if pulse is in valid RC servo range
if (pulseUs >= MIN_PULSE_US && pulseUs <= MAX_PULSE_US) {
const angle = Math.round(
((pulseUs - MIN_PULSE_US) / (MAX_PULSE_US - MIN_PULSE_US)) * 180
);
el.angle = angle;
}
}
});
return () => { unsubscribe(); };
}
}
// ── Fallback: poll OCR1A/ICR1 registers when no wire is connected ──
// OCR1A low byte = 0x88, high byte = 0x89
// ICR1L = 0x86, ICR1H = 0x87 // ICR1L = 0x86, ICR1H = 0x87
const OCR1AL = 0x88; const OCR1AL = 0x88;
const OCR1AH = 0x89; const OCR1AH = 0x89;
const ICR1L = 0x86; const ICR1L = 0x86;
const ICR1H = 0x87; const ICR1H = 0x87;
const SERVO_PERIOD_US = 20000;
let rafId: number | null = null; let rafId: number | null = null;
let lastOcr1a = -1; let lastOcr1a = -1;
const poll = () => { const poll = () => {
if (!avrSimulator.isRunning()) { rafId = requestAnimationFrame(poll); return; }
const cpu = (avrSimulator as any).cpu; const cpu = (avrSimulator as any).cpu;
if (!cpu) { rafId = requestAnimationFrame(poll); return; } if (!cpu) { rafId = requestAnimationFrame(poll); return; }
@ -278,39 +330,19 @@ PartSimulationRegistry.register('servo', {
lastOcr1a = ocr1a; lastOcr1a = ocr1a;
const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8); const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8);
// Calculate pulse width in microseconds
// prescaler 8, F_CPU 16MHz → 1 tick = 0.5µs
// pulse_us = ocr1a * 0.5
// But also handle prescaler 64 (1 tick = 4µs) and default ICR1 detection
let pulseUs: number; let pulseUs: number;
if (icr1 > 0) { if (icr1 > 0) {
// Proportional to ICR1 period (assume 20ms period) pulseUs = (ocr1a / icr1) * SERVO_PERIOD_US;
pulseUs = 1000 + (ocr1a / icr1) * 1000;
} else { } else {
// Fallback: prescaler 8 // prescaler 8, 16MHz → 0.5µs per tick
pulseUs = ocr1a * 0.5; pulseUs = ocr1a * 0.5;
} }
// Clamp to 1000-2000µs and map to 0-180° const clamped = Math.max(MIN_PULSE_US, Math.min(MAX_PULSE_US, pulseUs));
const clamped = Math.max(1000, Math.min(2000, pulseUs)); const angle = Math.round(((clamped - MIN_PULSE_US) / (MAX_PULSE_US - MIN_PULSE_US)) * 180);
const angle = Math.round(((clamped - 1000) / 1000) * 180);
el.angle = angle; el.angle = angle;
} }
// Also support PWM duty cycle approach via PinManager
if (pinSIG !== null) {
const pinManager = (avrSimulator as any).pinManager;
// Only override angle if cpu-based approach doesn't work
// (ICR1 = 0 means Timer1 not configured as servo)
const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8);
if (icr1 === 0 && pinManager) {
const dc = pinManager.getPwmValue(pinSIG);
if (dc > 0) {
el.angle = Math.round(dc * 180);
}
}
}
rafId = requestAnimationFrame(poll); rafId = requestAnimationFrame(poll);
}; };

View File

@ -415,31 +415,41 @@ function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement
const now = simulator.getCurrentCycles() as number; const now = simulator.getCurrentCycles() as number;
// Timing constants at 16 MHz (cycles per µs = 16) // Timing constants at 16 MHz (cycles per µs = 16)
// DHT22 starts pulling the line LOW ~20 µs after MCU releases it HIGH.
// The Adafruit DHT library v1.4.7 calls expectPulse(LOW) at ~55 µs (pullTime default),
// so the preamble LOW must already be active by then. Starting at 20 µs (320 cycles)
// guarantees the pin IS LOW when the library checks.
const RESPONSE_START = 320; // 20 µs — DHT22 response start
const LOW80 = 1280; // 80 µs LOW preamble const LOW80 = 1280; // 80 µs LOW preamble
const HIGH80 = 1280; // 80 µs HIGH preamble const HIGH80 = 1280; // 80 µs HIGH preamble
const LOW50 = 800; // 50 µs LOW marker before each bit const LOW50 = 800; // 50 µs LOW marker before each bit
const HIGH0 = 416; // 26 µs HIGH → bit '0' const HIGH0 = 416; // 26 µs HIGH → bit '0'
const HIGH1 = 1120; // 70 µs HIGH → bit '1' const HIGH1 = 1120; // 70 µs HIGH → bit '1'
let t = now; let t = now + RESPONSE_START;
// Preamble: 80 µs LOW then 80 µs HIGH // Preamble: 80 µs LOW
t += LOW80; simulator.schedulePinChange(pin, false, t); simulator.schedulePinChange(pin, false, t);
t += HIGH80; simulator.schedulePinChange(pin, true, t); t += LOW80;
// Preamble: 80 µs HIGH
simulator.schedulePinChange(pin, true, t);
t += HIGH80;
// 40 data bits, MSB first // 40 data bits, MSB first — schedule LOW then advance, schedule HIGH then advance
for (const byte of payload) { for (const byte of payload) {
for (let b = 7; b >= 0; b--) { for (let b = 7; b >= 0; b--) {
const bit = (byte >> b) & 1; const bit = (byte >> b) & 1;
t += LOW50; simulator.schedulePinChange(pin, false, t); simulator.schedulePinChange(pin, false, t);
t += LOW50;
simulator.schedulePinChange(pin, true, t);
t += bit ? HIGH1 : HIGH0; t += bit ? HIGH1 : HIGH0;
simulator.schedulePinChange(pin, true, t);
} }
} }
// Release line HIGH (it already is, but explicit for clarity) // Final release
t += LOW50; simulator.schedulePinChange(pin, false, t); simulator.schedulePinChange(pin, false, t);
t += HIGH0; simulator.schedulePinChange(pin, true, t); t += LOW50;
simulator.schedulePinChange(pin, true, t);
} }
PartSimulationRegistry.register('dht22', { PartSimulationRegistry.register('dht22', {
@ -449,10 +459,26 @@ PartSimulationRegistry.register('dht22', {
if (pin === null) return () => {}; if (pin === null) return () => {};
let wasLow = false; let wasLow = false;
// Prevent DHT22's own scheduled pin changes from re-triggering the response.
// After the MCU releases DATA HIGH and we begin responding, we ignore all
// pin-change callbacks until the full waveform has been emitted.
// DHT22 response is ~5 ms; at 16 MHz that is ~80 000 cycles. We gate for
// 200 000 cycles (~12.5 ms) to give plenty of headroom.
const RESPONSE_GATE_CYCLES = 200_000;
let responseEndCycle = 0;
const getCycles = (): number =>
typeof (simulator as any).getCurrentCycles === 'function'
? ((simulator as any).getCurrentCycles() as number)
: -1;
const unsub = (simulator as any).pinManager.onPinChange( const unsub = (simulator as any).pinManager.onPinChange(
pin, pin,
(_: number, state: boolean) => { (_: number, state: boolean) => {
// While DHT22 is driving the line, ignore our own scheduled changes.
const now = getCycles();
if (now >= 0 && now < responseEndCycle) return;
if (!state) { if (!state) {
// MCU drove DATA LOW — start signal detected // MCU drove DATA LOW — start signal detected
wasLow = true; wasLow = true;
@ -461,6 +487,8 @@ PartSimulationRegistry.register('dht22', {
if (wasLow) { if (wasLow) {
// MCU released DATA HIGH — begin DHT22 response // MCU released DATA HIGH — begin DHT22 response
wasLow = false; wasLow = false;
const cur = getCycles();
responseEndCycle = cur >= 0 ? cur + RESPONSE_GATE_CYCLES : 0;
scheduleDHT22Response(simulator, pin, element); scheduleDHT22Response(simulator, pin, element);
} }
}, },

View File

@ -642,12 +642,15 @@ PartSimulationRegistry.register('hc-sr04', {
// - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s // - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s
// (17150 cm/s = speed of sound, one-way = round-trip/2) // (17150 cm/s = speed of sound, one-way = round-trip/2)
if (typeof simulator.schedulePinChange === 'function') { if (typeof simulator.schedulePinChange === 'function') {
const clockHz: number = typeof (simulator as any).getClockHz === 'function'
? (simulator as any).getClockHz()
: 16_000_000;
const now = simulator.getCurrentCycles() as number; const now = simulator.getCurrentCycles() as number;
const processingCycles = 9600; // ~600 µs sensor overhead const processingCycles = Math.round(600e-6 * clockHz); // 600 µs sensor overhead
const echoCycles = Math.round((distanceCm / 17150) * 16_000_000); const echoCycles = Math.round((distanceCm / 17150) * clockHz);
simulator.schedulePinChange(echoPin, true, now + processingCycles); simulator.schedulePinChange(echoPin, true, now + processingCycles);
simulator.schedulePinChange(echoPin, false, now + processingCycles + echoCycles); simulator.schedulePinChange(echoPin, false, now + processingCycles + echoCycles);
console.log(`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles/16000).toFixed(1)} µs)`); console.log(`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles / (clockHz / 1e6)).toFixed(1)} µs)`);
} else { } else {
// Fallback: best-effort async (works with delay()-based sketches, not pulseIn) // Fallback: best-effort async (works with delay()-based sketches, not pulseIn)
const echoMs = Math.max(1, distanceCm / 17.15); const echoMs = Math.max(1, distanceCm / 17.15);

View File

@ -166,6 +166,8 @@ export function isBoardComponent(componentId: string): boolean {
*/ */
export function boardPinToNumber(boardId: string, pinName: string): number | null { export function boardPinToNumber(boardId: string, pinName: string): number | null {
if (boardId === 'arduino-uno' || boardId === 'arduino-nano') { if (boardId === 'arduino-uno' || boardId === 'arduino-nano') {
// Power / GND pins — not real GPIOs, skip silently
if (/^(GND|VCC|VIN|IOREF|AREF|RESET|3\.3V|3V3|5V|3V)/.test(pinName)) return -1;
// Try numeric (covers '0' through '13', also legacy examples using just numbers) // Try numeric (covers '0' through '13', also legacy examples using just numbers)
const num = parseInt(pinName, 10); const num = parseInt(pinName, 10);
if (!isNaN(num) && num >= 0 && num <= 21) return num; if (!isNaN(num) && num >= 0 && num <= 21) return num;
@ -190,6 +192,11 @@ export function boardPinToNumber(boardId: string, pinName: string): number | nul
} }
if (boardId === 'nano-rp2040' || boardId === 'raspberry-pi-pico') { if (boardId === 'nano-rp2040' || boardId === 'raspberry-pi-pico') {
// Power / GND pins — return -1 so callers skip silently
if (pinName.startsWith('GND') || pinName.startsWith('3.3V') || pinName.startsWith('3V3')
|| pinName.startsWith('5V') || pinName.startsWith('VBUS') || pinName.startsWith('VSYS')) {
return -1;
}
// Try D-prefix map first (D2 → GPIO25 = LED_BUILTIN, etc.) // Try D-prefix map first (D2 → GPIO25 = LED_BUILTIN, etc.)
const mapped = NANO_RP2040_PIN_MAP[pinName]; const mapped = NANO_RP2040_PIN_MAP[pinName];
if (mapped !== undefined) return mapped; if (mapped !== undefined) return mapped;

View File

@ -42,7 +42,11 @@ export function calculatePinPosition(
} }
// Find the specific pin // Find the specific pin
const pin = pinInfo.find((p: any) => p.name === pinName); let pin = pinInfo.find((p: any) => p.name === pinName);
// Fallback: try numbered variant (e.g. GND → GND.1) for pins that have suffix variants
if (!pin && !pinName.includes('.')) {
pin = pinInfo.find((p: any) => p.name === `${pinName}.1`);
}
if (!pin) { if (!pin) {
console.warn(`[pinPositionCalculator] Pin ${pinName} not found on component ${componentId}`); console.warn(`[pinPositionCalculator] Pin ${pinName} not found on component ${componentId}`);
console.warn(`Available pins:`, pinInfo.map((p: any) => p.name)); console.warn(`Available pins:`, pinInfo.map((p: any) => p.name));