fix(esp32): LEDC signal IDs are 71-86 per ESP32 TRM, not 72-87
User report: on the solar-tracker project (5218f9e3) only one servo
moved and the log showed `ch=0 duty=X% gpio=12` (wrong — servoPan was
attached to GPIO 13) and `ch=1 ... gpio=-1` (servoTilt's channel
never resolved).
Root cause traced through the GPIO Matrix dump: the firmware does
exactly what the Arduino-ESP32 Servo library says — `ledcAttachPin(
13, 0)` writes signal 71 (LEDC_HS_SIG_OUT0) into `gpio_out_sel[13]`,
and `ledcAttachPin(12, 1)` writes signal 72 (LEDC_HS_SIG_OUT1) into
`gpio_out_sel[12]`. Per the ESP32 Technical Reference Manual section
4.11, Table 4-3:
71 .. 78 → LEDC HS channels 0..7
79 .. 86 → LEDC LS channels 0..7
The legacy worker code at esp32_worker.py:426 used the off-by-one
range `72 <= signal <= 87` with `ledc_ch = signal - 72`. The mistake
masked itself for single-servo projects because the 0x5000 duty
callback's channel index was internally consistent with the bogus
math, so the duty STILL reached the correctly-routed pin (just
labelled wrong). The new SignalRouter unit tests caught the
discrepancy the moment two servos drove distinct channels: signal
71 (HS_CH0, gpio 13) was REJECTED by the off-by-one filter and
signal 72 (HS_CH1, gpio 12) was misclassified as channel 0.
When I ported the legacy range into `esp32_signals.SIG_LEDC_HS_CH0_OUT_IDX`
the bug came along for the ride. Fix both modules:
* `backend/app/services/esp32_signals.py`: HS 71-78, LS 79-86.
* `frontend/src/simulation/esp32-signals.ts`: mirror.
* tests updated; 20 backend + 23 frontend pass.
After deploy the user's two servos will resolve to their declared
pins:
ch=0 duty=X% gpio=13 (servoPan, was wrongly emitting gpio=12)
ch=1 duty=X% gpio=12 (servoTilt, was wrongly emitting gpio=-1)
This is also why the multi-servo blink "patch" in commit 77bf897
appeared to help: with both pins ALIASED to the same channel via
the off-by-one, the broadcast fallback was the only thing producing
ANY movement on the second servo at all.
This commit is contained in:
parent
f6131d432e
commit
adad446518
|
|
@ -18,12 +18,24 @@ from __future__ import annotations
|
|||
|
||||
|
||||
# ── LEDC (PWM peripheral) ─────────────────────────────────────────────────
|
||||
# High-speed channels (group 0): signals 72-79 → ledc channel 0-7
|
||||
# Low-speed channels (group 1): signals 80-87 → ledc channel 0-7
|
||||
SIG_LEDC_HS_CH0_OUT_IDX = 72 # add N for HS channel N (0..7)
|
||||
SIG_LEDC_HS_CH_LAST = 79
|
||||
SIG_LEDC_LS_CH0_OUT_IDX = 80 # add N for LS channel N (0..7)
|
||||
SIG_LEDC_LS_CH_LAST = 87
|
||||
# Per ESP32 Technical Reference Manual section 4.11, Table 4-3 (GPIO
|
||||
# Matrix output signals):
|
||||
# 71-78 → LEDC_HS_SIG_OUT[0..7] (high-speed channels 0-7)
|
||||
# 79-86 → LEDC_LS_SIG_OUT[0..7] (low-speed channels 0-7)
|
||||
#
|
||||
# The legacy worker code at esp32_worker.py:426 used the off-by-one
|
||||
# range 72-87; that masked itself because the channel index encoded in
|
||||
# the 0x5000 duty callback (0..15) was internally consistent with the
|
||||
# bogus signal-id math, so single-servo demos still appeared to work.
|
||||
# Multi-servo projects (e.g. solar-tracker, project 5218f9e3) exposed
|
||||
# the bug — `ledcAttachPin(13, 0)` actually writes signal 71 to
|
||||
# gpio_out_sel[13], which the off-by-one scan REJECTED, so channel 0
|
||||
# resolved to GPIO 12 (the next servo's pin, whose signal 72 WAS in
|
||||
# range and was misinterpreted as channel 0).
|
||||
SIG_LEDC_HS_CH0_OUT_IDX = 71 # add N for HS channel N (0..7)
|
||||
SIG_LEDC_HS_CH_LAST = 78
|
||||
SIG_LEDC_LS_CH0_OUT_IDX = 79 # add N for LS channel N (0..7)
|
||||
SIG_LEDC_LS_CH_LAST = 86
|
||||
|
||||
|
||||
def ledc_signal_for_channel(channel: int) -> int:
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@ describe('SignalRouter — core update/lookup', () => {
|
|||
|
||||
describe('esp32-signals — channel ↔ signal id helpers', () => {
|
||||
it.each([
|
||||
[0, SIG_LEDC_HS_CH0_OUT_IDX], // 72
|
||||
[7, SIG_LEDC_HS_CH0_OUT_IDX + 7], // 79
|
||||
[8, SIG_LEDC_LS_CH0_OUT_IDX], // 80
|
||||
[15, SIG_LEDC_LS_CH0_OUT_IDX + 7], // 87
|
||||
[0, SIG_LEDC_HS_CH0_OUT_IDX], // 71 (HS ch 0)
|
||||
[7, SIG_LEDC_HS_CH0_OUT_IDX + 7], // 78 (HS ch 7)
|
||||
[8, SIG_LEDC_LS_CH0_OUT_IDX], // 79 (LS ch 0)
|
||||
[15, SIG_LEDC_LS_CH0_OUT_IDX + 7], // 86 (LS ch 7)
|
||||
])('ledcSignalForChannel(%d) roundtrips through channelForLedcSignal → %d', (channel, sig) => {
|
||||
expect(ledcSignalForChannel(channel)).toBe(sig);
|
||||
expect(channelForLedcSignal(sig)).toBe(channel);
|
||||
|
|
@ -131,9 +131,10 @@ describe('esp32-signals — channel ↔ signal id helpers', () => {
|
|||
});
|
||||
|
||||
it('channelForLedcSignal returns null for non-LEDC signal ids', () => {
|
||||
// 70 sits immediately below the LEDC range, 87 immediately above.
|
||||
expect(channelForLedcSignal(0)).toBeNull();
|
||||
expect(channelForLedcSignal(71)).toBeNull();
|
||||
expect(channelForLedcSignal(88)).toBeNull();
|
||||
expect(channelForLedcSignal(70)).toBeNull();
|
||||
expect(channelForLedcSignal(87)).toBeNull();
|
||||
expect(channelForLedcSignal(256)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@
|
|||
*/
|
||||
|
||||
// ── LEDC (PWM peripheral) ─────────────────────────────────────────────────
|
||||
// High-speed channels (group 0): signals 72-79 → ledc channel 0-7
|
||||
// Low-speed channels (group 1): signals 80-87 → ledc channel 0-7
|
||||
export const SIG_LEDC_HS_CH0_OUT_IDX = 72;
|
||||
export const SIG_LEDC_HS_CH_LAST = 79;
|
||||
export const SIG_LEDC_LS_CH0_OUT_IDX = 80;
|
||||
export const SIG_LEDC_LS_CH_LAST = 87;
|
||||
// Per ESP32 TRM section 4.11, Table 4-3 (GPIO Matrix output signals):
|
||||
// 71-78 → LEDC_HS_SIG_OUT[0..7] (high-speed channels 0-7)
|
||||
// 79-86 → LEDC_LS_SIG_OUT[0..7] (low-speed channels 0-7)
|
||||
// The legacy worker code used 72-87 (off by one). That bug rerouted
|
||||
// every multi-servo project's channels — see
|
||||
// backend/app/services/esp32_signals.py for the full explanation.
|
||||
export const SIG_LEDC_HS_CH0_OUT_IDX = 71;
|
||||
export const SIG_LEDC_HS_CH_LAST = 78;
|
||||
export const SIG_LEDC_LS_CH0_OUT_IDX = 79;
|
||||
export const SIG_LEDC_LS_CH_LAST = 86;
|
||||
|
||||
/**
|
||||
* Map a velxio-style unified LEDC channel index (0..15) to its GPIO
|
||||
|
|
|
|||
|
|
@ -173,10 +173,10 @@ def test_replace_snapshot_combines_changes_and_clears() -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"channel,expected",
|
||||
[
|
||||
(0, SIG_LEDC_HS_CH0_OUT_IDX), # 72
|
||||
(7, SIG_LEDC_HS_CH0_OUT_IDX + 7), # 79
|
||||
(8, SIG_LEDC_LS_CH0_OUT_IDX), # 80
|
||||
(15, SIG_LEDC_LS_CH0_OUT_IDX + 7), # 87
|
||||
(0, SIG_LEDC_HS_CH0_OUT_IDX), # 71 (HS ch 0)
|
||||
(7, SIG_LEDC_HS_CH0_OUT_IDX + 7), # 78 (HS ch 7)
|
||||
(8, SIG_LEDC_LS_CH0_OUT_IDX), # 79 (LS ch 0)
|
||||
(15, SIG_LEDC_LS_CH0_OUT_IDX + 7), # 86 (LS ch 7)
|
||||
],
|
||||
)
|
||||
def test_ledc_signal_for_channel_roundtrip(channel: int, expected: int) -> None:
|
||||
|
|
@ -192,9 +192,12 @@ def test_ledc_signal_for_channel_rejects_out_of_range() -> None:
|
|||
|
||||
|
||||
def test_channel_for_ledc_signal_returns_none_for_non_ledc() -> None:
|
||||
# 70 is the signal immediately below the LEDC range; 87 is the
|
||||
# signal immediately above. Both must return None — anything
|
||||
# else implies the constants drifted away from the ESP32 TRM.
|
||||
assert channel_for_ledc_signal(0) is None
|
||||
assert channel_for_ledc_signal(71) is None
|
||||
assert channel_for_ledc_signal(88) is None
|
||||
assert channel_for_ledc_signal(70) is None
|
||||
assert channel_for_ledc_signal(87) is None
|
||||
assert channel_for_ledc_signal(256) is None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue