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:
parent
bffa8fd814
commit
5ae9fbb615
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue