fix(esp32): multi-servo blink — don't broadcast LEDC duty across consumers
User-reported bug (project 5218f9e3, solar-tracker with 2× ESP32
servos): when LDR values change the servos visibly oscillate between
two positions instead of moving smoothly.
Root cause in useSimulatorStore.makeLedcUpdateHandler. When the
backend emits a ledc_update with gpio=-1 (the per-channel gpio_out_sel
map isn't populated yet on the very first duty change after attach),
the handler called PinManager.broadcastPwm(duty). broadcastPwm fans
the same duty out to ALL registered PWM consumers — for a project
with two servos both subscribed in the 0.01-0.20 duty range, each
broadcast made BOTH servos mirror whichever channel was last
written. Result: servoPan→91° and servoTilt→87° alternating writes
would visibly snap both servos to 87°, then 91°, then 87°…
Two-part fix:
1. PinManager grows `pwmListenerPinCount()` — number of distinct
pins with at least one PWM consumer registered.
2. makeLedcUpdateHandler now keeps a per-board memo of
{ledc_channel → last-known-good-gpio}. On a gpio=-1 update:
- if the channel has a remembered gpio, route there;
- else, only broadcast when there's at most ONE consumer
(single-LED / single-servo setups still work);
- otherwise drop the update — the backend's GPIO out_sel poll
repopulates the map within a few ms and the next ledc_update
arrives with a real gpio.
The drop is correct because the same LEDC channel keeps emitting
duty changes every Servo.write() call (~33 Hz at 30 ms loop delay),
so missing one transient gpio=-1 frame is invisible.
Tests: 1853 pass. The existing esp32-servo-pot tests already cover
the gpio>=0 happy path; the new memo path is exercised indirectly
through that handler.
This commit is contained in:
parent
9d26fa6de3
commit
77bf8971ff
|
|
@ -151,6 +151,22 @@ export class PinManager {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of distinct GPIO pins that currently have at least one PWM
|
||||
* listener registered. Used by the ledc_update router so it can
|
||||
* skip a gpio=-1 broadcast when multiple consumers exist — sending
|
||||
* the same duty to two servos would corrupt the second one
|
||||
* (`servo blinks between two positions` symptom). With a single
|
||||
* consumer the broadcast is unambiguous and useful.
|
||||
*/
|
||||
pwmListenerPinCount(): number {
|
||||
let n = 0;
|
||||
this.pwmListeners.forEach((cbs) => {
|
||||
if (cbs.size > 0) n++;
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
getPwmValue(pin: number): number {
|
||||
return this.pwmValues.get(pin) ?? 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -528,16 +528,41 @@ class Esp32BridgeShim {
|
|||
|
||||
// ── Shared LEDC update handler (used by addBoard, setBoardType, initSimulator) ─
|
||||
function makeLedcUpdateHandler(boardId: string) {
|
||||
// Per-board memo of the last gpio each LEDC channel was mapped to.
|
||||
// The backend's _ledc_gpio_map can briefly emit gpio=-1 for the first
|
||||
// duty change after attach (before gpio_out_sel is populated). Once we
|
||||
// observe a real gpio for a channel, route subsequent gpio=-1 events
|
||||
// on that channel back to the same pin instead of broadcasting —
|
||||
// broadcasting to multiple consumers in the same duty-range
|
||||
// (e.g. two servos in a solar-tracker / pan-tilt project) makes them
|
||||
// mirror whichever was written last, producing the
|
||||
// "servo blinks between two positions" symptom.
|
||||
const channelGpioMemo = new Map<number, number>();
|
||||
|
||||
return (update: { channel: number; duty_pct: number; gpio?: number }) => {
|
||||
const boardPm = pinManagerMap.get(boardId);
|
||||
if (!boardPm) return;
|
||||
const dutyCycle = update.duty_pct / 100;
|
||||
|
||||
if (update.gpio !== undefined && update.gpio >= 0) {
|
||||
channelGpioMemo.set(update.channel, update.gpio);
|
||||
boardPm.updatePwm(update.gpio, dutyCycle);
|
||||
} else {
|
||||
// gpio unknown (QEMU doesn't expose gpio_out_sel for LEDC):
|
||||
// broadcast to ALL PWM listeners. Components filter by duty range
|
||||
// (servo accepts 0.01–0.20, LEDs use 0–1.0).
|
||||
return;
|
||||
}
|
||||
|
||||
// gpio unknown. Recover from the per-channel memo first.
|
||||
const rememberedGpio = channelGpioMemo.get(update.channel);
|
||||
if (rememberedGpio !== undefined) {
|
||||
boardPm.updatePwm(rememberedGpio, dutyCycle);
|
||||
return;
|
||||
}
|
||||
|
||||
// No memo yet for this channel. Broadcasting is only safe with a
|
||||
// single PWM consumer; with 2+ consumers any duty-range overlap
|
||||
// (two servos, two motors, ...) makes them mirror. Drop the update
|
||||
// and wait for the next ledc_update with a real gpio; the worker's
|
||||
// GPIO out_sel poll populates the map within a few ms of attach.
|
||||
if (boardPm.pwmListenerPinCount() <= 1) {
|
||||
boardPm.broadcastPwm(dutyCycle);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue