2026-03-03 10:20:49 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* PinManager - Manages Arduino pin states and notifies listeners
|
|
|
|
|
|
*
|
2026-03-09 20:08:14 +07:00
|
|
|
|
* Maps AVR PORT registers to Arduino pin numbers.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Arduino Uno / Nano (ATmega328P):
|
2026-03-03 10:20:49 +07:00
|
|
|
|
* - PORTB (0x25) → Digital pins 8-13
|
|
|
|
|
|
* - PORTC (0x28) → Analog pins A0-A5 (14-19)
|
|
|
|
|
|
* - PORTD (0x2B) → Digital pins 0-7
|
2026-03-05 04:27:14 +07:00
|
|
|
|
*
|
2026-03-09 20:08:14 +07:00
|
|
|
|
* Arduino Mega 2560 (ATmega2560): uses explicit per-bit pin maps
|
|
|
|
|
|
* for non-linear port ↔ Arduino-pin relationships.
|
|
|
|
|
|
*
|
2026-03-05 04:27:14 +07:00
|
|
|
|
* Also supports:
|
|
|
|
|
|
* - Analog voltage injection (for potentiometers, sensors)
|
|
|
|
|
|
* - PWM duty cycle tracking (for servos, RGB LEDs, buzzers)
|
2026-03-03 10:20:49 +07:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
export type PinState = boolean;
|
|
|
|
|
|
export type PinChangeCallback = (pin: number, state: PinState) => void;
|
2026-03-05 04:27:14 +07:00
|
|
|
|
export type AnalogCallback = (pin: number, voltage: number) => void;
|
|
|
|
|
|
export type PwmCallback = (pin: number, dutyCycle: number) => void;
|
2026-03-03 10:20:49 +07:00
|
|
|
|
|
|
|
|
|
|
export class PinManager {
|
|
|
|
|
|
private listeners: Map<number, Set<PinChangeCallback>> = new Map();
|
2026-03-05 04:27:14 +07:00
|
|
|
|
private pwmListeners: Map<number, Set<PwmCallback>> = new Map();
|
|
|
|
|
|
private analogListeners: Map<number, Set<AnalogCallback>> = new Map();
|
2026-03-03 10:20:49 +07:00
|
|
|
|
private pinStates: Map<number, boolean> = new Map();
|
2026-03-05 04:27:14 +07:00
|
|
|
|
private pwmValues: Map<number, number> = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Digital pin API ──────────────────────────────────────────────────────
|
2026-03-03 10:20:49 +07:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-05 04:27:14 +07:00
|
|
|
|
* Register callback for digital pin state changes.
|
|
|
|
|
|
* Returns unsubscribe function.
|
2026-03-03 10:20:49 +07:00
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-05 04:27:14 +07:00
|
|
|
|
* Update port register and notify digital pin listeners.
|
2026-03-09 20:08:14 +07:00
|
|
|
|
*
|
|
|
|
|
|
* @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.
|
2026-03-03 10:20:49 +07:00
|
|
|
|
*/
|
2026-03-09 20:08:14 +07:00
|
|
|
|
updatePort(portName: string, newValue: number, oldValue: number = 0, pinMap?: number[]) {
|
|
|
|
|
|
const legacyOffsets: Record<string, number> = { 'PORTB': 8, 'PORTC': 14, 'PORTD': 0 };
|
2026-03-03 10:20:49 +07:00
|
|
|
|
|
|
|
|
|
|
for (let bit = 0; bit < 8; bit++) {
|
|
|
|
|
|
const mask = 1 << bit;
|
|
|
|
|
|
const oldState = (oldValue & mask) !== 0;
|
|
|
|
|
|
const newState = (newValue & mask) !== 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (oldState !== newState) {
|
2026-03-09 20:08:14 +07:00
|
|
|
|
const arduinoPin = pinMap ? pinMap[bit] : (legacyOffsets[portName] ?? 0) + bit;
|
|
|
|
|
|
if (arduinoPin < 0) continue; // unmapped bit
|
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
|
this.pinStates.set(arduinoPin, newState);
|
|
|
|
|
|
|
|
|
|
|
|
const callbacks = this.listeners.get(arduinoPin);
|
|
|
|
|
|
if (callbacks) {
|
|
|
|
|
|
callbacks.forEach(cb => cb(arduinoPin, newState));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`Pin ${arduinoPin} (${portName}${bit}): ${oldState ? 'HIGH' : 'LOW'} → ${newState ? 'HIGH' : 'LOW'}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getPinState(arduinoPin: number): boolean {
|
|
|
|
|
|
return this.pinStates.get(arduinoPin) || false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 05:28:33 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 04:27:14 +07:00
|
|
|
|
// ── 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);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
|
/**
|
2026-03-05 04:27:14 +07:00
|
|
|
|
* Called by AVRSimulator each frame when an OCR register changes.
|
2026-03-03 10:20:49 +07:00
|
|
|
|
*/
|
2026-03-05 04:27:14 +07:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
|
getListenersCount(): number {
|
|
|
|
|
|
let count = 0;
|
|
|
|
|
|
this.listeners.forEach(set => count += set.size);
|
|
|
|
|
|
return count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearAllListeners() {
|
|
|
|
|
|
this.listeners.clear();
|
2026-03-05 04:27:14 +07:00
|
|
|
|
this.pwmListeners.clear();
|
|
|
|
|
|
this.analogListeners.clear();
|
2026-03-03 10:20:49 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|