/** * PinManager - Manages Arduino pin states and notifies listeners * * Maps AVR PORT registers to Arduino pin numbers. * * Arduino Uno / Nano (ATmega328P): * - PORTB (0x25) → Digital pins 8-13 * - PORTC (0x28) → Analog pins A0-A5 (14-19) * - PORTD (0x2B) → Digital pins 0-7 * * Arduino Mega 2560 (ATmega2560): uses explicit per-bit pin maps * for non-linear port ↔ Arduino-pin relationships. * * Also supports: * - Analog voltage injection (for potentiometers, sensors) * - PWM duty cycle tracking (for servos, RGB LEDs, buzzers) */ export type PinState = boolean; export type PinChangeCallback = (pin: number, state: PinState) => void; export type AnalogCallback = (pin: number, voltage: number) => void; export type PwmCallback = (pin: number, dutyCycle: number) => void; export class PinManager { private listeners: Map> = new Map(); private pwmListeners: Map> = new Map(); private analogListeners: Map> = new Map(); private pinStates: Map = new Map(); private pwmValues: Map = new Map(); // ── Digital pin API ────────────────────────────────────────────────────── /** * Register callback for digital pin state changes. * Returns unsubscribe function. */ onPinChange(arduinoPin: number, callback: PinChangeCallback): () => void { if (!this.listeners.has(arduinoPin)) { this.listeners.set(arduinoPin, new Set()); } this.listeners.get(arduinoPin)!.add(callback); return () => { this.listeners.get(arduinoPin)?.delete(callback); }; } /** * Update port register and notify digital pin listeners. * * @param portName Human-readable port name for log output (e.g. 'PORTB'). * @param newValue New 8-bit port value. * @param oldValue Previous 8-bit port value (default 0). * @param pinMap Optional per-bit Arduino pin numbers (length 8). * Use -1 for bits that are not exposed as Arduino pins. * When omitted the legacy Uno/Nano fixed offsets are used: * PORTB→8, PORTC→14, PORTD→0. */ updatePort(portName: string, newValue: number, oldValue: number = 0, pinMap?: number[]) { const legacyOffsets: Record = { 'PORTB': 8, 'PORTC': 14, 'PORTD': 0 }; for (let bit = 0; bit < 8; bit++) { const mask = 1 << bit; const oldState = (oldValue & mask) !== 0; const newState = (newValue & mask) !== 0; if (oldState !== newState) { const arduinoPin = pinMap ? pinMap[bit] : (legacyOffsets[portName] ?? 0) + bit; if (arduinoPin < 0) continue; // unmapped bit this.pinStates.set(arduinoPin, newState); const callbacks = this.listeners.get(arduinoPin); if (callbacks) { callbacks.forEach(cb => cb(arduinoPin, newState)); } } } } getPinState(arduinoPin: number): boolean { return this.pinStates.get(arduinoPin) || false; } /** * Set a single pin state and notify listeners. * Alias for triggerPinChange — used by ESP32-C3, RISC-V, and RP2040 simulators. */ setPinState(pin: number, state: boolean): void { this.triggerPinChange(pin, state); } /** * Directly fire pin change callbacks for a specific pin. * Used by RP2040Simulator which has individual GPIO listeners instead of PORT registers. */ triggerPinChange(pin: number, state: boolean): void { const current = this.pinStates.get(pin); if (current === state) return; // no change this.pinStates.set(pin, state); const callbacks = this.listeners.get(pin); if (callbacks) { callbacks.forEach(cb => cb(pin, state)); } } // ── PWM duty cycle API ─────────────────────────────────────────────────── /** * Register callback for PWM duty cycle changes on a pin. * dutyCycle is 0.0–1.0. */ onPwmChange(pin: number, callback: PwmCallback): () => void { if (!this.pwmListeners.has(pin)) { this.pwmListeners.set(pin, new Set()); } this.pwmListeners.get(pin)!.add(callback); return () => { this.pwmListeners.get(pin)?.delete(callback); }; } /** * Called by AVRSimulator each frame when an OCR register changes. */ updatePwm(pin: number, dutyCycle: number): void { this.pwmValues.set(pin, dutyCycle); const callbacks = this.pwmListeners.get(pin); if (callbacks) { callbacks.forEach(cb => cb(pin, dutyCycle)); } } /** * Broadcast PWM duty to ALL registered PWM listeners. * Used when the LEDC channel→GPIO mapping is unknown (gpio=-1). * Components filter by duty range (e.g., servo accepts 0.01-0.20). */ broadcastPwm(dutyCycle: number): void { this.pwmListeners.forEach((callbacks, pin) => { this.pwmValues.set(pin, dutyCycle); callbacks.forEach(cb => cb(pin, dutyCycle)); }); } getPwmValue(pin: number): number { return this.pwmValues.get(pin) ?? 0; } // ── Analog voltage API ─────────────────────────────────────────────────── /** * Register callback when external code sets an analog voltage on a pin. */ onAnalogChange(pin: number, callback: AnalogCallback): () => void { if (!this.analogListeners.has(pin)) { this.analogListeners.set(pin, new Set()); } this.analogListeners.get(pin)!.add(callback); return () => { this.analogListeners.get(pin)?.delete(callback); }; } /** * Inject a simulated analog voltage (0–5V) on an Arduino pin. * Notifies any registered analog listeners. */ setAnalogVoltage(arduinoPin: number, voltage: number): void { const callbacks = this.analogListeners.get(arduinoPin); if (callbacks) { callbacks.forEach(cb => cb(arduinoPin, voltage)); } } // ── Utility ────────────────────────────────────────────────────────────── getListenersCount(): number { let count = 0; this.listeners.forEach(set => count += set.size); return count; } clearAllListeners() { this.listeners.clear(); this.pwmListeners.clear(); this.analogListeners.clear(); } }