diff --git a/HARDWARE.md b/HARDWARE.md index dd0a43d..abb1abf 100644 --- a/HARDWARE.md +++ b/HARDWARE.md @@ -6,6 +6,7 @@ |--------|------|------------|-----------|------|----------| | **Any ESP32 + MCP2515 → X179** | **~$5-7** | X179 4-wire | 1 (bus 6 = mixed) | Yes | Cheapest full-feature setup | | M5Stack ATOM Lite + ATOMIC CAN → X179 | ~$13-15 | X179 4-wire | 1 (bus 6) | Yes | Plug & play, no soldering | +| M5Stack ATOM Matrix + ATOMIC CAN → X179 | ~$22-25 | X179 4-wire | 1 (bus 6) | Yes | 5×5 LED status, timer-poll deep sleep | | **LILYGO T-2CAN ESP32-S3** → X179 | **~$24** | X179 4-wire (+ spare CAN2) | **2 independent** | Yes | Future-proof, dual-CAN ready | | **LILYGO T-CAN485** → X179 | **~$15** | X179 4-wire | 1 (SN65HVD230) | Yes | SD card CAN dump, tested on Model X/S | | Waveshare ESP32-S3-RS485-CAN → X179 | ~$18 | X179 4-wire | 1 (TWAI) | Yes | All-in-one board | @@ -142,6 +143,29 @@ X179 Pin 15 → 12V ────┤── buck converter → 3.3V/5V X179 Pin 20 → GND ────┘ (26-pin: use Pin 26 for GND) ``` +### enhauto Commander harness — Chassis CAN at the passenger footwell + +A separate viable install path for owners of the enhauto Commander cable +who want to repurpose the harness with our firmware. The cable exposes +two CAN pairs labeled **Chassis CAN H/L** and **Vehicle CAN H/L**. + +- **Chassis CAN** carries the autopilot control frames (`0x3FD`), + steering (`0x129`), DAS_status / DAS_autopilot (`0x331`), DI_speed + (`0x257`), DI_torque (`0x108`) — i.e. enough for FSD activation, + HW detection (with override — see PR notes), and most diagnostics. +- **Vehicle CAN** does not carry the AP frames; HW auto-detect won't + fire on this bus. Use Chassis CAN instead. + +Confirmed limitations on Chassis CAN (2026.8.3 EU HW3): +- `0x398` GTW_carConfig is not on this tap → use the dashboard's + HW Override dropdown to pin HW3 manually +- `0x132` / `0x292` / `0x312` BMS frames are not broadcast on this + bus → battery dashboard stays empty (the enhauto Commander itself + reads them via UDS query/response, which our passive listener + doesn't implement) +- `0x39B` DAS_status is not broadcast (only `0x39D` is, which carries + different fields) + --- ## Recommended setups diff --git a/esp32/.firmware/config.h b/esp32/.firmware/config.h index 2237726..7eefb65 100644 --- a/esp32/.firmware/config.h +++ b/esp32/.firmware/config.h @@ -15,6 +15,25 @@ #define CAN_ID_DAS_AP_CONFIG 0x331u // 817 - DAS autopilot config (tier restore target, ~1 Hz) #define CAN_ID_AP_CONTROL 0x3FDu // 1021 - DAS_autopilotControl: HW3 / HW4 core #define CAN_ID_DAS_STATUS 0x39Bu // 923 - DAS_status: AP hands-on state (nag gating) +#define CAN_ID_DI_SPEED 0x257u // 599 - DI_speed: vehicle speed +#define CAN_ID_ESP_STATUS 0x145u // 325 - ESP_status: brake apply, stability (Party CAN) +#define CAN_ID_BATT_STATUS 0x420u // 1056 - Empirically (2026.8.3 EU HW3 on Chassis CAN): + // byte 2 = SoC % (verified at 64%) + // Identified on Chassis CAN where the standard + // 0x132/0x292/0x312 BMS frames aren't broadcast. + // byte 0 and 3 hold stable values whose meaning + // is unconfirmed. +#define CAN_ID_BATT_TEMP 0x239u // 569 - Empirically (Chassis CAN, low-rate, ~1 Hz): + // byte 5 × 0.5 − 40 = battery temperature °C + // (verified at 22.5°C reported vs cluster 21.8°C — + // matches standard Tesla temp encoding). +#define CAN_ID_DC_BUS 0x2B5u // 693 - Empirically (2026.8.3 EU HW3 on Chassis CAN): + // bytes 0-1 LE × 0.01 = LV (12V) bus voltage + // bytes 2-3 LE × 0.1 = HV pack voltage + // byte 4 × 0.1 = LV bus current (A) + // Verified against enhauto Commander readouts. +#define CAN_ID_DI_TORQUE 0x108u // 264 - DI_torque: drive motor torque +#define CAN_ID_STEER_ANGLE 0x129u // 297 - SCCM_steeringAngleSensor // ── GPIO ────────────────────────────────────────────────────────────────────── #if defined(BOARD_LILYGO) @@ -75,10 +94,28 @@ #define ME2107_EN 16 #endif -// ── Deep sleep (BOARD_LILYGO only) ─────────────────────────────────────────── -// SN65HVD230 RXD (= PIN_CAN_RX, GPIO 26) is an RTC-capable GPIO on ESP32. -// When the CAN bus goes dominant the pin goes LOW — used as ext0 wakeup source. -// The SN65HVD230 Rs pin has an internal 100 kΩ pull-down, so it stays in -// normal-receive mode when GPIO 23 floats during deep sleep. -#define SLEEP_IDLE_MS 120000u // CAN silence before entering deep sleep -#define SLEEP_WARN_MS 5000u // serial/log warning this many ms before sleep +// ── Deep sleep ──────────────────────────────────────────────────────────────── +// Two strategies, selected per board: +// SLEEP_STRATEGY_EXT0 — wake on CAN_RX edge. Needs PIN_CAN_RX on an +// RTC-capable GPIO. LilyGO's GPIO 26 qualifies; the +// SN65HVD230 Rs pin has an internal 100 kΩ pull-down +// so the transceiver stays in normal-receive mode +// while GPIO 23 floats during deep sleep. +// SLEEP_STRATEGY_TIMER — wake periodically and listen briefly for CAN +// traffic. Works on any board. Used by M5Stack ATOM +// variants whose ATOMIC CAN Base routes RX to GPIO 19 +// (not RTC-capable, so EXT0 is unavailable). +// Both ATOM Lite and ATOM Matrix share the ATOMIC CAN Base wiring (RX on +// GPIO 19, not RTC-capable), so neither can use EXT0-on-CAN_RX. Both opt +// into the timer-poll strategy. To disable sleep on a permanently-powered +// dev setup, raise sleep_idle_ms via the web dashboard (max 3600 s). +#if defined(BOARD_LILYGO) + #define SLEEP_STRATEGY_EXT0 1 +#elif defined(BOARD_M5STACK_ATOM) || defined(BOARD_M5STACK_ATOM_MATRIX) + #define SLEEP_STRATEGY_TIMER 1 +#endif + +#define SLEEP_IDLE_MS 60000u // CAN silence before entering deep sleep (runtime override via web UI) +#define SLEEP_WARN_MS 5000u // serial/log warning this many ms before sleep +#define SLEEP_TIMER_WAKE_S 60u // TIMER strategy: deep-sleep duration between probes +#define SLEEP_PROBE_MS 5000u // TIMER strategy: listen window for CAN after a timer wake diff --git a/esp32/.firmware/fsd_handler.cpp b/esp32/.firmware/fsd_handler.cpp index 81b1e59..14f4d84 100644 --- a/esp32/.firmware/fsd_handler.cpp +++ b/esp32/.firmware/fsd_handler.cpp @@ -53,9 +53,20 @@ void fsd_state_init(FSDState *state, TeslaHWVersion hw) { state->force_fsd = false; state->bms_output = false; state->sleep_idle_ms = SLEEP_IDLE_MS; - - strncpy(state->wifi_ssid, "Tesla-FSD", sizeof(state->wifi_ssid)); - strncpy(state->wifi_pass, "12345678", sizeof(state->wifi_pass)); + state->hw_override = TeslaHW_Unknown; // 0 = auto-detect + state->ota_ignore = false; + + // Build-time overridable via WIFI_DEFAULT_SSID / WIFI_DEFAULT_PASS in + // platformio.ini (or a gitignored platformio_local.ini) so personal + // credentials don't ship in the public repo. + #ifndef WIFI_DEFAULT_SSID + #define WIFI_DEFAULT_SSID "Tesla-FSD" + #endif + #ifndef WIFI_DEFAULT_PASS + #define WIFI_DEFAULT_PASS "12345678" + #endif + strncpy(state->wifi_ssid, WIFI_DEFAULT_SSID, sizeof(state->wifi_ssid)); + strncpy(state->wifi_pass, WIFI_DEFAULT_PASS, sizeof(state->wifi_pass)); state->wifi_hidden = false; } @@ -74,7 +85,7 @@ void fsd_apply_hw_version(FSDState *state, TeslaHWVersion hw) { bool fsd_can_transmit(const FSDState *state) { if (state->op_mode == OpMode_ListenOnly) return false; - if (state->tesla_ota_in_progress) return false; + if (state->tesla_ota_in_progress && !state->ota_ignore) return false; return true; } @@ -423,11 +434,171 @@ bool fsd_handle_tlssc_restore(FSDState *state, CanFrame *frame) { return true; } -// ── DAS status (0x39B) — nag killer gating ─────────────────────────────────── +// ── DAS status (0x39B) — nag killer gating + diagnostics readback ─────────── void fsd_handle_das_status(FSDState *state, const CanFrame *frame) { + if (frame->dlc == 0) return; + // Mark seen so the dashboard knows the ID is on this bus, even if a + // particular field's bytes aren't present. + state->das_seen = true; + state->raw_39b_dlc = frame->dlc; + for (uint8_t i = 0; i < frame->dlc && i < 8; i++) + state->raw_39b_bytes[i] = frame->data[i]; if (frame->dlc < 6) return; // DAS_autopilotHandsOnState: bit42|4 LE → byte5 bits[5:2] state->das_hands_on_state = (frame->data[5] >> 2) & 0x0Fu; - state->das_seen = true; + // DAS_autoLaneChangeState: bit46|5 LE → byte5 bits[7:6] + byte6 bits[2:0] + if (frame->dlc >= 7) { + state->das_lane_change = ((frame->data[5] >> 6) & 0x03u) | + ((frame->data[6] & 0x07u) << 2); + } + // DAS_sideCollisionWarning: bit32|2 → byte4 bits[1:0] + // DAS_sideCollisionAvoid: bit30|2 → byte3 bits[7:6] + if (frame->dlc >= 5) { + state->das_side_coll_warn = frame->data[4] & 0x03u; + state->das_side_coll_avoid = (frame->data[3] >> 6) & 0x03u; + } + // DAS_forwardCollisionWarning: bit22|2 → byte2 bits[7:6] + // DAS_visionOnlySpeedLimit: bit16|5 → byte2 bits[4:0], ×5 = kph + if (frame->dlc >= 3) { + state->das_fcw = (frame->data[2] >> 6) & 0x03u; + state->das_vision_speed_lim = frame->data[2] & 0x1Fu; + } +} + +// ── DAS_autopilot config readback (0x331) ──────────────────────────────────── +// byte[0] lower 6 bits encodes two 3-bit tiers (DAS_autopilot / Base). +// Tier enum: 0=NONE 1=HIGHWAY 2=ENHANCED 3=SELF_DRIVING 4=BASIC. +void fsd_handle_das_ap_config(FSDState *state, const CanFrame *frame) { + if (frame->dlc < 1) return; + uint8_t b0 = frame->data[0]; + state->das_autopilot = (b0 >> 3) & 0x07u; + state->das_autopilot_base = b0 & 0x07u; + state->das_ap_seen = true; +} + +// ── DI_speed (0x257) — vehicle speed ───────────────────────────────────────── +// DI_vehicleSpeed: 12-bit LE starting at bit 12 → byte1 high nibble + byte2 +// scale 0.08, offset -40, units kph. +// DI_uiSpeed: bit24|8 → byte 3 (display speed, integer kph or mph). +void fsd_handle_di_speed(FSDState *state, const CanFrame *frame) { + if (frame->dlc < 4) return; + uint16_t raw = (((uint16_t)frame->data[2]) << 4) | ((frame->data[1] >> 4) & 0x0Fu); + float v = (float)raw * 0.08f - 40.0f; + if (v < 0.0f) v = 0.0f; + state->vehicle_speed_kph = v; + state->ui_speed = frame->data[3]; + state->speed_seen = true; +} + +// ── ESP_v118 (0x145) — driver brake pedal ──────────────────────────────────── +// opendbc tesla_model3_party.dbc: +// ESP_brakePedalPressed: 19|1@1+ → byte 2, bit 3 (LE) +// (The Flipper code reads byte 3 bits [6:5]; that's a different bit on this +// firmware version and reads as constantly non-zero, so we use the opendbc +// position here.) +void fsd_handle_esp_status(FSDState *state, const CanFrame *frame) { + if (frame->dlc == 0) return; + state->raw_145_dlc = frame->dlc; + for (uint8_t i = 0; i < frame->dlc && i < 8; i++) + state->raw_145_bytes[i] = frame->data[i]; + if (frame->dlc < 3) return; + // Note: this position (byte 2 bit 3 per opendbc) is observed to NOT toggle + // on EU HW3 firmware via Chassis CAN — the Chassis-CAN brake bit lives in + // 0x102 instead (see fsd_handle_vcleft_brake). We only set brake_applied + // here when the brake_seen flag isn't already being driven by 0x102. + if (!state->brake_seen) { + state->driver_brake_applied = ((frame->data[2] >> 3) & 0x01u) != 0; + state->brake_seen = true; + } +} + +// (NOTE: 0x102 byte 4 bit 1 was tested as a brake candidate from a 3-tap +// capture but turned out to flicker independently of brake input — false +// correlation. Brake on Chassis CAN appears to live somewhere we haven't +// identified yet; needs more reverse engineering. The 0x145 ESP_status +// parser above remains the Party-CAN brake source per opendbc.) + +// ── DI_torque1 (0x108) — drive motor torque ────────────────────────────────── +// opendbc tesla_model3_party.dbc: +// DI_torqueMotor: 21|13@1+ (0.222656, -750) — actual motor output, Nm +// LE 13-bit unsigned starting at bit 21: +// byte[2] bits [7:5] = bits 21..23 (low 3 bits) +// byte[3] bits [7:0] = bits 24..31 (mid 8 bits) +// byte[4] bits [1:0] = bits 32..33 (high 2 bits) +// (The Flipper code read DI_torqueDriver at bit 0 with scale 0.25 — on this +// firmware that value moves around unrelated to actual motor torque.) +void fsd_handle_di_torque(FSDState *state, const CanFrame *frame) { + if (frame->dlc < 5) return; + uint16_t raw = (uint16_t)((frame->data[2] >> 5) & 0x07u) + | ((uint16_t)frame->data[3] << 3) + | ((uint16_t)(frame->data[4] & 0x03u) << 11); + state->motor_torque_nm = (float)raw * 0.222656f - 750.0f; + state->torque_seen = true; +} + +// ── SCCM_steeringAngleSensor (0x129) — steering wheel angle ────────────────── +// opendbc tesla_model3_party.dbc: +// SCCM_steeringAngleSensor: 16|14@1+ (0.1, -819.2) +// LE 14-bit unsigned starting at bit 16 = byte 2, with offset: +// byte[2] = bits 16..23 (low 8 bits) +// byte[3] bits [5:0] = bits 24..29 (high 6 bits) +// (The Flipper code read raw int16 from bytes 0-1, which on this firmware is +// a different field that jumps unrelated to wheel position.) +void fsd_handle_steering_angle(FSDState *state, const CanFrame *frame) { + if (frame->dlc == 0) return; + state->steering_seen = true; + state->raw_129_dlc = frame->dlc; + for (uint8_t i = 0; i < frame->dlc && i < 8; i++) + state->raw_129_bytes[i] = frame->data[i]; + if (frame->dlc < 4) return; + uint16_t raw = (uint16_t)frame->data[2] | ((uint16_t)(frame->data[3] & 0x3Fu) << 8); + state->steering_angle_deg = (float)raw * 0.1f - 819.2f; +} + +// ── 0x420 — Chassis-CAN battery status fallback ───────────────────────────── +// Reverse-engineered against a 2026.8.3 EU HW3 car (Chassis CAN tap): +// byte 0 = battery temp °C × 2 (tentative — verified at 0x28 = 40 raw, +// so 20°C, vs cluster reading of ~21.5°C — within sensor delta). +// byte 2 = SoC % (verified at 64% with byte 2 = 0x40). +// byte 3 = stable value, meaning unknown (verified != charge target). +// Standard BMS frames (0x132/0x292/0x312) aren't broadcast on Chassis CAN +// here. Pack voltage/current still unavailable without UDS query/response. +void fsd_handle_batt_status_chassis(FSDState *state, const CanFrame *frame) { + if (frame->dlc < 3) return; + state->soc_percent = (float)frame->data[2]; + state->bms_seen = true; +} + +// ── 0x239 — Chassis-CAN battery temperature ────────────────────────────────── +// Low-rate broadcast. byte 5 × 0.5 − 40 = battery temperature °C using the +// standard Tesla cell-temp encoding. Verified on live data against a +// 22°C-cluster reading. byte 2 also decodes in the temp range but the +// values don't track with the actual battery temp — likely some other +// field that just happens to fall in the same numeric range. +void fsd_handle_batt_temp(FSDState *state, const CanFrame *frame) { + if (frame->dlc < 6) return; + int t = (int)frame->data[5] / 2 - 40; + state->batt_temp_min_c = (int8_t)t; + state->batt_temp_max_c = (int8_t)t; + state->bms_seen = true; +} + +// ── 0x2B5 — Chassis-CAN DC bus status ──────────────────────────────────────── +// Reverse-engineered against a 2026.8.3 EU HW3 car (Chassis CAN tap), +// values cross-verified against enhauto Commander: +// bytes 0-1 LE × 0.01 = LV (12V) bus voltage (e.g. 1580 = 15.80 V) +// bytes 2-3 LE × 0.1 = HV pack voltage (e.g. 3785 = 378.5 V) +// byte 4 × 0.1 = LV bus current (A) (e.g. 0xC8 = 20.0 A) +// Populates pack_voltage_v (HV) and the new lv_bus_* fields. HV pack +// current isn't in this message; left as 0. +void fsd_handle_dc_bus(FSDState *state, const CanFrame *frame) { + if (frame->dlc < 5) return; + uint16_t lv_raw = (uint16_t)frame->data[0] | ((uint16_t)frame->data[1] << 8); + uint16_t hv_raw = (uint16_t)frame->data[2] | ((uint16_t)frame->data[3] << 8); + state->lv_bus_voltage_v = (float)lv_raw * 0.01f; + state->lv_bus_current_a = (float)frame->data[4] * 0.1f; + state->pack_voltage_v = (float)hv_raw * 0.1f; + state->lv_bus_seen = true; + state->bms_seen = true; } diff --git a/esp32/.firmware/fsd_handler.h b/esp32/.firmware/fsd_handler.h index f567b27..6f745c5 100644 --- a/esp32/.firmware/fsd_handler.h +++ b/esp32/.firmware/fsd_handler.h @@ -28,6 +28,12 @@ typedef enum { // ── Full FSD state ──────────────────────────────────────────────────────────── struct FSDState { TeslaHWVersion hw_version; + // User-set override. TeslaHW_Unknown = use auto-detect; any other value + // pins hw_version to that HW and tells apply_detected_hw() to skip + // auto-detect updates. Useful when 0x398 isn't on the tapped CAN bus + // and the 0x399-fallback misclassifies the car (e.g. EU HW3 with ISA + // active broadcasts 0x399 even though it isn't HW4). + TeslaHWVersion hw_override; int speed_profile; // 0-4 depending on HW int speed_offset; // HW3 only, 0-100 @@ -46,6 +52,10 @@ struct FSDState { // ── Mode + diagnostics ──────────────────────────────────────────────────── OpMode op_mode; bool tesla_ota_in_progress; // pause TX during OTA + bool ota_ignore; // user override: ignore OTA detection + // (workaround for cars whose 0x318 + // byte-6 encoding doesn't match the + // upstream OTA_IN_PROGRESS_RAW_VALUE) uint8_t ota_raw_state; // raw GTW_updateInProgress bits [1:0] uint8_t ota_assert_count; // consecutive "in-progress" samples uint8_t ota_clear_count; // consecutive "not in-progress" samples @@ -54,6 +64,7 @@ struct FSDState { uint32_t seen_gtw_car_state; // 0x318 seen count uint32_t seen_gtw_car_config; // 0x398 seen count uint32_t seen_ap_control; // 0x3FD seen count + uint32_t seen_follow_dist; // 0x3F8 seen count (stalk) uint32_t seen_bms_hv; // 0x132 seen count uint32_t seen_bms_soc; // 0x292 seen count uint32_t seen_bms_thermal; // 0x312 seen count @@ -66,6 +77,54 @@ struct FSDState { float soc_percent; int8_t batt_temp_min_c; int8_t batt_temp_max_c; + // 12 V auxiliary bus (Chassis-CAN 0x2B5). Useful diagnostic — a low LV + // bus voltage is a common Tesla failure mode (dead 12V battery). + bool lv_bus_seen; + float lv_bus_voltage_v; + float lv_bus_current_a; + + // ── Vehicle dynamics (read-only, parsed from Party CAN) ───────────────── + bool speed_seen; + float vehicle_speed_kph; // 0x257 DI_vehicleSpeed (12-bit, 0.08, -40) + uint8_t ui_speed; // 0x257 DI_uiSpeed (display value) + bool steering_seen; + float steering_angle_deg; // 0x129 (signed 16-bit, ×0.1) + bool torque_seen; + float motor_torque_nm; // 0x108 DI_torque1 (13-bit, 0.25, -750) + bool brake_seen; + bool driver_brake_applied; // 0x145 ESP_driverBrakeApply + + // ── DAS_status full (0x39B) — extends das_hands_on_state below ────────── + uint8_t das_lane_change; // 5-bit, lane change state + uint8_t das_side_coll_warn; // 2-bit, side collision warning (blind spot) + uint8_t das_side_coll_avoid; // 2-bit, side collision avoid + uint8_t das_fcw; // 2-bit, forward collision warning + uint8_t das_vision_speed_lim; // 5-bit, ×5 = kph (vision-based limit) + + // ── Raw frame snapshots (for in-car bit-position debugging) ───────────── + // Last seen DLC + bytes for IDs whose parsing is firmware-version-fragile. + // Surfaced on the dashboard as hex; press the relevant control on the car + // (brake pedal, turn wheel) and watch which byte changes to identify the + // correct bit position. + uint8_t raw_145_dlc; + uint8_t raw_145_bytes[8]; + uint8_t raw_39b_dlc; + uint8_t raw_39b_bytes[8]; + uint8_t raw_129_dlc; + uint8_t raw_129_bytes[8]; + + // ── CAN serial trace (for finding unknown signal positions) ───────────── + // When can_trace is true, every frame whose bytes differ from the last + // capture for its ID is printed to serial as + // [TRACE] 0x145 dlc=8: 41 02 00 00 00 00 00 03 + // The last-bytes table below is kept regardless of trace state so that + // turning the trace on doesn't briefly print every steady-state frame. + bool can_trace; // runtime-only, default off + uint16_t trace_count; // number of unique IDs seen + #define TRACE_MAX 80 + uint16_t trace_ids[TRACE_MAX]; + uint8_t trace_dlc[TRACE_MAX]; + uint8_t trace_bytes[TRACE_MAX][8]; // ── Precondition trigger ────────────────────────────────────────────────── bool precondition; // periodically inject 0x082 @@ -82,6 +141,15 @@ struct FSDState { bool tlssc_restore; uint32_t tlssc_restore_count; + // ── DAS_autopilot readback (parsed from 0x331 byte[0]) ────────────────── + // byte[0] lower 6 bits encodes two 3-bit tiers (0..4): + // bits 5:3 = DAS_autopilot, bits 2:0 = DAS_autopilotBase + // Tier enum: 0=NONE 1=HIGHWAY 2=ENHANCED 3=SELF_DRIVING 4=BASIC + bool das_ap_seen; + uint8_t das_autopilot; // bits 5:3 + uint8_t das_autopilot_base; // bits 2:0 + uint32_t seen_das_ap_config; // 0x331 frame counter + // ── DAS status (0x39B) — nag killer gating ─────────────────────────────── // 0=NOT_REQD, 8=SUSPENDED — both mean DAS is satisfied, skip echo. // das_seen starts false; if 0x39B is absent from the tapped bus the nag @@ -147,5 +215,36 @@ void fsd_build_precondition_frame(CanFrame *frame); * Returns true if frame was modified and should be re-sent. */ bool fsd_handle_tlssc_restore(FSDState *state, CanFrame *frame); -/** Parse DAS_status (0x39B) — updates das_hands_on_state for nag killer gating. */ +/** Parse DAS_status (0x39B) — updates das_hands_on_state plus lane change, + * side-collision warn/avoid, FCW and vision speed limit fields. */ void fsd_handle_das_status(FSDState *state, const CanFrame *frame); + +/** Parse DAS config (0x331) — updates das_autopilot / das_autopilot_base + * tier readback fields (0=NONE 1=HIGHWAY 2=ENHANCED 3=SELF_DRIVING 4=BASIC). */ +void fsd_handle_das_ap_config(FSDState *state, const CanFrame *frame); + +/** Parse DI_speed (0x257) — vehicle speed in kph + UI display value. */ +void fsd_handle_di_speed(FSDState *state, const CanFrame *frame); + +/** Parse ESP_status (0x145) — driver brake apply flag (Party CAN). */ +void fsd_handle_esp_status(FSDState *state, const CanFrame *frame); + + +/** Parse DI_torque (0x108) — drive motor torque in Nm. */ +void fsd_handle_di_torque(FSDState *state, const CanFrame *frame); + +/** Parse SCCM_steeringAngleSensor (0x129) — steering angle in degrees. */ +void fsd_handle_steering_angle(FSDState *state, const CanFrame *frame); + +/** Parse 0x420 — Chassis-CAN battery status fallback. byte 2 = SoC %. + * Empirically identified on 2026.8.3 EU HW3 where the standard BMS frames + * (0x132/0x292/0x312) are not broadcast on Chassis CAN. */ +void fsd_handle_batt_status_chassis(FSDState *state, const CanFrame *frame); + +/** Parse 0x239 — Chassis-CAN battery temperature. + * byte 5 × 0.5 − 40 = battery temp °C (standard Tesla cell-temp encoding). */ +void fsd_handle_batt_temp(FSDState *state, const CanFrame *frame); + +/** Parse 0x2B5 — Chassis-CAN DC bus status. Carries LV (12V) bus voltage + + * current and the HV pack voltage. Empirically identified on Chassis CAN. */ +void fsd_handle_dc_bus(FSDState *state, const CanFrame *frame); diff --git a/esp32/.firmware/led.cpp b/esp32/.firmware/led.cpp index 4005cc1..bcb24ae 100644 --- a/esp32/.firmware/led.cpp +++ b/esp32/.firmware/led.cpp @@ -2,12 +2,33 @@ #include "config.h" #include -// M5Stack ATOM Lite: single SK6812 (GRB order) on PIN_LED (GPIO27) -static Adafruit_NeoPixel g_strip(1, PIN_LED, NEO_GRB + NEO_KHZ800); +// LED_COUNT defaults to 1 (ATOM Lite single SK6812). Override to 25 for the +// ATOM Matrix 5x5 grid; both share PIN_LED = GPIO27 and the GRB SK6812 driver. +#ifndef LED_COUNT +#define LED_COUNT 1 +#endif + +// 25 LEDs at brightness=25 white draws ~150 mA; cap matrix brightness lower so +// USB power and the M5Stack regulator stay happy. +#if LED_COUNT > 1 +#define LED_BRIGHT_NORMAL 8 +#define LED_BRIGHT_DIM 2 +#define LED_BRIGHT_FULL 40 +#else +#define LED_BRIGHT_NORMAL 25 +#define LED_BRIGHT_DIM 5 +#define LED_BRIGHT_FULL 255 +#endif + +static Adafruit_NeoPixel g_strip(LED_COUNT, PIN_LED, NEO_GRB + NEO_KHZ800); + +static inline void fill(uint32_t c) { + for (uint16_t i = 0; i < LED_COUNT; i++) g_strip.setPixelColor(i, c); +} void led_init() { g_strip.begin(); - g_strip.setBrightness(25); // keep dim — the ATOM LED is very bright + g_strip.setBrightness(LED_BRIGHT_NORMAL); g_strip.clear(); g_strip.show(); } @@ -15,17 +36,17 @@ void led_init() { void led_set(LedColor color) { uint32_t c; if (color == LED_SLEEP) { - g_strip.setBrightness(5); - g_strip.setPixelColor(0, g_strip.Color(255, 255, 255)); // dim white + g_strip.setBrightness(LED_BRIGHT_DIM); + fill(g_strip.Color(255, 255, 255)); g_strip.show(); - g_strip.setBrightness(25); + g_strip.setBrightness(LED_BRIGHT_NORMAL); return; } if (color == LED_WHITE) { - g_strip.setBrightness(255); - g_strip.setPixelColor(0, g_strip.Color(255, 255, 255)); + g_strip.setBrightness(LED_BRIGHT_FULL); + fill(g_strip.Color(255, 255, 255)); g_strip.show(); - g_strip.setBrightness(25); + g_strip.setBrightness(LED_BRIGHT_NORMAL); return; } switch (color) { @@ -35,7 +56,7 @@ void led_set(LedColor color) { case LED_RED: c = g_strip.Color(255, 0, 0); break; default: c = 0; break; } - g_strip.setPixelColor(0, c); + fill(c); g_strip.show(); } diff --git a/esp32/.firmware/main.cpp b/esp32/.firmware/main.cpp index fd14b95..a7ce4a5 100644 --- a/esp32/.firmware/main.cpp +++ b/esp32/.firmware/main.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "config.h" #include "fsd_handler.h" #include "can_driver.h" @@ -31,6 +32,13 @@ static FSDState g_state = {}; static void apply_detected_hw(TeslaHWVersion hw, const char *reason) { if (hw == TeslaHW_Unknown || g_state.hw_version == hw) return; + // If the user has pinned a HW version via the dashboard, ignore auto- + // detect updates. Useful when the OBD-II Party CAN tap doesn't carry + // 0x398 and the 0x399-fallback misclassifies the car. + if (g_state.hw_override != TeslaHW_Unknown) { + Serial.printf("[HW] Auto-detect %s ignored (override pinned)\n", reason); + return; + } fsd_apply_hw_version(&g_state, hw); @@ -52,10 +60,27 @@ static bool g_factory_reset_window = false; // set true on clean boot, cl static bool g_factory_reset_eligible = false; // latched at leading edge if press was in window static bool g_factory_reset_armed = false; // blink done, waiting for release -#if defined(BOARD_LILYGO) +#if defined(SLEEP_STRATEGY_EXT0) || defined(SLEEP_STRATEGY_TIMER) static uint32_t g_last_can_rx_ms = 0; static bool g_sleep_warned = false; #endif +#if defined(SLEEP_STRATEGY_TIMER) +// True for the brief listen window after a timer wake. Cleared on the first +// CAN frame seen, so the device stays awake whenever the car is alive. +static bool g_probe_mode = false; +// Track whether WiFi/dashboard were started this boot, so the lazy starter +// (called from setup or after a successful probe) is idempotent. RAM is wiped +// on deep sleep, so this is naturally false on every wake. +static bool g_wifi_started = false; + +static void start_wifi_lazy() { + if (g_wifi_started) return; + g_wifi_started = true; + if (wifi_ap_init(&g_state)) { + web_dashboard_init(&g_state, g_can); + } +} +#endif static void dispatch_clicks(int n) { if (n == 1) { @@ -96,6 +121,17 @@ static void button_tick() { g_btn_down_ms = now; g_long_fired = false; g_factory_reset_eligible = g_factory_reset_window; // latch at press time +#if defined(SLEEP_STRATEGY_TIMER) + // A button press during a post-wake probe is deliberate user intent; + // clear probe mode so the normal sleep_idle_ms threshold takes over + // and the device doesn't immediately re-sleep on probe expiry. + if (g_probe_mode) { + g_probe_mode = false; + g_last_can_rx_ms = now; // restart the sleep clock from this press + Serial.println("[WAKE] Button press during probe — staying awake"); + start_wifi_lazy(); + } +#endif } if (g_btn_down && pressed && !g_long_fired) { @@ -146,9 +182,15 @@ static void update_led() { led_set(LED_WHITE); return; } +#if defined(SLEEP_STRATEGY_TIMER) + if (g_probe_mode) { + led_set(LED_SLEEP); // dim white while listening for CAN after timer wake + return; + } +#endif if (g_state.rx_count == 0 && millis() > WIRING_WARN_MS) { led_set(LED_RED); - } else if (g_state.tesla_ota_in_progress) { + } else if (g_state.tesla_ota_in_progress && !g_state.ota_ignore) { led_set(LED_YELLOW); } else if (g_state.op_mode == OpMode_Active) { led_set(LED_GREEN); @@ -161,14 +203,64 @@ static void update_led() { static void process_frame(const CanFrame &frame) { g_state.rx_count++; can_dump_record(frame); -#if defined(BOARD_LILYGO) +#if defined(SLEEP_STRATEGY_EXT0) || defined(SLEEP_STRATEGY_TIMER) g_last_can_rx_ms = millis(); g_sleep_warned = false; + #if defined(SLEEP_STRATEGY_TIMER) + if (g_probe_mode) { + g_probe_mode = false; + Serial.println("[WAKE] CAN traffic detected — staying awake"); + start_wifi_lazy(); + } + #endif #endif + // ── CAN serial trace: print to serial when an ID's bytes change ───────── + // Bounded table tracks the last-seen bytes per unique ID. When can_trace + // is on, each change emits one [TRACE] line — perfect for finding which + // ID carries an unknown signal (e.g. brake pedal): trigger the input on + // the car and watch which IDs print. + { + int slot = -1; + for (uint16_t i = 0; i < g_state.trace_count; i++) { + if (g_state.trace_ids[i] == (uint16_t)frame.id) { slot = (int)i; break; } + } + if (slot < 0 && g_state.trace_count < TRACE_MAX) { + slot = g_state.trace_count++; + g_state.trace_ids[slot] = (uint16_t)frame.id; + // Fresh slot starts with sentinel so the first frame of a new ID + // does not register as a "change" and spam serial when trace + // is toggled on. + g_state.trace_dlc[slot] = 0xFF; + } + if (slot >= 0) { + uint8_t dlc = frame.dlc < 8 ? frame.dlc : 8; + bool changed = (g_state.trace_dlc[slot] != 0xFF) && + (g_state.trace_dlc[slot] != dlc); + for (uint8_t i = 0; i < dlc; i++) { + if (g_state.trace_dlc[slot] != 0xFF && + g_state.trace_bytes[slot][i] != frame.data[i]) changed = true; + g_state.trace_bytes[slot][i] = frame.data[i]; + } + g_state.trace_dlc[slot] = dlc; + if (changed && g_state.can_trace) { + // Backpressure guard: if the UART TX buffer doesn't have room + // for a full line (~40 chars), drop this print rather than + // block the loop. Without USB connected the FIFO fills fast + // and Serial.printf would otherwise stall web_dashboard_update. + if (Serial.availableForWrite() >= 40) { + Serial.printf("[TRACE] 0x%03X dlc=%u:", (unsigned)frame.id, dlc); + for (uint8_t i = 0; i < dlc; i++) Serial.printf(" %02X", frame.data[i]); + Serial.println(); + } + } + } + } + if (frame.id == CAN_ID_GTW_CAR_STATE) g_state.seen_gtw_car_state++; if (frame.id == CAN_ID_GTW_CAR_CONFIG) g_state.seen_gtw_car_config++; if (frame.id == CAN_ID_AP_CONTROL) g_state.seen_ap_control++; + if (frame.id == CAN_ID_FOLLOW_DIST) g_state.seen_follow_dist++; if (frame.id == CAN_ID_BMS_HV_BUS) g_state.seen_bms_hv++; if (frame.id == CAN_ID_BMS_SOC) g_state.seen_bms_soc++; if (frame.id == CAN_ID_BMS_THERMAL) g_state.seen_bms_thermal++; @@ -203,9 +295,18 @@ static void process_frame(const CanFrame &frame) { if (frame.id == CAN_ID_BMS_SOC) { fsd_handle_bms_soc(&g_state, &frame); return; } if (frame.id == CAN_ID_BMS_THERMAL) { fsd_handle_bms_thermal(&g_state, &frame); return; } - // ── DAS status (read-only, always) — gating for NAG killer ─────────────── + // ── DAS status (read-only, always) — gating for NAG killer + diagnostics ─ if (frame.id == CAN_ID_DAS_STATUS) { fsd_handle_das_status(&g_state, &frame); return; } + // ── Vehicle dynamics (read-only, always) ───────────────────────────────── + if (frame.id == CAN_ID_DI_SPEED) { fsd_handle_di_speed(&g_state, &frame); return; } + if (frame.id == CAN_ID_ESP_STATUS) { fsd_handle_esp_status(&g_state, &frame); return; } + if (frame.id == CAN_ID_DI_TORQUE) { fsd_handle_di_torque(&g_state, &frame); return; } + if (frame.id == CAN_ID_STEER_ANGLE) { fsd_handle_steering_angle(&g_state, &frame); return; } + if (frame.id == CAN_ID_BATT_STATUS) { fsd_handle_batt_status_chassis(&g_state, &frame); return; } + if (frame.id == CAN_ID_BATT_TEMP) { fsd_handle_batt_temp(&g_state, &frame); return; } + if (frame.id == CAN_ID_DC_BUS) { fsd_handle_dc_bus(&g_state, &frame); return; } + // ── Beyond here only run when TX is allowed ─────────────────────────────── bool tx = fsd_can_transmit(&g_state); @@ -277,8 +378,12 @@ static void process_frame(const CanFrame &frame) { return; } - // TLSSC Restore (0x331) — DAS config spoof + // TLSSC Restore (0x331) — DAS config spoof + DAS_autopilot tier readback if (frame.id == CAN_ID_DAS_AP_CONFIG) { + // Always parse the readback first so the dashboard reflects the + // car's reported tier even when TLSSC restore is disabled. + fsd_handle_das_ap_config(&g_state, &frame); + g_state.seen_das_ap_config++; CanFrame f = frame; if (fsd_handle_tlssc_restore(&g_state, &f) && tx) g_can->send(f); @@ -294,14 +399,14 @@ static void process_frame(const CanFrame &frame) { } } -#if defined(BOARD_LILYGO) -// ── Deep-sleep watchdog (Lilygo only) ──────────────────────────────────────── +#if defined(SLEEP_STRATEGY_EXT0) +// ── Deep-sleep watchdog (EXT0 wake on CAN_RX edge) ─────────────────────────── static void sleep_tick(uint32_t now) { if (now < g_last_can_rx_ms) return; uint32_t idle_ms = now - g_last_can_rx_ms; if (idle_ms >= g_state.sleep_idle_ms) { - Serial.printf("[SLEEP] Entering deep sleep after %lu ms CAN silence\n", + Serial.printf("[SLEEP] Entering deep sleep (EXT0 wake) after %lu ms CAN silence\n", (unsigned long)idle_ms); can_dump_stop(); sd_syslog_close(); @@ -316,12 +421,53 @@ static void sleep_tick(uint32_t now) { (unsigned long)idle_ms, (unsigned long)remaining_ms); } } +#elif defined(SLEEP_STRATEGY_TIMER) +// ── Deep-sleep watchdog (timer wake + post-wake CAN probe) ─────────────────── +// In probe mode the threshold is the short SLEEP_PROBE_MS window; once a CAN +// frame arrives the probe flag clears and we revert to the user-configured +// sleep_idle_ms (driven from the web dashboard slider). +static void sleep_tick(uint32_t now) { + if (now < g_last_can_rx_ms) return; + uint32_t idle_ms = now - g_last_can_rx_ms; + uint32_t threshold_ms = g_probe_mode ? SLEEP_PROBE_MS : g_state.sleep_idle_ms; + + if (idle_ms >= threshold_ms) { + Serial.printf("[SLEEP] Entering deep sleep (timer %us / button wake) after %lu ms idle (%s)\n", + (unsigned)SLEEP_TIMER_WAKE_S, (unsigned long)idle_ms, + g_probe_mode ? "probe miss" : "user threshold"); + can_dump_stop(); + led_set(LED_SLEEP); + // Wake on button press (active-LOW, EXT0). Timer + EXT0 are + // independent wake sources — either fires. + // pullup_en/pulldown_dis are no-ops on input-only pins (GPIO 34-39) + // which have no internal pulls; those rely on an external pull-up + // (e.g. M5Stack ATOM PCB has one on GPIO 39). + rtc_gpio_pullup_en((gpio_num_t)PIN_BUTTON); + rtc_gpio_pulldown_dis((gpio_num_t)PIN_BUTTON); + esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BUTTON, 0); + esp_sleep_enable_timer_wakeup((uint64_t)SLEEP_TIMER_WAKE_S * 1000000ULL); + esp_deep_sleep_start(); + // never returns + } else if (!g_probe_mode && !g_sleep_warned && + idle_ms >= (g_state.sleep_idle_ms - SLEEP_WARN_MS)) { + g_sleep_warned = true; + uint32_t remaining_ms = g_state.sleep_idle_ms - idle_ms; + Serial.printf("[SLEEP] Warning: %lu ms idle, sleeping in %lu ms\n", + (unsigned long)idle_ms, (unsigned long)remaining_ms); + } +} #endif // ── setup ───────────────────────────────────────────────────────────────────── void setup() { -#if defined(BOARD_LILYGO) +#if defined(SLEEP_STRATEGY_EXT0) || defined(SLEEP_STRATEGY_TIMER) g_last_can_rx_ms = millis(); +#endif +#if defined(SLEEP_STRATEGY_TIMER) + // If we just woke via EXT0 the button pin is still held by the RTC IO + // subsystem; release it before pinMode() below so digital IO regains + // control. Safe no-op when the pin was never in RTC mode (cold boot). + rtc_gpio_deinit((gpio_num_t)PIN_BUTTON); #endif Serial.begin(115200); delay(300); @@ -335,6 +481,8 @@ void setup() { Serial.println("[CAN] Driver: ESP32-S3 TWAI (Waveshare ESP32-S3-RS485-CAN)"); #elif defined(BOARD_LILYGO) Serial.println("[CAN] Driver: ESP32 TWAI (LilyGO T-CAN485)"); + #elif defined(BOARD_M5STACK_ATOM_MATRIX) + Serial.println("[CAN] Driver: ESP32 TWAI (M5Stack ATOM Matrix + ATOMIC CAN Base)"); #else Serial.println("[CAN] Driver: ESP32 TWAI (M5Stack ATOM Lite + ATOMIC CAN Base)"); #endif @@ -356,7 +504,15 @@ void setup() { Serial.printf("[CFG] pins: LED=%d BUTTON=%d CAN_TX=%d CAN_RX=%d\n", PIN_LED, PIN_BUTTON, PIN_CAN_TX, PIN_CAN_RX); + // ESP32 input-only pins (GPIO 34-39) have no internal pulls; the M5Stack + // ATOM Matrix wires the front button to GPIO 39 with an external pull-up + // on the PCB, so plain INPUT is correct. All other supported boards wire + // the button to a regular GPIO with internal pull-up available. +#if (PIN_BUTTON >= 34 && PIN_BUTTON <= 39) + pinMode(PIN_BUTTON, INPUT); +#else pinMode(PIN_BUTTON, INPUT_PULLUP); +#endif led_init(); fsd_state_init(&g_state, TeslaHW_Unknown); @@ -370,6 +526,18 @@ void setup() { prefs_load(&g_state); + // If the user has pinned a HW version, apply it now so subsequent + // auto-detect attempts (which run from process_frame) are short-circuited + // by apply_detected_hw(). + if (g_state.hw_override != TeslaHW_Unknown) { + fsd_apply_hw_version(&g_state, g_state.hw_override); + const char *hw_str = + (g_state.hw_override == TeslaHW_HW4) ? "HW4" : + (g_state.hw_override == TeslaHW_HW3) ? "HW3" : + (g_state.hw_override == TeslaHW_Legacy) ? "Legacy" : "?"; + Serial.printf("[HW] Pinned by override: %s (auto-detect disabled)\n", hw_str); + } + { esp_sleep_wakeup_cause_t wakeup = esp_sleep_get_wakeup_cause(); g_factory_reset_window = (wakeup == ESP_SLEEP_WAKEUP_UNDEFINED); @@ -386,7 +554,7 @@ void setup() { can_dump_init(); -#if defined(BOARD_LILYGO) +#if defined(SLEEP_STRATEGY_EXT0) { esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); if (cause == ESP_SLEEP_WAKEUP_EXT0) { @@ -396,6 +564,23 @@ void setup() { } g_last_can_rx_ms = millis(); } +#elif defined(SLEEP_STRATEGY_TIMER) + { + esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + if (cause == ESP_SLEEP_WAKEUP_TIMER) { + g_probe_mode = true; + Serial.printf("[WAKE] Timer probe — listening %u ms for CAN traffic\n", + (unsigned)SLEEP_PROBE_MS); + } else if (cause == ESP_SLEEP_WAKEUP_EXT0) { + // Deliberate user wake via the button — skip probe and run + // normally. RTC IO has already been released for this pin at the + // top of setup() so pinMode could regain control. + Serial.printf("[WAKE] Button press (GPIO %d) — staying awake\n", PIN_BUTTON); + } else if (cause != ESP_SLEEP_WAKEUP_UNDEFINED) { + Serial.printf("[WAKE] Wakeup cause=%d\n", (int)cause); + } + g_last_can_rx_ms = millis(); + } #endif g_can = can_driver_create(); @@ -421,9 +606,19 @@ void setup() { Serial.println("[LED] Blue=Listen Green=Active Yellow=OTA Red=Error"); // ── WiFi AP + Web dashboard (non-fatal if WiFi fails) ───────────────────── + // For TIMER-sleep boards we defer WiFi until the post-wake CAN probe + // succeeds — saves ~50 mA × probe duration on every miss while parked. +#if defined(SLEEP_STRATEGY_TIMER) + if (!g_probe_mode) { + start_wifi_lazy(); + } else { + Serial.println("[WiFi] Deferred — will start once CAN traffic confirms wake"); + } +#else if (wifi_ap_init(&g_state)) { web_dashboard_init(&g_state, g_can); } +#endif } // ── loop ────────────────────────────────────────────────────────────────────── @@ -498,9 +693,14 @@ void loop() { } // ── Wiring sanity warning ───────────────────────────────────────────────── + // Suppressed during a timer-wake probe: silence is the expected case there + // and we'd drop straight back to sleep before the user could act on it. static uint32_t last_warn_ms = 0; - if (g_state.rx_count == 0 && now > WIRING_WARN_MS && - (now - last_warn_ms) >= 2000u) { + bool warn_eligible = (g_state.rx_count == 0 && now > WIRING_WARN_MS); +#if defined(SLEEP_STRATEGY_TIMER) + if (g_probe_mode) warn_eligible = false; +#endif + if (warn_eligible && (now - last_warn_ms) >= 2000u) { Serial.println("[WARN] No CAN traffic after 5 s — check wiring"); Serial.println("[WARN] Verify CAN-H on OBD pin 6, CAN-L on pin 14"); last_warn_ms = now; @@ -508,7 +708,7 @@ void loop() { can_dump_tick(now); -#if defined(BOARD_LILYGO) +#if defined(SLEEP_STRATEGY_EXT0) || defined(SLEEP_STRATEGY_TIMER) sleep_tick(now); #endif diff --git a/esp32/.firmware/prefs.cpp b/esp32/.firmware/prefs.cpp index 8d6eedb..bd6611e 100644 --- a/esp32/.firmware/prefs.cpp +++ b/esp32/.firmware/prefs.cpp @@ -26,9 +26,15 @@ void prefs_load(FSDState *state) { state->wifi_hidden = g_prefs.getBool("wsh", false); state->op_mode = (OpMode)g_prefs.getUChar("mode", (uint8_t)OpMode_ListenOnly); - - Serial.printf("[NVS] Loaded: NAG=%d Sleep=%u SSID=\"%s\" HIDDEN=%d\n", - state->nag_killer, state->sleep_idle_ms, state->wifi_ssid, state->wifi_hidden); + state->hw_override = (TeslaHWVersion)g_prefs.getUChar("hwov", (uint8_t)TeslaHW_Unknown); + state->ota_ignore = g_prefs.getBool("otaig", false); + // can_trace is intentionally NOT loaded from NVS: it floods serial and + // can starve the loop if left on accidentally. It must be re-enabled + // from the dashboard each boot. + + Serial.printf("[NVS] Loaded: NAG=%d Sleep=%u SSID=\"%s\" HIDDEN=%d HWov=%d\n", + state->nag_killer, state->sleep_idle_ms, state->wifi_ssid, state->wifi_hidden, + (int)state->hw_override); g_prefs.end(); } @@ -57,8 +63,12 @@ void prefs_save(const FSDState *state) { g_prefs.putBool("wsh", state->wifi_hidden); g_prefs.putUChar("mode", (uint8_t)state->op_mode); - - Serial.printf("[NVS] Saved: NAG=%d Sleep=%u SSID=\"%s\" HIDDEN=%d\n", - state->nag_killer, state->sleep_idle_ms, state->wifi_ssid, state->wifi_hidden); + g_prefs.putUChar("hwov", (uint8_t)state->hw_override); + g_prefs.putBool("otaig", state->ota_ignore); + // can_trace deliberately not persisted (see prefs_load) + + Serial.printf("[NVS] Saved: NAG=%d Sleep=%u SSID=\"%s\" HIDDEN=%d HWov=%d\n", + state->nag_killer, state->sleep_idle_ms, state->wifi_ssid, state->wifi_hidden, + (int)state->hw_override); g_prefs.end(); } diff --git a/esp32/.firmware/web_dashboard.cpp b/esp32/.firmware/web_dashboard.cpp index 438d4dd..658e59c 100644 --- a/esp32/.firmware/web_dashboard.cpp +++ b/esp32/.firmware/web_dashboard.cpp @@ -221,6 +221,10 @@ input:checked+.sl2:before{transform:translateX(20px);background:#fff}
--
Current
--
Temp
+
+ 12V bus + -- +
@@ -233,6 +237,49 @@ input:checked+.sl2:before{transform:translateX(20px);background:#fff}
0
CRC Errors
0.0
Frames/s
+
+ Frames seen + 0x398:0 0x3FD:0 0x3F8:0 0x318:0 +
+
+ Stalk → speed profile + -- +
+ + + +
+
V

Vehicle (live)

+
Speed--
+
Steering--
+
Motor torque--
+
Brake pedal--
+
+ DAS_autopilot-- +
+
DAS_autopilotBase--
+
Hands-on level--
+
Lane change--
+
Blind spot--
+
Forward coll. warn--
+
Vision speed limit--
+
+ + +
+
R

Raw frames

+
+ 0x145 ESP-- +
+
+ 0x39B DAS-- +
+
+ 0x129 STR-- +
+
+ Watch which byte changes when you press brake (0x145) or turn the wheel (0x129) — that tells us the right bit position for this firmware. +
@@ -263,6 +310,31 @@ input:checked+.sl2:before{transform:translateX(20px);background:#fff} Deep Sleep (sec) +
+ HW Override + +
+
+ Ignore OTA detect + +
+
+ CAN trace (serial) + +
+
+ OTA raw value + -- +
+
+ +
@@ -380,8 +452,17 @@ function upd(d){ if(document.getElementById('swTlssc')) document.getElementById('swTlssc').checked=d.tlssc_restore; if(document.getElementById('swDump')) document.getElementById('swDump').checked=!!d.can_dump; - if(document.activeElement.id!=='numSleep' && document.getElementById('numSleep')) + if(document.activeElement.id!=='numSleep' && document.getElementById('numSleep')) document.getElementById('numSleep').value=Math.floor((d.sleep_ms||0)/1000); + if(document.activeElement.id!=='hwOv' && document.getElementById('hwOv')) + document.getElementById('hwOv').value=String(d.hw_override||0); + if(document.getElementById('swOtaIg')) document.getElementById('swOtaIg').checked=!!d.ota_ignore; + if(document.getElementById('swTrace')) document.getElementById('swTrace').checked=!!d.can_trace; + if(document.getElementById('otaRaw')){ + var rv=(d.ota_raw===undefined?'?':String(d.ota_raw)); + var note=d.ota_ignore?' (detected, but ignored)':(d.ota?' (in progress)':' (idle)'); + document.getElementById('otaRaw').textContent=rv+note; + } pill('dumpSt',d.can_dump,d.can_dump?'Recording':'Idle'); @@ -390,6 +471,31 @@ function upd(d){ if(document.getElementById('txCnt')) document.getElementById('txCnt').textContent=(d.tx_count||0).toLocaleString(); if(document.getElementById('crcErr')) document.getElementById('crcErr').textContent=d.crc_errors||0; if(document.getElementById('fps')) document.getElementById('fps').textContent=(d.fps||0.0).toFixed(1); + if(document.getElementById('seenIds')) + document.getElementById('seenIds').textContent= + '0x398:'+(d.seen_398||0)+' 0x3FD:'+(d.seen_3fd||0)+ + ' 0x3F8:'+(d.seen_3f8||0)+' 0x318:'+(d.seen_318||0); + if(document.getElementById('stalkVal')) + document.getElementById('stalkVal').textContent= + 'profile '+((d.speed_profile===undefined||d.speed_profile===null)?'?':d.speed_profile); + + // Vehicle live readouts. Tier enum: 0=NONE 1=HIGHWAY 2=ENHANCED 3=SELF_DRIVING 4=BASIC. + var TIER=['NONE','HIGHWAY','ENHANCED','SELF_DRIVING','BASIC']; + var setT=function(id,t){var el=document.getElementById(id);if(el)el.textContent=t;}; + setT('vSpeed', d.speed_seen?(d.vehicle_speed_kph.toFixed(1)+' kph (UI:'+d.ui_speed+')'):'--'); + setT('vSteer', d.steering_seen?(d.steering_deg.toFixed(1)+'°'):'--'); + setT('vTorque',d.torque_seen?(d.motor_torque_nm.toFixed(0)+' Nm'):'--'); + setT('vBrake', d.brake_seen?(d.brake?'pressed':'released'):'--'); + setT('vDasAp', d.das_ap_seen?((TIER[d.das_ap]||'?')+' ('+d.das_ap+')'):'--'); + setT('vDasApBase', d.das_ap_seen?((TIER[d.das_ap_base]||'?')+' ('+d.das_ap_base+')'):'--'); + setT('vHands', d.das_seen?String(d.das_hands_on):'--'); + setT('vLane', d.das_seen?String(d.das_lane):'--'); + setT('vBlind', d.das_seen?String(d.das_blind):'--'); + setT('vFcw', d.das_seen?String(d.das_fcw):'--'); + setT('vVisLim',d.das_seen?(d.das_vis_lim?(d.das_vis_lim*5+' kph'):'unset'):'--'); + setT('raw145', d.raw_145||'--'); + setT('raw39b', d.raw_39b||'--'); + setT('raw129', d.raw_129||'--'); // Battery if(d.bms && d.bms.seen){ @@ -407,6 +513,14 @@ function upd(d){ } if(document.getElementById('bTemp')) document.getElementById('bTemp').textContent=d.bms.temp_min+'~'+d.bms.temp_max+'\u00b0C'; } + if(document.getElementById('lvBus')){ + if(d.lv_bus_seen){ + document.getElementById('lvBus').textContent= + d.lv_bus_v.toFixed(2)+' V / '+d.lv_bus_a.toFixed(1)+' A'; + } else { + document.getElementById('lvBus').textContent='--'; + } + } // Device if(document.getElementById('fwBuild')) document.getElementById('fwBuild').textContent=d.fw_build; @@ -513,7 +627,60 @@ static String build_json() { j += "\"fsd_enabled\":"; j += g_state->fsd_enabled ? "true" : "false"; j += ','; j += "\"op_mode\":"; j += (int)g_state->op_mode; j += ','; j += "\"hw_version\":"; j += (int)g_state->hw_version; j += ','; - j += "\"ota\":"; j += g_state->tesla_ota_in_progress ? "true" : "false"; j += ','; + j += "\"hw_override\":"; j += (int)g_state->hw_override; j += ','; + j += "\"speed_profile\":"; j += (int)g_state->speed_profile; j += ','; + j += "\"ota_raw\":"; j += (int)g_state->ota_raw_state; j += ','; + j += "\"ota_ignore\":"; j += g_state->ota_ignore ? "true" : "false"; j += ','; + j += "\"can_trace\":"; j += g_state->can_trace ? "true" : "false"; j += ','; + // Vehicle dynamics + DAS readbacks (read-only, parsed from Party CAN). + // *_seen flags let the UI render "--" when the bus doesn't carry that ID. + j += "\"speed_seen\":"; j += g_state->speed_seen ? "true" : "false"; j += ','; + j += "\"vehicle_speed_kph\":"; j += g_state->vehicle_speed_kph; j += ','; + j += "\"ui_speed\":"; j += (int)g_state->ui_speed; j += ','; + j += "\"steering_seen\":"; j += g_state->steering_seen ? "true" : "false"; j += ','; + j += "\"steering_deg\":"; j += g_state->steering_angle_deg; j += ','; + j += "\"torque_seen\":"; j += g_state->torque_seen ? "true" : "false"; j += ','; + j += "\"motor_torque_nm\":"; j += g_state->motor_torque_nm; j += ','; + j += "\"brake_seen\":"; j += g_state->brake_seen ? "true" : "false"; j += ','; + j += "\"brake\":"; j += g_state->driver_brake_applied ? "true" : "false"; j += ','; + j += "\"das_seen\":"; j += g_state->das_seen ? "true" : "false"; j += ','; + j += "\"das_hands_on\":"; j += (int)g_state->das_hands_on_state; j += ','; + j += "\"das_lane\":"; j += (int)g_state->das_lane_change; j += ','; + j += "\"das_blind\":"; j += (int)g_state->das_side_coll_warn; j += ','; + j += "\"das_avoid\":"; j += (int)g_state->das_side_coll_avoid; j += ','; + j += "\"das_fcw\":"; j += (int)g_state->das_fcw; j += ','; + j += "\"das_vis_lim\":"; j += (int)g_state->das_vision_speed_lim; j += ','; + j += "\"das_ap_seen\":"; j += g_state->das_ap_seen ? "true" : "false"; j += ','; + j += "\"das_ap\":"; j += (int)g_state->das_autopilot; j += ','; + j += "\"das_ap_base\":"; j += (int)g_state->das_autopilot_base; j += ','; + j += "\"lv_bus_seen\":"; j += g_state->lv_bus_seen ? "true" : "false"; j += ','; + j += "\"lv_bus_v\":"; j += g_state->lv_bus_voltage_v; j += ','; + j += "\"lv_bus_a\":"; j += g_state->lv_bus_current_a; j += ','; + // Raw byte snapshots for debugging firmware-version-specific bit positions. + auto raw_arr = [&](const char *key, uint8_t dlc, const uint8_t *bytes) { + j += '"'; j += key; j += "\":\""; + if (dlc == 0) { j += "--"; } + else { + char hex[4]; + for (uint8_t i = 0; i < dlc && i < 8; i++) { + snprintf(hex, sizeof(hex), "%02X ", bytes[i]); + j += hex; + } + } + j += "\","; + }; + raw_arr("raw_145", g_state->raw_145_dlc, g_state->raw_145_bytes); + raw_arr("raw_39b", g_state->raw_39b_dlc, g_state->raw_39b_bytes); + raw_arr("raw_129", g_state->raw_129_dlc, g_state->raw_129_bytes); + j += "\"seen_398\":"; j += g_state->seen_gtw_car_config; j += ','; + j += "\"seen_3fd\":"; j += g_state->seen_ap_control; j += ','; + j += "\"seen_3f8\":"; j += g_state->seen_follow_dist; j += ','; + j += "\"seen_318\":"; j += g_state->seen_gtw_car_state; j += ','; + // "ota" is the *effective* state (banner / TX-gate). When ota_ignore is + // on we bypass the detection, so callers see false even if the raw bits + // still match OTA_IN_PROGRESS_RAW_VALUE. The raw value is exposed + // separately in ota_raw for diagnostics. + j += "\"ota\":"; j += (g_state->tesla_ota_in_progress && !g_state->ota_ignore) ? "true" : "false"; j += ','; j += "\"nag_killer\":"; j += g_state->nag_killer ? "true" : "false"; j += ','; j += "\"bms_output\":"; j += g_state->bms_output ? "true" : "false"; j += ','; j += "\"force_fsd\":"; j += g_state->force_fsd ? "true" : "false"; j += ','; @@ -618,6 +785,44 @@ static void ws_event(uint8_t num, WStype_t type, prefs_save(g_state); } } + } else if (strstr(buf, "\"reboot\"")) { + Serial.println("[Web] Reboot requested via dashboard"); + delay(200); // let the WebSocket frame land in the browser + ESP.restart(); + } else if (strstr(buf, "\"ota_ignore\"")) { + if (vptr) { + while (*vptr == ' ' || *vptr == ':') vptr++; + g_state->ota_ignore = (strncmp(vptr, "true", 4) == 0); + Serial.printf("[Web] OTA ignore: %s (raw value seen: %u)\n", + g_state->ota_ignore ? "ON" : "OFF", + (unsigned)g_state->ota_raw_state); + prefs_save(g_state); + } + } else if (strstr(buf, "\"can_trace\"")) { + if (vptr) { + while (*vptr == ' ' || *vptr == ':') vptr++; + g_state->can_trace = (strncmp(vptr, "true", 4) == 0); + Serial.printf("[Web] CAN trace: %s\n", g_state->can_trace ? "ON" : "OFF"); + prefs_save(g_state); // persisted so it survives the reset that + // happens when the user opens a serial monitor + } + } else if (strstr(buf, "\"hw_override\"")) { + if (vptr) { + while (*vptr == ' ' || *vptr == ':') vptr++; + int v = atoi(vptr); + if (v >= TeslaHW_Unknown && v <= TeslaHW_HW4) { + g_state->hw_override = (TeslaHWVersion)v; + if (g_state->hw_override != TeslaHW_Unknown) { + fsd_apply_hw_version(g_state, g_state->hw_override); + } + const char *hw_str = + (v == TeslaHW_HW4) ? "HW4" : + (v == TeslaHW_HW3) ? "HW3" : + (v == TeslaHW_Legacy) ? "Legacy" : "Auto"; + Serial.printf("[Web] HW Override: %s\n", hw_str); + prefs_save(g_state); + } + } } else if (strstr(buf, "\"wifi_cfg\"")) { // Find the "value":{ object start const char *vobj = strstr(buf, "\"value\":"); diff --git a/esp32/.gitignore b/esp32/.gitignore index c76a768..1d8d0a2 100644 --- a/esp32/.gitignore +++ b/esp32/.gitignore @@ -2,3 +2,4 @@ .vscode/ TESLA.md PROJECT.md +platformio_local.ini diff --git a/esp32/README.md b/esp32/README.md index 902c5b1..4a7d287 100644 --- a/esp32/README.md +++ b/esp32/README.md @@ -107,6 +107,7 @@ Any ESP32 board + CAN transceiver works. Pick the matching build env in `platfor |---|---|---|---|---| | `m5stack-atom` | M5Stack ATOM Lite + ATOMIC CAN Base | TWAI | 22 / 19 | Default, cheapest | | `m5stack-atom-swap-pins` | M5Stack ATOM Lite + ATOMIC CAN Base | TWAI | 19 / 22 | For boards with swapped silkscreen | +| `m5stack-atom-matrix` | M5Stack ATOM Matrix + ATOMIC CAN Base | TWAI | 22 / 19 | Drives all 25 pixels (5×5) for status | | `esp32-mcp2515` | Generic ESP32 + MCP2515 module | MCP2515 SPI | SPI CS=5 | 8 MHz crystal | | `esp32-lilygo` | LilyGO T-CAN485 | TWAI | 27 / 26 | Built-in SN65HVD230 + SD slot | | `waveshare-s3-can` | Waveshare ESP32-S3-RS485-CAN | TWAI | 15 / 16 | ESP32-S3, 8MB flash/PSRAM, USB-CDC | diff --git a/esp32/platformio.ini b/esp32/platformio.ini index 4de4218..554a13f 100644 --- a/esp32/platformio.ini +++ b/esp32/platformio.ini @@ -1,5 +1,7 @@ [platformio] src_dir = .firmware +; Optional gitignored overrides (e.g. WiFi password); silently skipped if absent. +extra_configs = platformio_local.ini [env:m5stack-atom] platform = espressif32 @@ -27,6 +29,31 @@ lib_deps = adafruit/Adafruit NeoPixel@^1.12.0 Links2004/WebSockets@^2.4.0 +; Shared build flags for the Matrix env — referenced from this file and from +; the optional gitignored platformio_local.ini, avoiding self-recursive merges +; when local overrides need to append to the env's build_flags. +[common-atom-matrix] +build_flags = + -D BOARD_M5STACK_ATOM + -D BOARD_M5STACK_ATOM_MATRIX + -D CAN_DRIVER_TWAI + -D LED_COUNT=25 + ; M5Stack ATOM Matrix front face button is on GPIO 39 (not GPIO 0, which + ; is the side BOOT button). GPIO 39 is RTC-capable for EXT0 deep-sleep + ; wake and is not a strapping pin, so wake-by-press is reliable. The pin + ; has no internal pull-up but the M5Stack PCB provides an external one. + -D PIN_BUTTON=39 + +[env:m5stack-atom-matrix] +platform = espressif32 +board = m5stack-atom +framework = arduino +monitor_speed = 115200 +build_flags = ${common-atom-matrix.build_flags} +lib_deps = + adafruit/Adafruit NeoPixel@^1.12.0 + Links2004/WebSockets@^2.4.0 + [env:esp32-mcp2515] platform = espressif32 board = esp32dev