dashboard: add passive CAN capture markers

This commit is contained in:
2026-07-26 15:26:42 -06:00
parent ed9611310e
commit 8c2375d402
6 changed files with 227 additions and 56 deletions
+32 -8
View File
@@ -76,13 +76,37 @@ The dashboard uses field-filtered `/api/v1/status` calls:
This keeps the endpoint model simple while reducing payload size, JSON parsing, and UI churn.
## OBD-II / CAN Diagnostics
## Passive CAN Logger
The hidden diagnostics overlay distinguishes raw CAN traffic from parsed
OBD-II coolant responses. It reports request successes and failures, total
raw frames received, the last frame identifier and DLC, TWAI queue depth,
controller error counters, missed or overrun frames, arbitration losses,
and bus errors.
The dashboard runs its 500 kbps TWAI controller in listen-only mode and never
transmits onto the vehicle bus. At 115200 baud it prints changed frames as:
Raw RX activity confirms that the display is receiving CAN frames even when
the vehicle does not return a matching Mode 01 PID 05 coolant response.
CAN,ms,id,format,dlc,b0,b1,b2,b3,b4,b5,b6,b7
During a cold-start warm-up, enter the scan-tool coolant reading periodically:
TEMP,68
TEMP,95
TEMP,140
Generic timestamped markers can be added with `MARK,<label>`. For a stationary
4WD capture, start in 2H and enter each marker only after the corresponding
dashboard indicator has settled:
MARK,2H
MARK,4H
MARK,4LO
MARK,4H
MARK,2H
Wait 10-15 seconds between mode changes. Follow the vehicle owner's manual for
the conditions required to select 4LO, keep the vehicle stationary where
required, and save the complete serial output. The repeated return transitions
help distinguish an actual mode signal from unrelated counters and timers.
Save the serial output, then rank CAN ID/byte candidates with:
python tools/analyze_can_coolant.py can-warmup.log
Verify strong candidates over multiple warm-up captures before using one for
the dashboard gauge.
@@ -1,6 +1,6 @@
#pragma once
static const char *DASHBOARD_VERSION = "0.1.127-first-boot-wifi";
static const char *DASHBOARD_VERSION = "0.1.129-can-markers";
// First-boot defaults. Credentials are stored in Preferences/NVS after setup.
static const char *DASHBOARD_DEFAULT_CARGO_WIFI_SSID = "OverlandController";
@@ -40,6 +40,20 @@ static constexpr uint32_t OBD_REQUEST_ID = 0x7DF;
static constexpr uint32_t OBD_RESPONSE_ID_MIN = 0x7E8;
static constexpr uint32_t OBD_RESPONSE_ID_MAX = 0x7EF;
static constexpr uint8_t OBD_PID_COOLANT_TEMP = 0x05;
static constexpr size_t CAN_TRACKED_ID_COUNT = 96;
static constexpr unsigned long CAN_LOG_MIN_INTERVAL_MS = 100;
struct CanTrackedFrame {
bool used = false;
bool extended = false;
uint32_t identifier = 0;
uint8_t dlc = 0;
uint8_t data[8] = {};
unsigned long last_log_ms = 0;
};
static CanTrackedFrame can_tracked_frames[CAN_TRACKED_ID_COUNT];
static String can_serial_command;
static bool obd_can_ready = false;
static bool obd_can_started = false;
@@ -444,9 +458,9 @@ static bool init_obd_can()
return true;
}
twai_general_config_t general_config = TWAI_GENERAL_CONFIG_DEFAULT(OBD_CAN_TX_PIN, OBD_CAN_RX_PIN, TWAI_MODE_NORMAL);
general_config.tx_queue_len = 5;
general_config.rx_queue_len = 20;
twai_general_config_t general_config = TWAI_GENERAL_CONFIG_DEFAULT(OBD_CAN_TX_PIN, OBD_CAN_RX_PIN, TWAI_MODE_LISTEN_ONLY);
general_config.tx_queue_len = 0;
general_config.rx_queue_len = 64;
twai_timing_config_t timing_config = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
@@ -471,37 +485,12 @@ static bool init_obd_can()
obd_can_started = true;
obd_last_error = "started";
Serial.println("OBD CAN: TWAI started on TX GPIO15 / RX GPIO16 at 500 kbps");
Serial.println("OBD CAN: passive logger started on RX GPIO16 at 500 kbps");
Serial.println("CAN CSV: CAN,ms,id,format,dlc,b0,b1,b2,b3,b4,b5,b6,b7");
Serial.println("Enter TEMP,<degrees F> when recording a scan-tool coolant reading.");
return true;
}
static void send_obd_coolant_request()
{
twai_message_t message = {};
message.identifier = OBD_REQUEST_ID;
message.extd = 0;
message.rtr = 0;
message.data_length_code = 8;
message.data[0] = 0x02;
message.data[1] = 0x01;
message.data[2] = OBD_PID_COOLANT_TEMP;
message.data[3] = 0x55;
message.data[4] = 0x55;
message.data[5] = 0x55;
message.data[6] = 0x55;
message.data[7] = 0x55;
esp_err_t result = twai_transmit(&message, 0);
if (result == ESP_OK) {
obd_tx_count++;
last_obd_tx_ms = millis();
obd_last_error = "coolant request sent";
} else {
obd_tx_fail_count++;
obd_last_error = "tx failed: " + String((int)result);
}
}
static void handle_obd_can_message(const twai_message_t &message)
{
if (message.extd || message.rtr) {
@@ -533,16 +522,97 @@ static void handle_obd_can_message(const twai_message_t &message)
}
}
static CanTrackedFrame *find_can_tracked_frame(const twai_message_t &message)
{
CanTrackedFrame *free_slot = nullptr;
for (size_t i = 0; i < CAN_TRACKED_ID_COUNT; i++) {
CanTrackedFrame &tracked = can_tracked_frames[i];
if (tracked.used && tracked.identifier == message.identifier && tracked.extended == message.extd) {
return &tracked;
}
if (!tracked.used && free_slot == nullptr) free_slot = &tracked;
}
return free_slot;
}
static void log_changed_can_frame(const twai_message_t &message)
{
if (message.rtr) return;
CanTrackedFrame *tracked = find_can_tracked_frame(message);
if (tracked == nullptr) return;
bool changed = !tracked->used || tracked->dlc != message.data_length_code;
for (uint8_t i = 0; i < message.data_length_code && i < 8; i++) {
changed = changed || tracked->data[i] != message.data[i];
}
unsigned long now = millis();
bool should_log = changed && (!tracked->used || now - tracked->last_log_ms >= CAN_LOG_MIN_INTERVAL_MS);
tracked->used = true;
tracked->extended = message.extd;
tracked->identifier = message.identifier;
tracked->dlc = message.data_length_code;
memcpy(tracked->data, message.data, sizeof(tracked->data));
if (!should_log) return;
tracked->last_log_ms = now;
Serial.print("CAN,");
Serial.print(now);
Serial.print(",");
Serial.print(message.identifier, HEX);
Serial.print(message.extd ? ",EXT," : ",STD,");
Serial.print(message.data_length_code);
for (uint8_t i = 0; i < 8; i++) {
Serial.print(",");
if (i < message.data_length_code) {
if (message.data[i] < 0x10) Serial.print("0");
Serial.print(message.data[i], HEX);
}
}
Serial.println();
}
static void process_can_serial_commands()
{
while (Serial.available() > 0) {
char value = (char)Serial.read();
if (value == '\r') continue;
if (value != '\n') {
if (can_serial_command.length() < 48) can_serial_command += value;
continue;
}
can_serial_command.trim();
if (can_serial_command.startsWith("TEMP,")) {
String temperature = can_serial_command.substring(5);
temperature.trim();
Serial.print("TEMP,");
Serial.print(millis());
Serial.print(",");
Serial.println(temperature);
} else if (can_serial_command.startsWith("MARK,")) {
String label = can_serial_command.substring(5);
label.trim();
Serial.print("MARK,");
Serial.print(millis());
Serial.print(",");
Serial.println(label);
} else if (can_serial_command.length() > 0) {
Serial.println("ERROR,expected TEMP,<degrees F> or MARK,<label>");
}
can_serial_command = "";
}
}
static void process_obd_can()
{
if (!init_obd_can()) {
return;
}
if (millis() - last_obd_poll_ms >= 1000) {
last_obd_poll_ms = millis();
send_obd_coolant_request();
}
process_can_serial_commands();
twai_message_t message = {};
while (twai_receive(&message, 0) == ESP_OK) {
@@ -552,6 +622,7 @@ static void process_obd_can()
obd_last_rx_extended = message.extd;
last_obd_raw_rx_ms = millis();
log_changed_can_frame(message);
handle_obd_can_message(message);
}
@@ -635,23 +706,14 @@ static void update_diagnostics_overlay()
text += String(status_model.cell_delta_mv);
text += " mV";
text += "\n\nOBD-II / CAN\n";
text += "\n\nPassive CAN Logger\n";
text += " TWAI: ";
text += obd_can_started ? "started" : "offline";
text += " @ 500 kbps";
text += "\n Pins: TX GPIO15 / RX GPIO16";
text += "\n Coolant: ";
text += obd_coolant_temp_f > -100 ? String(obd_coolant_temp_f) + "°F" : String("--");
text += "\n Requests OK/fail: ";
text += String(obd_tx_count);
text += "/";
text += String(obd_tx_fail_count);
text += "\n Raw RX/coolant RX: ";
text += "\n Mode: listen-only (no transmit)";
text += "\n Raw RX: ";
text += String(obd_raw_rx_count);
text += "/";
text += String(obd_rx_count);
text += "\n Last frame: ";
if (obd_raw_rx_count > 0) {