feat: enhance simulation accuracy and component interactions across various modules
parent
f6fdae6b0e
commit
dc5dfb8635
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"generatedAt": "2026-03-19T00:06:03.160Z",
|
||||
"generatedAt": "2026-03-20T17:33:38.631Z",
|
||||
"components": [
|
||||
{
|
||||
"id": "arduino-mega",
|
||||
|
|
|
|||
|
|
@ -197,7 +197,8 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
if (logic && logic.attachEvents && simulator) {
|
||||
// Helper to find Arduino pin connected to a component pin
|
||||
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.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 :
|
||||
isBoardComponent(w.end.componentId) ? w.end : null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -454,34 +454,41 @@ export const SimulatorCanvas = () => {
|
|||
|
||||
// Helper to add subscription
|
||||
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(
|
||||
pin,
|
||||
(_pin, state) => {
|
||||
// 1. Update React state for standard properties
|
||||
if (!hasSelfManagedVisuals) {
|
||||
// 1. Update React state for standard properties (LEDs, buttons, etc.)
|
||||
updateComponentState(component.id, state);
|
||||
}
|
||||
|
||||
// 2. Delegate to PartSimulationRegistry for custom visual updates
|
||||
const logic = PartSimulationRegistry.get(component.metadataId);
|
||||
if (logic && logic.onPinStateChange) {
|
||||
const el = document.getElementById(component.id);
|
||||
if (el) {
|
||||
logic.onPinStateChange(componentPinName || 'A', state, el);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Component ${component.id} on pin ${pin}: ${state ? 'HIGH' : 'LOW'}`);
|
||||
}
|
||||
);
|
||||
unsubscribers.push(unsubscribe);
|
||||
|
||||
// PWM subscription: update LED opacity when the pin receives a LEDC duty cycle.
|
||||
// duty=0 means no PWM / analogWrite(0) — clear the inline style so the
|
||||
// component keeps its default visibility instead of becoming invisible.
|
||||
// PWM subscription: update LED opacity when the pin receives a PWM duty cycle.
|
||||
// Skip for self-managed components (servo, buzzer) — their duty cycle is a
|
||||
// control signal, not a brightness value, so setting opacity would cause flicker.
|
||||
if (!hasSelfManagedVisuals) {
|
||||
const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => {
|
||||
const el = document.getElementById(component.id);
|
||||
if (el) el.style.opacity = duty > 0 ? String(duty) : '';
|
||||
});
|
||||
unsubscribers.push(pwmUnsub);
|
||||
}
|
||||
};
|
||||
|
||||
components.forEach((component) => {
|
||||
|
|
|
|||
|
|
@ -309,6 +309,11 @@ export class AVRSimulator {
|
|||
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.
|
||||
* Used by timing-sensitive peripherals to schedule future pin changes.
|
||||
|
|
|
|||
|
|
@ -73,8 +73,6 @@ export class PinManager {
|
|||
if (callbacks) {
|
||||
callbacks.forEach(cb => cb(arduinoPin, newState));
|
||||
}
|
||||
|
||||
console.log(`Pin ${arduinoPin} (${portName}${bit}): ${oldState ? 'HIGH' : 'LOW'} → ${newState ? 'HIGH' : 'LOW'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ export class RP2040Simulator {
|
|||
private speed = 1.0;
|
||||
private gpioUnsubscribers: Array<() => void> = [];
|
||||
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 */
|
||||
public onSerialData: ((char: string) => void) | null = null;
|
||||
|
|
@ -269,7 +271,10 @@ export class RP2040Simulator {
|
|||
const jump: number = clock.nanosToNextAlarm;
|
||||
if (jump <= 0) break; // no pending alarms
|
||||
clock.tick(jump);
|
||||
cyclesDone += Math.ceil(jump / CYCLE_NANOS);
|
||||
const jumped = Math.ceil(jump / CYCLE_NANOS);
|
||||
cyclesDone += jumped;
|
||||
this.totalCycles += jumped;
|
||||
this.flushScheduledPinChanges();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -277,6 +282,8 @@ export class RP2040Simulator {
|
|||
const cycles: number = core.executeInstruction();
|
||||
if (clock) clock.tick(cycles * CYCLE_NANOS);
|
||||
cyclesDone += cycles;
|
||||
this.totalCycles += cycles;
|
||||
this.flushScheduledPinChanges();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -308,6 +315,8 @@ export class RP2040Simulator {
|
|||
|
||||
reset(): void {
|
||||
this.stop();
|
||||
this.totalCycles = 0;
|
||||
this.scheduledPinChanges = [];
|
||||
if (this.rp2040 && this.flashCopy) {
|
||||
this.initMCU(this.flashCopy);
|
||||
// Re-register any previously added I2C devices
|
||||
|
|
@ -328,6 +337,34 @@ export class RP2040Simulator {
|
|||
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).
|
||||
* GPIO n = Arduino D(n) for Raspberry Pi Pico.
|
||||
|
|
|
|||
|
|
@ -184,23 +184,29 @@ PartSimulationRegistry.register('photoresistor-sensor', {
|
|||
*/
|
||||
PartSimulationRegistry.register('analog-joystick', {
|
||||
attachEvents: (element, avrSimulator, getArduinoPinHelper, componentId) => {
|
||||
const pinX = getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT');
|
||||
const pinY = getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT');
|
||||
const pinSW = getArduinoPinHelper('SW');
|
||||
// wokwi-analog-joystick uses VERT/HORZ/SEL pin names
|
||||
const pinX = getArduinoPinHelper('VERT') ?? getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT');
|
||||
const pinY = getArduinoPinHelper('HORZ') ?? getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT');
|
||||
const pinSW = getArduinoPinHelper('SEL') ?? getArduinoPinHelper('SW');
|
||||
const el = element as any;
|
||||
|
||||
// Center position is mid-range (~2.5V)
|
||||
if (pinX !== null) setAdcVoltage(avrSimulator, pinX, 2.5);
|
||||
if (pinY !== null) setAdcVoltage(avrSimulator, pinY, 2.5);
|
||||
// RP2040 uses 3.3V reference; AVR uses 5V
|
||||
const vcc = avrSimulator instanceof RP2040Simulator ? 3.3 : 5.0;
|
||||
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 = () => {
|
||||
// xValue / yValue are 0-1023
|
||||
if (pinX !== null) {
|
||||
const vx = ((el.xValue ?? 512) / 1023.0) * 5.0;
|
||||
const vx = ((el.xValue ?? 512) / 1023.0) * vcc;
|
||||
setAdcVoltage(avrSimulator, pinX, vx);
|
||||
}
|
||||
if (pinY !== null) {
|
||||
const vy = ((el.yValue ?? 512) / 1023.0) * 5.0;
|
||||
const vy = ((el.yValue ?? 512) / 1023.0) * vcc;
|
||||
setAdcVoltage(avrSimulator, pinY, vy);
|
||||
}
|
||||
};
|
||||
|
|
@ -219,13 +225,13 @@ PartSimulationRegistry.register('analog-joystick', {
|
|||
element.addEventListener('button-press', onPress);
|
||||
element.addEventListener('button-release', onRelease);
|
||||
|
||||
// SensorControlPanel: xAxis/yAxis -512..512 → voltage 0–5V (center = 2.5V)
|
||||
// SensorControlPanel: xAxis/yAxis -512..512 → voltage 0–VCC (center = VCC/2)
|
||||
registerSensorUpdate(componentId, (values) => {
|
||||
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) {
|
||||
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 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:
|
||||
* - 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 544–2400 µs, NOT the generic 1000–2000 µs range)
|
||||
*
|
||||
* With Timer1, prescaler=8, F_CPU=16MHz:
|
||||
* - ICR1 = 20000 for 50Hz
|
||||
* - OCR1A = 1000 → 0°, 1500 → 90°, 2000 → 180°
|
||||
* Approach: subscribe to the servo's PWM pin state changes, record the CPU
|
||||
* cycle count at the rising edge, then compute pulse width on the falling edge.
|
||||
* 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', {
|
||||
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
|
||||
const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1');
|
||||
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
|
||||
const OCR1AL = 0x88;
|
||||
const OCR1AH = 0x89;
|
||||
const ICR1L = 0x86;
|
||||
const ICR1H = 0x87;
|
||||
const SERVO_PERIOD_US = 20000;
|
||||
|
||||
let rafId: number | null = null;
|
||||
let lastOcr1a = -1;
|
||||
|
||||
const poll = () => {
|
||||
if (!avrSimulator.isRunning()) { rafId = requestAnimationFrame(poll); return; }
|
||||
|
||||
const cpu = (avrSimulator as any).cpu;
|
||||
if (!cpu) { rafId = requestAnimationFrame(poll); return; }
|
||||
|
||||
|
|
@ -278,39 +330,19 @@ PartSimulationRegistry.register('servo', {
|
|||
lastOcr1a = ocr1a;
|
||||
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;
|
||||
if (icr1 > 0) {
|
||||
// Proportional to ICR1 period (assume 20ms period)
|
||||
pulseUs = 1000 + (ocr1a / icr1) * 1000;
|
||||
pulseUs = (ocr1a / icr1) * SERVO_PERIOD_US;
|
||||
} else {
|
||||
// Fallback: prescaler 8
|
||||
// prescaler 8, 16MHz → 0.5µs per tick
|
||||
pulseUs = ocr1a * 0.5;
|
||||
}
|
||||
|
||||
// Clamp to 1000-2000µs and map to 0-180°
|
||||
const clamped = Math.max(1000, Math.min(2000, pulseUs));
|
||||
const angle = Math.round(((clamped - 1000) / 1000) * 180);
|
||||
const clamped = Math.max(MIN_PULSE_US, Math.min(MAX_PULSE_US, pulseUs));
|
||||
const angle = Math.round(((clamped - MIN_PULSE_US) / (MAX_PULSE_US - MIN_PULSE_US)) * 180);
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -415,31 +415,41 @@ function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement
|
|||
const now = simulator.getCurrentCycles() as number;
|
||||
|
||||
// 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 HIGH80 = 1280; // 80 µs HIGH preamble
|
||||
const LOW50 = 800; // 50 µs LOW marker before each bit
|
||||
const HIGH0 = 416; // 26 µs HIGH → bit '0'
|
||||
const HIGH1 = 1120; // 70 µs HIGH → bit '1'
|
||||
|
||||
let t = now;
|
||||
let t = now + RESPONSE_START;
|
||||
|
||||
// Preamble: 80 µs LOW then 80 µs HIGH
|
||||
t += LOW80; simulator.schedulePinChange(pin, false, t);
|
||||
t += HIGH80; simulator.schedulePinChange(pin, true, t);
|
||||
// Preamble: 80 µs LOW
|
||||
simulator.schedulePinChange(pin, false, 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 (let b = 7; b >= 0; b--) {
|
||||
const bit = (byte >> b) & 1;
|
||||
t += LOW50; simulator.schedulePinChange(pin, false, t);
|
||||
t += bit ? HIGH1 : HIGH0;
|
||||
simulator.schedulePinChange(pin, false, t);
|
||||
t += LOW50;
|
||||
simulator.schedulePinChange(pin, true, t);
|
||||
t += bit ? HIGH1 : HIGH0;
|
||||
}
|
||||
}
|
||||
|
||||
// Release line HIGH (it already is, but explicit for clarity)
|
||||
t += LOW50; simulator.schedulePinChange(pin, false, t);
|
||||
t += HIGH0; simulator.schedulePinChange(pin, true, t);
|
||||
// Final release
|
||||
simulator.schedulePinChange(pin, false, t);
|
||||
t += LOW50;
|
||||
simulator.schedulePinChange(pin, true, t);
|
||||
}
|
||||
|
||||
PartSimulationRegistry.register('dht22', {
|
||||
|
|
@ -449,10 +459,26 @@ PartSimulationRegistry.register('dht22', {
|
|||
if (pin === null) return () => {};
|
||||
|
||||
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(
|
||||
pin,
|
||||
(_: 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) {
|
||||
// MCU drove DATA LOW — start signal detected
|
||||
wasLow = true;
|
||||
|
|
@ -461,6 +487,8 @@ PartSimulationRegistry.register('dht22', {
|
|||
if (wasLow) {
|
||||
// MCU released DATA HIGH — begin DHT22 response
|
||||
wasLow = false;
|
||||
const cur = getCycles();
|
||||
responseEndCycle = cur >= 0 ? cur + RESPONSE_GATE_CYCLES : 0;
|
||||
scheduleDHT22Response(simulator, pin, element);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -642,12 +642,15 @@ PartSimulationRegistry.register('hc-sr04', {
|
|||
// - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s
|
||||
// (17150 cm/s = speed of sound, one-way = round-trip/2)
|
||||
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 processingCycles = 9600; // ~600 µs sensor overhead
|
||||
const echoCycles = Math.round((distanceCm / 17150) * 16_000_000);
|
||||
const processingCycles = Math.round(600e-6 * clockHz); // 600 µs sensor overhead
|
||||
const echoCycles = Math.round((distanceCm / 17150) * clockHz);
|
||||
simulator.schedulePinChange(echoPin, true, now + processingCycles);
|
||||
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 {
|
||||
// Fallback: best-effort async (works with delay()-based sketches, not pulseIn)
|
||||
const echoMs = Math.max(1, distanceCm / 17.15);
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ export function isBoardComponent(componentId: string): boolean {
|
|||
*/
|
||||
export function boardPinToNumber(boardId: string, pinName: string): number | null {
|
||||
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)
|
||||
const num = parseInt(pinName, 10);
|
||||
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') {
|
||||
// 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.)
|
||||
const mapped = NANO_RP2040_PIN_MAP[pinName];
|
||||
if (mapped !== undefined) return mapped;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ export function calculatePinPosition(
|
|||
}
|
||||
|
||||
// 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) {
|
||||
console.warn(`[pinPositionCalculator] Pin ${pinName} not found on component ${componentId}`);
|
||||
console.warn(`Available pins:`, pinInfo.map((p: any) => p.name));
|
||||
|
|
|
|||
Loading…
Reference in New Issue