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.
*/
PartSimulationRegistry.register('rgb-led', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
attachEvents: (element, avrSimulator, getArduinoPinHelper, _componentId, getPinResolver) => {
const pinManager = (avrSimulator as any).pinManager;
if (!pinManager) return () => {};
const el = element as any;
const unsubscribers: (() => void)[] = [];
const useResolver = typeof getPinResolver === 'function';
const pinR = getArduinoPinHelper('R');
const pinG = getArduinoPinHelper('G');
const pinB = getArduinoPinHelper('B');
// Digital fallback
if (pinR !== null) {
unsubscribers.push(
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' },
// Digital path: prefer PinResolver so each channel works when driven
// through an active device (e.g. a P-MOSFET high-side switch).
type Channel = { pinName: 'R' | 'G' | 'B'; prop: 'ledRed' | 'ledGreen' | 'ledBlue' };
const channels: Channel[] = [
{ pinName: 'R', prop: 'ledRed' },
{ pinName: 'G', prop: 'ledGreen' },
{ pinName: 'B', prop: 'ledBlue' },
];
for (const { pin, prop } of pwmPins) {
if (pin !== null) {
unsubscribers.push(
pinManager.onPwmChange(pin, (_: number, dc: number) => {
el[prop] = Math.round(dc * 255);
}),
);
// Track Arduino pin numbers for the PWM hook below — analogWrite()
// override still needs the integer pin number because PinResolver
// doesn't (yet) expose PWM duty.
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());
@ -482,10 +490,16 @@ PartSimulationRegistry.register('servo', {
* Activates when duty cycle > 0 (pin is driven HIGH).
*/
PartSimulationRegistry.register('buzzer', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
attachEvents: (element, avrSimulator, getArduinoPinHelper, _componentId, getPinResolver) => {
const pinSIG =
getArduinoPinHelper('1') ?? getArduinoPinHelper('+') ?? getArduinoPinHelper('POS');
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 oscillator: OscillatorNode | null = null;
@ -568,19 +582,33 @@ PartSimulationRegistry.register('buzzer', {
}),
);
// Also respond to digital HIGH/LOW (tone() toggles the pin)
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)));
} else if (isSounding && !state) {
// Don't stop on every LOW — tone() generates a square wave
// We stop only when duty cycle drops to 0 via onPwmChange
}
}),
);
// Also respond to digital HIGH/LOW (tone() toggles the pin).
// Prefer the resolver — a buzzer driven through a transistor sees
// the real collector voltage and threshold-converts via the board
// logic family.
if (sigResolver) {
unsubscribers.push(
sigResolver.onChange((state) => {
if (!isSounding && state === 'HIGH') {
const cpu = (avrSimulator as any).cpu;
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 () => {