esp32: M5Stack ATOM Matrix + timer-poll sleep + Chassis-CAN diagnostics#46
Open
maslyankov wants to merge 1 commit into
Open
esp32: M5Stack ATOM Matrix + timer-poll sleep + Chassis-CAN diagnostics#46maslyankov wants to merge 1 commit into
maslyankov wants to merge 1 commit into
Conversation
5ba9d17 to
0774ecd
Compare
…assis-CAN diagnostics
Behind a new `m5stack-atom-matrix` PlatformIO env. Tested on bench (boot,
WiFi AP, LED matrix, dashboard, sleep cycle, button wake) and on a 2022
EU Model 3 HW3 firmware 2026.8.3 with the enhauto cable tapping Chassis
CAN at the passenger footwell — that's the first practical real-vehicle
test of the ESP32 port on a non-OBD-II install.
Matrix LED. The 5x5 SK6812 grid is on the same GPIO 27 as the Lite's
single LED, so led.cpp takes a `LED_COUNT` macro (default 1) and a
`fill()` helper drives all pixels. Brightness scales down to ~8 normal /
40 peak so 25 LEDs at white don't blow the M5Stack USB regulator.
Front button on GPIO 39. The M5Stack ATOM Matrix's front face button is
wired to GPIO 39, not GPIO 0 — GPIO 0 is the small side BOOT button used
for entering UART download mode. The matrix env now overrides
PIN_BUTTON=39 via build flag. GPIO 39 is input-only (no internal pull-up,
M5Stack PCB has an external one) and RTC-capable, and is not a strapping
pin so wake-by-press is reliable. ATOM Lite stays on GPIO 0 for backward
compat.
Timer-poll deep sleep. The LilyGO EXT0-on-CAN_RX strategy doesn't work
on the M5Stack — its ATOMIC CAN Base routes RX to GPIO 19, which isn't
RTC-capable. Both `m5stack-atom` and `m5stack-atom-matrix` envs now use
a timer-poll wake: 60s deep sleep, 5s post-wake CAN probe, sleep again
on miss. WiFi+dashboard are deferred during the probe to save ~50 mA x 5s
on every miss while parked. Probe window shows as dim white on the
matrix and the wiring warning is suppressed during probe (silence is
expected then). Default sleep_idle_ms = 60s; raise via dashboard up to
3600s (1h) if you want effectively no sleep on a desk setup.
The button is also enabled as an EXT0 wake source alongside the timer.
RTC pull-up handling is no-op'd on input-only pins (relying on the
external pull-up), and pinMode is re-issued AFTER rtc_gpio_deinit on
EXT0 wake or digital IO never regains control. Existing g_btn_ignore_boot
already prevents the wake-press from registering as a phantom click. A
button press during a probe also clears probe mode so the device doesn't
re-sleep on the user.
WiFi credential overrides. fsd_handler.cpp uses WIFI_DEFAULT_SSID /
WIFI_DEFAULT_PASS macros with the existing "Tesla-FSD" / "12345678" as
fallback (no behavior change for existing builds). platformio.ini
references an optional gitignored platformio_local.ini via
extra_configs, so personal AP passwords don't ship in the public repo.
Verified PIO no-ops cleanly when the local file is absent. Shared
[common-atom-matrix] section avoids self-recursive build_flags merges.
The on-car testing surfaced two firmware-version-specific issues that
the upstream ESP32 code didn't account for. Both got runtime overrides
so any user who hits them can work around without recompiling.
HW override (dashboard dropdown). The 0x398 GTW_carConfig isn't on every
tap (Party CAN doesn't have it on EU HW3 with ISA active, and Chassis
CAN doesn't carry it at all on the enhauto tap), and the 0x399-fallback
misclassifies that car as HW4. Added an `hw_override` field — set Auto /
Legacy / HW3 / HW4 from the dashboard — that pins hw_version and short-
circuits apply_detected_hw(). Pinned override is applied at boot before
dispatcher runs.
OTA-detect bypass. The upstream OTA detection at `0x318` byte 6 bits
[1:0] treats raw value `1` as "in progress" — but on this car's firmware
that value is the steady-state idle / "background download" code, not
the install-imminent code. With the upstream check, TX stays suspended
indefinitely. Added `ota_ignore` toggle (NVS-persisted) so consumers
(TX gate, LED, dashboard banner) bypass the detection. The raw value is
exposed on the dashboard so the user can see what their car actually
broadcasts.
Reboot-from-dashboard button. Useful when an NVS setting needs a clean
boot to apply. ESP.restart() with a 200 ms delay so the WebSocket ack
lands in the browser before the chip resets.
Read-only diagnostics card. Ports the Flipper-side parsers for vehicle
speed (0x257), steering angle (0x129), motor torque (0x108 bit-21
DI_torqueMotor with the 0.222656 scale, not the 0.25 the original
Flipper port used), DAS_status full (0x39B: hands-on, lane change, side
collision, FCW, vision speed limit), and DAS_autopilot tier readback
(0x331 byte[0]). Stalk position (0x3F8 → speed_profile) is exposed too
so a stalk push is the reliable end-to-end CAN-pipeline check in park.
Fields use `*_seen` flags so the dashboard renders "--" rather than
stale zeros when an ID isn't on the user's bus.
CAN serial trace mode. Per-ID change-detected printer (one [TRACE] line
per ID when its bytes change vs last capture) so contributors can
identify firmware-version-specific bit positions empirically. Toggleable
from the dashboard, RAM-only — not persisted in NVS because leaving it
on can starve the loop. Backpressure-guarded via Serial.availableForWrite
to never block when USB is disconnected.
Chassis-CAN BMS fallback (new — closes a gap on this bus). The standard
BMS frames (0x132/0x292/0x312) aren't broadcast on Chassis CAN on this
firmware. Reverse-engineered three substitute message IDs by capturing
trace and grepping for known ground-truth values from the enhauto
Commander running on the same harness:
- 0x420 byte 2 = SoC % (verified at 64% / 0x40)
- 0x239 byte 5 × 0.5 − 40 = battery temp °C (verified ~22°C
against cluster reading)
- 0x2B5 bytes 0-1 LE × 0.01 = LV (12V) bus voltage (15.85 V)
- 0x2B5 bytes 2-3 LE × 0.1 = HV pack voltage (378.5 V)
- 0x2B5 byte 4 × 0.1 = LV bus current (~20 A)
HV pack current still unavailable on this bus — the message that
carries it isn't broadcast here either. New `lv_bus_*` state fields
expose 12V bus on the dashboard (low LV bus is a common Tesla failure
mode). Existing `pack_voltage_v` / `soc_percent` / `batt_temp_*` fields
populate from the new parsers, so the existing BMS card works without
any new UI plumbing.
What's still NOT working on Chassis CAN (documented as N/A, not bug):
- Brake state: the 0x145 ESP_status byte 2 bit 3 position from opendbc
doesn't toggle on this firmware/bus. Tested 0x102 byte 4 bit 1 from a
3-tap capture, but it false-correlates (toggles independently of brake
input — likely a temperature sensor with the same ×0.5−40 encoding).
Brake source on Chassis CAN remains unidentified. Not blocking:
driver_brake_applied isn't read by any TX path in either ESP32 or
Flipper code, only displayed.
- HV pack current: not in the messages we've identified
- DAS_status (0x39B): not broadcast on Chassis CAN here. 0x39D is on
this bus but carries different fields.
- GTW autopilot tier (0x7FF): mixed/Ethernet bus only.
- 0x398 HW config: not on this tap (hence the HW override above).
Matrix LED, sleep, button, OTA bypass, HW override, FSD activation
(bit 46 on 0x3FD via Active mode) and TLSSC Restore (0x331 spoof —
confirmed working: triggers MCU reboot as the README documents) are all
verified working.
All six existing PlatformIO envs build clean (m5stack-atom,
m5stack-atom-swap-pins, m5stack-atom-matrix, esp32-mcp2515, esp32-lilygo,
waveshare-s3-can).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
0774ecd to
67af863
Compare
Author
|
Battery SoC does not seem accurate so I will work on that next. |
hypery11
approved these changes
Apr 30, 2026
Owner
hypery11
left a comment
There was a problem hiding this comment.
High-quality first contribution with real-car testing. Approve with one minor fix.
Fix before merge: Remove the prefs_save(g_state) call from the can_trace WebSocket handler — the comment says "can_trace deliberately not persisted" but the save call writes all NVS keys anyway (unnecessary flash wear). Either remove the save call or add can_trace to prefs_load for consistency.
Everything else is production-ready:
- ATOM Matrix LED: clean addition, LED_COUNT=1 default preserves all existing boards
- Chassis-CAN: all read-only parsers, no new TX paths, correct ID range
- BMS parser: additive fallback for 0x420/0x239/0x2B5, doesn't break standard 0x132/0x292/0x312
- Sleep strategy refactor: generalizes BOARD_LILYGO guards cleanly
- NVS: hw_override and ota_ignore keys follow existing pattern, no conflicts with PR #40
- Security: no issues
Your reverse-engineering documentation is more thorough than most DBC contributions. Welcome to the project.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tested on a 2022 EU Model 3 HW3 running firmware 2026.8.3 with the
enhauto cable tapping Chassis CAN at the passenger footwell, plus
bench-tested for boot, WiFi AP, LED matrix, sleep cycle, and button
wake. This is the first practical real-vehicle test of the ESP32 port
on a non-OBD-II install, which surfaced two firmware-version-specific
issues that needed runtime overrides plus a substantial gap in the
upstream BMS parser coverage (closed via reverse-engineering against
ground-truth values from the enhauto Commander on the same harness).
Hardware: M5Stack ATOM Matrix + ATOMIC CAN Base + enhauto harness, ~$30.
Same wiring approach as the existing ATOM Lite, just with the front face
button wired correctly (see below) and the matrix LED driven properly.
HARDWARE.md updated with both the new ATOM Matrix row and the enhauto
Chassis-CAN install path with its known limitations.
Matrix LED
5x5 SK6812 grid is on the same GPIO 27 as the Lite's single LED, so
led.cpp now takes a
LED_COUNTmacro (default 1, 25 in the matrix env)and a
fill()helper drives all pixels. Brightness scaled down so 25LEDs at full white don't pull more through the M5Stack USB regulator
than it can handle.
Front button on GPIO 39
The M5Stack ATOM (Lite and Matrix both) has the user-facing front button
wired to GPIO 39. The current firmware polls GPIO 0, which is the small
side BOOT button. Anyone pressing the obvious front button sees nothing
happen because the firmware never reads that pin.
The matrix env now overrides
PIN_BUTTON=39via build flag. GPIO 39 isinput-only (M5Stack PCB has external pull-up) and is RTC-capable, and
isn't a strapping pin so wake-by-press is reliable. ATOM Lite stays on
GPIO 0 for backward compat with anyone using the side button.
Deep sleep for M5Stack envs
LilyGO's EXT0-on-CAN_RX strategy doesn't work on the M5Stack — the
ATOMIC CAN Base routes RX to GPIO 19, which isn't RTC-capable. So both
m5stack-atomandm5stack-atom-matrixenvs use a timer-poll wake:60s deep sleep, 5s post-wake CAN probe, sleep again on miss.
WiFi+dashboard are deferred during the probe — saves ~50 mA × 5s on
every miss while parked. Probe window shows as dim white on the matrix.
Wiring warning is suppressed during probe (silence is expected then).
The button is wired as an EXT0 wake source alongside the timer.
Button-wake skips probe mode and goes straight to normal operation.
Button press during a probe also extends the wake. Existing
g_btn_ignore_bootalready prevents the wake-press from being read asa phantom click.
User knob is the existing
sleep_idle_msslider — default lowered from120s to 60s for an in-car experience. Desk-setup users can raise it to
3600s and effectively disable.
Firmware-version workarounds (the on-car part)
These are what got added once I plugged into a real Tesla and started
discovering that the upstream assumptions don't always hold on
2026.8.3 EU HW3.
HW override.
0x398GTW_carConfig isn't on every tap — Party CANdoesn't have it on EU HW3 with ISA active, and Chassis CAN doesn't have
it at all on the enhauto tap. The 0x399 fallback in main.cpp then
misclassifies the car as HW4 (because ISA is HW4-only on the older
firmware the upstream code was tested against, but newer EU HW3
broadcasts 0x399 too for regulatory compliance). Added an
hw_overridefield — Auto / Legacy / HW3 / HW4 from a dashboard dropdown — that pins
hw_versionand short-circuitsapply_detected_hw(). Pinned overrideis applied at boot before the dispatcher runs.
OTA-detect bypass. The upstream OTA detection at
0x318byte 6bits [1:0] treats raw value
1as "in progress." On 2026.8.3, raw1is the steady-state idle value (or "background download in progress,
safe to drive"); the install-imminent code is something else. With the
upstream check, TX stays suspended indefinitely. Added
ota_ignoretoggle (NVS-persisted) so consumers (TX gate, LED, dashboard banner)
bypass the detection. The raw value is exposed on the dashboard so the
user can see what their car actually broadcasts and report back if
anyone wants to refine the detection later.
Reboot button. Useful when an NVS setting needs a clean boot to
apply. ESP.restart() with a 200ms delay so the WebSocket ack lands in
the browser before the chip resets.
Read-only diagnostics
Ports the Flipper-side parsers for vehicle speed (0x257), steering
angle (0x129), motor torque (0x108 — using DI_torqueMotor at bit 21
with the 0.222656 scale, not DI_torqueDriver at bit 0 with 0.25 scale
which the original Flipper port had), DAS_status (0x39B: hands-on,
lane change, side collision, FCW, vision speed limit), and
DAS_autopilot tier readback (0x331 byte[0]).
Stalk position (0x3F8 → speed_profile) is also exposed because pushing
the follow-distance stalk in park is the most reliable in-park test
that the whole CAN pipeline (wiring → driver → parser → state →
dashboard) works end-to-end.
Fields use
*_seenflags so the dashboard renders--rather thanstale zeros when an ID isn't on the user's bus.
CAN serial trace mode
A per-ID change-detected printer that emits one
[TRACE]line per IDwhen its bytes change vs the previous capture. Toggle from the
dashboard. RAM-only (not NVS-persisted) because leaving it on can
starve the loop. Backpressure-guarded via
Serial.availableForWriteso it never blocks when USB is disconnected. Used this myself to find
the BMS substitute IDs (below) and to disprove a brake-bit candidate
(0x102 byte 4 bit 1) that looked promising in a single capture but
flickered independently on broader testing.
Chassis-CAN BMS fallback
The standard BMS frames (0x132/0x292/0x312) aren't broadcast on Chassis
CAN on this firmware. Reverse-engineered three substitute message IDs
by capturing trace and grepping for ground-truth values from the
enhauto Commander running on the same harness:
0x420byte 2 = SoC % (verified at 64% / 0x40)0x239byte 5 × 0.5 − 40 = battery temp °C (verified ~22°C againstcluster)
0x2B5bytes 0-1 LE × 0.01 = LV (12V) bus voltage (15.85 V)0x2B5bytes 2-3 LE × 0.1 = HV pack voltage (378.5 V)0x2B5byte 4 × 0.1 = LV bus current (~20 A)HV pack current still unavailable on this bus — the message that
carries it isn't broadcast here either. Added
lv_bus_*state fieldsto expose the 12V bus on the dashboard (low LV bus is a common Tesla
failure mode). The existing
pack_voltage_v/soc_percent/batt_temp_*fields populate from the new parsers, so the existingBMS card works without any new UI plumbing.
Limitations on Chassis CAN (documented as N/A, not as bugs)
Things visible on the dashboard as
--because they're genuinely noton this bus tap on 2026.8.3:
this bus. Tested 0x102 byte 4 bit 1 from a 3-tap capture but it
false-correlates (it's likely a temperature sensor with the same
×0.5−40 encoding). Brake on Chassis CAN remains unidentified. Not
blocking:
driver_brake_appliedisn't read by any TX path in eitherESP32 or Flipper code, only displayed.
bus but carries different fields.
What was actually verified on the car
(
ENHANCED (2)correctly displayed)documented MCU reboot (read-modify-retransmit on 0x331 worked end-
to-end)
match enhauto Commander readouts within sensor accuracy
Did NOT test in motion. FSD activation bit 46 path wasn't engaged
because the test car has EAP entitlement, not FSD, and Tesla's server-
side entitlement check overrides the bit-level signal anyway.
Build matrix
All six existing PlatformIO envs build clean:
m5stack-atom,m5stack-atom-swap-pins,m5stack-atom-matrix,esp32-mcp2515,esp32-lilygo,waveshare-s3-can.Tagging
needs-on-car-testfor: ATOM Matrix on Party CAN (OBD-II) — Ionly tested it on Chassis CAN — and FSD activation on a car with FSD
entitlement, which I don't have. (Label not actually applied because
it doesn't exist on the repo and I lack write access; flagging in
text.)