feat(sim): Phase 5 — migrate RGB LED + buzzer handlers to PinResolver

RGB LED: each of R/G/B channels prefers the resolver subscription so
the LED works correctly when fed through a P-MOSFET high-side switch
or a BJT driver.  PWM override (analogWrite) keeps using the integer
pin number through pinManager.onPwmChange — duty cycle handling isn't
yet exposed on PinResolver.

Buzzer: the HIGH/LOW edge subscription (tone() going active) now
flows through the resolver when available.  Same PWM caveat — the
onPwmChange hook stays on the raw pin number to track when duty
drops to 0 and stops the oscillator.

Both fall back to pinManager.onPinChange when the resolver isn't
provided (tests / Phase-0-less builds).

Phase 5 progress: 19 of ~22 handlers migrated. Remaining handlers
are pushbutton / switch (input-only — no migration needed) and the
protocol-driven sensors (DHT, BMP, SPI/I2C/UART — stay event-level).
This is effectively the migration plateau.

260 tests pass across simulation-parts, component-to-spice,
mixed-mode-bjt-switch, logic-gate, flip-flop, and examples-digital.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-15 18:25:40 +02:00
parent bffa8fd814
commit 5ae9fbb615
1 changed files with 82 additions and 54 deletions

View File

@ -13,54 +13,62 @@ import { registerSensorUpdate, unregisterSensorUpdate } from '../SensorUpdateReg
* Falls back to digital mode if no PWM is detected. * Falls back to digital mode if no PWM is detected.
*/ */
PartSimulationRegistry.register('rgb-led', { PartSimulationRegistry.register('rgb-led', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => { attachEvents: (element, avrSimulator, getArduinoPinHelper, _componentId, getPinResolver) => {
const pinManager = (avrSimulator as any).pinManager; const pinManager = (avrSimulator as any).pinManager;
if (!pinManager) return () => {}; if (!pinManager) return () => {};
const el = element as any; const el = element as any;
const unsubscribers: (() => void)[] = []; const unsubscribers: (() => void)[] = [];
const useResolver = typeof getPinResolver === 'function';
const pinR = getArduinoPinHelper('R'); // Digital path: prefer PinResolver so each channel works when driven
const pinG = getArduinoPinHelper('G'); // through an active device (e.g. a P-MOSFET high-side switch).
const pinB = getArduinoPinHelper('B'); type Channel = { pinName: 'R' | 'G' | 'B'; prop: 'ledRed' | 'ledGreen' | 'ledBlue' };
const channels: Channel[] = [
// Digital fallback { pinName: 'R', prop: 'ledRed' },
if (pinR !== null) { { pinName: 'G', prop: 'ledGreen' },
unsubscribers.push( { pinName: 'B', prop: 'ledBlue' },
pinManager.onPinChange(pinR, (_: number, state: boolean) => {
el.ledRed = state ? 255 : 0;
}),
);
}
if (pinG !== null) {
unsubscribers.push(
pinManager.onPinChange(pinG, (_: number, state: boolean) => {
el.ledGreen = state ? 255 : 0;
}),
);
}
if (pinB !== null) {
unsubscribers.push(
pinManager.onPinChange(pinB, (_: number, state: boolean) => {
el.ledBlue = state ? 255 : 0;
}),
);
}
// PWM override — when analogWrite() is used the OCR value supersedes digital
const pwmPins = [
{ pin: pinR, prop: 'ledRed' },
{ pin: pinG, prop: 'ledGreen' },
{ pin: pinB, prop: 'ledBlue' },
]; ];
for (const { pin, prop } of pwmPins) {
if (pin !== null) { // Track Arduino pin numbers for the PWM hook below — analogWrite()
unsubscribers.push( // override still needs the integer pin number because PinResolver
pinManager.onPwmChange(pin, (_: number, dc: number) => { // doesn't (yet) expose PWM duty.
el[prop] = Math.round(dc * 255); const pwmPins: Array<{ pin: number; prop: Channel['prop'] }> = [];
}),
); for (const { pinName, prop } of channels) {
if (useResolver) {
const resolver = getPinResolver!(pinName);
if (resolver) {
el[prop] = resolver.getCurrentState() === 'HIGH' ? 255 : 0;
unsubscribers.push(
resolver.onChange((state) => {
el[prop] = state === 'HIGH' ? 255 : 0;
}),
);
}
} else {
const pin = getArduinoPinHelper(pinName);
if (pin !== null) {
unsubscribers.push(
pinManager.onPinChange(pin, (_: number, state: boolean) => {
el[prop] = state ? 255 : 0;
}),
);
}
} }
// PWM hook still uses the raw pin number — duty cycle handling
// doesn't live in PinResolver yet.
const rawPin = getArduinoPinHelper(pinName);
if (rawPin !== null) pwmPins.push({ pin: rawPin, prop });
}
// PWM override — analogWrite() value supersedes digital state.
for (const { pin, prop } of pwmPins) {
unsubscribers.push(
pinManager.onPwmChange(pin, (_: number, dc: number) => {
el[prop] = Math.round(dc * 255);
}),
);
} }
return () => unsubscribers.forEach((u) => u()); return () => unsubscribers.forEach((u) => u());
@ -482,10 +490,16 @@ PartSimulationRegistry.register('servo', {
* Activates when duty cycle > 0 (pin is driven HIGH). * Activates when duty cycle > 0 (pin is driven HIGH).
*/ */
PartSimulationRegistry.register('buzzer', { PartSimulationRegistry.register('buzzer', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => { attachEvents: (element, avrSimulator, getArduinoPinHelper, _componentId, getPinResolver) => {
const pinSIG = const pinSIG =
getArduinoPinHelper('1') ?? getArduinoPinHelper('+') ?? getArduinoPinHelper('POS'); getArduinoPinHelper('1') ?? getArduinoPinHelper('+') ?? getArduinoPinHelper('POS');
const pinManager = (avrSimulator as any).pinManager; const pinManager = (avrSimulator as any).pinManager;
// PWM tracking still needs the integer pin number; resolver doesn't
// expose duty. The HIGH/LOW path migrates to PinResolver below.
const useResolver = typeof getPinResolver === 'function';
const sigResolver = useResolver
? getPinResolver!('1') ?? getPinResolver!('+') ?? getPinResolver!('POS')
: null;
let audioCtx: AudioContext | null = null; let audioCtx: AudioContext | null = null;
let oscillator: OscillatorNode | null = null; let oscillator: OscillatorNode | null = null;
@ -568,19 +582,33 @@ PartSimulationRegistry.register('buzzer', {
}), }),
); );
// Also respond to digital HIGH/LOW (tone() toggles the pin) // Also respond to digital HIGH/LOW (tone() toggles the pin).
unsubscribers.push( // Prefer the resolver — a buzzer driven through a transistor sees
pinManager.onPinChange(pinSIG, (_: number, state: boolean) => { // the real collector voltage and threshold-converts via the board
if (!isSounding && state) { // logic family.
const cpu = (avrSimulator as any).cpu; if (sigResolver) {
const freq = cpu ? getFrequency(cpu) : 440; unsubscribers.push(
startTone(Math.max(20, Math.min(20000, freq))); sigResolver.onChange((state) => {
} else if (isSounding && !state) { if (!isSounding && state === 'HIGH') {
// Don't stop on every LOW — tone() generates a square wave const cpu = (avrSimulator as any).cpu;
// We stop only when duty cycle drops to 0 via onPwmChange const freq = cpu ? getFrequency(cpu) : 440;
} startTone(Math.max(20, Math.min(20000, freq)));
}), }
); // tone() produces a square wave — don't stop on every LOW;
// stop only when duty drops to 0 via onPwmChange.
}),
);
} else {
unsubscribers.push(
pinManager.onPinChange(pinSIG, (_: number, state: boolean) => {
if (!isSounding && state) {
const cpu = (avrSimulator as any).cpu;
const freq = cpu ? getFrequency(cpu) : 440;
startTone(Math.max(20, Math.min(20000, freq)));
}
}),
);
}
} }
return () => { return () => {