diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 8d8532b..0ea5fec 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -3467,16 +3467,16 @@ void loop() { { id: 'pico-hcsr04', title: 'Pico: HC-SR04 Ultrasonic Distance', - description: 'Measure distance with an HC-SR04 sensor on the Raspberry Pi Pico. TRIG on GP9, ECHO on GP10.', + description: 'Measure distance with an HC-SR04 sensor on the Raspberry Pi Pico. TRIG on D5 (GP17), ECHO on D6 (GP18).', category: 'sensors', difficulty: 'beginner', boardType: 'raspberry-pi-pico', boardFilter: 'raspberry-pi-pico', code: `// Raspberry Pi Pico — HC-SR04 Ultrasonic Distance Sensor -// Wiring: TRIG → GP9 | ECHO → GP10 | VCC → 3.3V | GND → GND +// Wiring: TRIG → D5(GP17) | ECHO → D6(GP18) | VCC → 3.3V | GND → GND -#define TRIG_PIN 9 // GPIO 9 -#define ECHO_PIN 10 // GPIO 10 +#define TRIG_PIN 17 // GPIO 17 (D5) +#define ECHO_PIN 18 // GPIO 18 (D6) void setup() { Serial.begin(115200); @@ -3510,22 +3510,22 @@ void loop() { wires: [ { id: 'pcs-vcc', start: { componentId: 'nano-rp2040', pinName: '3.3V' }, end: { componentId: 'pico-sr1', pinName: 'VCC' }, color: '#ff4444' }, { id: 'pcs-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pico-sr1', pinName: 'GND' }, color: '#000000' }, - { id: 'pcs-trig', start: { componentId: 'nano-rp2040', pinName: 'GP9' }, end: { componentId: 'pico-sr1', pinName: 'TRIG' }, color: '#ff8800' }, - { id: 'pcs-echo', start: { componentId: 'nano-rp2040', pinName: 'GP10' }, end: { componentId: 'pico-sr1', pinName: 'ECHO' }, color: '#22cc22' }, + { id: 'pcs-trig', start: { componentId: 'nano-rp2040', pinName: 'D5' }, end: { componentId: 'pico-sr1', pinName: 'TRIG' }, color: '#ff8800' }, + { id: 'pcs-echo', start: { componentId: 'nano-rp2040', pinName: 'D6' }, end: { componentId: 'pico-sr1', pinName: 'ECHO' }, color: '#22cc22' }, ], }, { id: 'pico-pir', title: 'Pico: PIR Motion Detector', - description: 'Detect movement with a PIR sensor on GP14. The built-in LED (GP25) activates when motion is detected.', + description: 'Detect movement with a PIR sensor on D4 (GP16). The built-in LED (GP25) activates when motion is detected.', category: 'sensors', difficulty: 'beginner', boardType: 'raspberry-pi-pico', boardFilter: 'raspberry-pi-pico', code: `// Raspberry Pi Pico — PIR Motion Sensor -// Wiring: OUT → GP14 | VCC → 3.3V | GND → GND +// Wiring: OUT → D4(GP16) | VCC → 3.3V | GND → GND -#define PIR_PIN 14 // GPIO 14 +#define PIR_PIN 16 // GPIO 16 (D4) #define LED_PIN 25 // on-board LED (LED_BUILTIN on Pico) bool prevMotion = false; @@ -3557,24 +3557,24 @@ void loop() { wires: [ { id: 'pp-vcc', start: { componentId: 'nano-rp2040', pinName: '3.3V' }, end: { componentId: 'pico-pir1', pinName: 'VCC' }, color: '#ff4444' }, { id: 'pp-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pico-pir1', pinName: 'GND' }, color: '#000000' }, - { id: 'pp-out', start: { componentId: 'nano-rp2040', pinName: 'GP14' }, end: { componentId: 'pico-pir1', pinName: 'OUT' }, color: '#ffcc00' }, + { id: 'pp-out', start: { componentId: 'nano-rp2040', pinName: 'D4' }, end: { componentId: 'pico-pir1', pinName: 'OUT' }, color: '#ffcc00' }, ], }, { id: 'pico-servo', title: 'Pico: Servo Motor Sweep', - description: 'Sweep a servo motor from 0° to 180° and back on the Raspberry Pi Pico using GP15 (PWM).', + description: 'Sweep a servo motor from 0° to 180° and back on the Raspberry Pi Pico using D3 / GP15 (PWM).', category: 'robotics', difficulty: 'beginner', boardType: 'raspberry-pi-pico', boardFilter: 'raspberry-pi-pico', code: `// Raspberry Pi Pico — Servo Motor Sweep -// Wiring: PWM → GP15 | V+ → 3.3V | GND → GND +// Wiring: PWM → D3(GP15) | V+ → 3.3V | GND → GND // Uses built-in Servo library #include -#define SERVO_PIN 15 // GPIO 15 +#define SERVO_PIN 15 // GPIO 15 (D3) Servo myServo; @@ -3596,7 +3596,7 @@ void loop() { wires: [ { id: 'psv-vcc', start: { componentId: 'nano-rp2040', pinName: '3.3V' }, end: { componentId: 'pico-sv1', pinName: 'V+' }, color: '#ff4444' }, { id: 'psv-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pico-sv1', pinName: 'GND' }, color: '#000000' }, - { id: 'psv-pwm', start: { componentId: 'nano-rp2040', pinName: 'GP15' }, end: { componentId: 'pico-sv1', pinName: 'PWM' }, color: '#ff8800' }, + { id: 'psv-pwm', start: { componentId: 'nano-rp2040', pinName: 'D3' }, end: { componentId: 'pico-sv1', pinName: 'PWM' }, color: '#ff8800' }, ], }, { @@ -3649,18 +3649,18 @@ void loop() { { id: 'pico-joystick', title: 'Pico: Analog Joystick', - description: 'Read X/Y axes and button press from an analog joystick. VERT on A0 (GP26), HORZ on A1 (GP27), SEL button on GP14.', + description: 'Read X/Y axes and button press from an analog joystick. VERT on A0 (GP26), HORZ on A1 (GP27), SEL button on D4 (GP16).', category: 'sensors', difficulty: 'beginner', boardType: 'raspberry-pi-pico', boardFilter: 'raspberry-pi-pico', code: `// Raspberry Pi Pico — Analog Joystick -// Wiring: VERT → A0(GP26) | HORZ → A1(GP27) | SEL → GP14 +// Wiring: VERT → A0(GP26) | HORZ → A1(GP27) | SEL → D4(GP16) // VCC → 3.3V | GND → GND #define JOY_VERT A0 // GP26 #define JOY_HORZ A1 // GP27 -#define JOY_BTN 14 // GP14 +#define JOY_BTN 16 // GP16 (D4) void setup() { Serial.begin(115200); @@ -3691,7 +3691,7 @@ void loop() { { id: 'pj-gnd', start: { componentId: 'nano-rp2040', pinName: 'GND.1' }, end: { componentId: 'pico-joy1', pinName: 'GND' }, color: '#000000' }, { id: 'pj-vert', start: { componentId: 'nano-rp2040', pinName: 'A0' }, end: { componentId: 'pico-joy1', pinName: 'VERT' }, color: '#22aaff' }, { id: 'pj-horz', start: { componentId: 'nano-rp2040', pinName: 'A1' }, end: { componentId: 'pico-joy1', pinName: 'HORZ' }, color: '#22cc44' }, - { id: 'pj-sel', start: { componentId: 'nano-rp2040', pinName: 'GP14' }, end: { componentId: 'pico-joy1', pinName: 'SEL' }, color: '#aa44ff' }, + { id: 'pj-sel', start: { componentId: 'nano-rp2040', pinName: 'D4' }, end: { componentId: 'pico-joy1', pinName: 'SEL' }, color: '#aa44ff' }, ], }, diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index 72566c3..5c7e259 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -232,10 +232,10 @@ export class RP2040Simulator { const isHigh = state === GPIOPinState.High; this.pinManager.triggerPinChange(pin, isHigh); if (this.onPinChangeWithTime && this.rp2040) { - // Use clock cycles if available, otherwise 0 + // IClock interface exposes `nanos` (not `timeUs`) // eslint-disable-next-line @typescript-eslint/no-explicit-any const clk = (this.rp2040 as any).clock; - const timeMs = clk ? clk.timeUs / 1000 : 0; + const timeMs = clk ? (clk.nanos as number) / 1_000_000 : 0; this.onPinChangeWithTime(pin, isHigh, timeMs); } }); diff --git a/frontend/src/simulation/parts/ComplexParts.ts b/frontend/src/simulation/parts/ComplexParts.ts index e0071be..20e0112 100644 --- a/frontend/src/simulation/parts/ComplexParts.ts +++ b/frontend/src/simulation/parts/ComplexParts.ts @@ -274,26 +274,72 @@ PartSimulationRegistry.register('servo', { const MAX_PULSE_US = 2400; const CPU_HZ = 16_000_000; - // ── Primary: cycle-accurate pulse width measurement ──────────────── + // ── RP2040 path: read PWM CC/TOP registers directly ──────────────── + // Arduino-Pico Servo library uses analogWriteFreq(50) + analogWriteRange(20000) + // Each PWM slice CC value / (TOP+1) * 20000 gives pulse width in µs. + // GPIO n → slice = n>>1, channel A (even) or B (odd) of `pwm.channels[slice].cc`. + if (avrSimulator instanceof RP2040Simulator && pinSIG !== null) { + const rp2040 = (avrSimulator as unknown as Record).rp2040 as Record | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pwmChannels = (rp2040 as any)?.pwm?.channels as Array<{ cc: number; top: number }> | undefined; + if (pwmChannels) { + const slice = pinSIG >> 1; + const isChannelB = (pinSIG & 1) === 1; + let lastPulseUs = -1; + let rafId: number | null = null; + + const pollPWM = () => { + if (avrSimulator.isRunning()) { + const ch = pwmChannels[slice]; + if (ch && ch.top > 0) { + const ccRaw = ch.cc; + const ccVal = isChannelB ? (ccRaw >>> 16) & 0xffff : ccRaw & 0xffff; + // Servo period is 20ms (50 Hz via analogWriteFreq(50)) + const pulseUs = Math.round((ccVal / (ch.top + 1)) * 20_000); + if (pulseUs !== lastPulseUs) { + lastPulseUs = pulseUs; + 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; + } + } + } + } + rafId = requestAnimationFrame(pollPWM); + }; + + rafId = requestAnimationFrame(pollPWM); + return () => { if (rafId !== null) cancelAnimationFrame(rafId); }; + } + } + + // ── AVR primary: cycle-accurate pulse width measurement ──────────── if (pinSIG !== null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const pinManager = (avrSimulator as any).pinManager as import('../PinManager').PinManager | undefined; if (pinManager) { - let riseTime = -1; // cpu.cycles at last rising edge + let riseTime = -1; // cycle count at last rising edge + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getCycles = () => typeof (avrSimulator as any).getCurrentCycles === 'function' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ? (avrSimulator as any).getCurrentCycles() as number + // eslint-disable-next-line @typescript-eslint/no-explicit-any + : ((avrSimulator as any).cpu?.cycles ?? 0) as number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const clockHz = typeof (avrSimulator as any).getClockHz === 'function' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ? (avrSimulator as any).getClockHz() as number + : CPU_HZ; 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; + riseTime = getCycles(); } else if (riseTime >= 0) { - // Falling edge — compute pulse width in µs - const pulseCycles = cpu.cycles - riseTime; - const pulseUs = (pulseCycles / CPU_HZ) * 1_000_000; + const pulseCycles = getCycles() - riseTime; + const pulseUs = (pulseCycles / clockHz) * 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 diff --git a/frontend/src/utils/pinPositionCalculator.ts b/frontend/src/utils/pinPositionCalculator.ts index 04325e6..2dfdddb 100644 --- a/frontend/src/utils/pinPositionCalculator.ts +++ b/frontend/src/utils/pinPositionCalculator.ts @@ -47,6 +47,14 @@ export function calculatePinPosition( if (!pin && !pinName.includes('.')) { pin = pinInfo.find((p: any) => p.name === `${pinName}.1`); } + // Fallback: GP-prefix → match description field (e.g. 'GP15' → description 'GPIO15') + // Needed for Nano RP2040 Connect which uses D-prefix pin names but GPIO descriptions + if (!pin && pinName.startsWith('GP')) { + const gpioNum = parseInt(pinName.substring(2), 10); + if (!isNaN(gpioNum)) { + pin = pinInfo.find((p: any) => p.description === `GPIO${gpioNum}`); + } + } if (!pin) { console.warn(`[pinPositionCalculator] Pin ${pinName} not found on component ${componentId}`); console.warn(`Available pins:`, pinInfo.map((p: any) => p.name));