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.