velxio/frontend/src/simulation/PinManager.ts

189 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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<number, Set<PinChangeCallback>> = new Map();
private pwmListeners: Map<number, Set<PwmCallback>> = new Map();
private analogListeners: Map<number, Set<AnalogCallback>> = new Map();
private pinStates: Map<number, boolean> = new Map();
private pwmValues: Map<number, number> = 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<string, number> = { '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.01.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 (05V) 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();
}
}