AndruinoXTrail/firmware/CanSniffer/CanSniffer.ino
kykaevv f182df290f Add T31 CAN fold-on-lock Arduino firmware.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 13:26:49 +03:00

231 lines
6.2 KiB
C++

/*
* Nissan X-Trail T31 — CAN sniffer for lock and mirror-fold capture.
*
* Use this first. Park, ignition OFF, serial 115200.
*
* Commands:
* l — mark LOCK snapshot (press after locking the car)
* u — mark UNLOCK snapshot
* f — mark FOLD snapshot (press after tapping the door fold switch)
* o — mark UNFOLD / open-mirrors snapshot
* d — dump last frames of all tracked IDs
* s — silent (only print 0x60D / 0x358)
* a — print all frames (verbose)
*
* Capture method for fold:
* 1. Type 'd' to see live IDs.
* 2. Type 'f', then immediately press the factory fold button.
* 3. Frames that changed vs the previous dump are printed as DIFF.
* Copy any new ID+payload into XTrailMirrors/config.h (FOLD_CAN_*).
*
* If DIFF is empty, the fold switch is not on this CAN. Use relay mode.
*
* Library: https://github.com/coryjfowler/MCP_CAN_lib
* MCP2515: CS=D10 INT=D2, 8 MHz crystal, 120 ohm jumper OFF.
* OBD: CAN-H pin 6, CAN-L pin 14.
* BCM (glovebox, green 40-pin): pin 20 CAN-H blue, pin 40 CAN-L pink.
*/
#include <SPI.h>
#include <string.h>
#include <mcp_can.h>
#include "config.h"
#include "can_ids.h"
MCP_CAN CAN(MCP_CS_PIN);
struct Tracked {
unsigned long id;
uint8_t len;
uint8_t data[8];
bool used;
};
static Tracked tracked[MAX_TRACKED_IDS];
static bool verbose = false;
static bool sawLockPrint = false;
static bool lastLocked = false;
static uint8_t last358 = 0xFF;
static Tracked *findOrAlloc(unsigned long id) {
Tracked *empty = 0;
for (uint8_t i = 0; i < MAX_TRACKED_IDS; i++) {
if (tracked[i].used && tracked[i].id == id) {
return &tracked[i];
}
if (!tracked[i].used && empty == 0) {
empty = &tracked[i];
}
}
return empty;
}
static void printFrame(unsigned long id, uint8_t len, const uint8_t *data) {
char line[80];
snprintf(line, sizeof(line), "%03lX [%u]", id, len);
Serial.print(line);
for (uint8_t i = 0; i < len && i < 8; i++) {
snprintf(line, sizeof(line), " %02X", data[i]);
Serial.print(line);
}
Serial.println();
}
static void printLockDecode(unsigned long id, uint8_t len, const uint8_t *data) {
if (id == CAN_ID_BCM_60D && len >= 3) {
bool locked = (data[2] & BCM60D_B2_LOCKED) != 0;
uint8_t ign = data[1] & BCM60D_B1_IGN_MASK;
if (locked != lastLocked || !sawLockPrint) {
Serial.print(F("60D lock="));
Serial.print(locked ? 1 : 0);
Serial.print(F(" ign="));
Serial.print(ign, HEX);
Serial.print(F(" b0="));
Serial.println(data[0], HEX);
lastLocked = locked;
sawLockPrint = true;
}
} else if (id == CAN_ID_LOCK_358 && len >= 6) {
if (data[5] != last358) {
last358 = data[5];
Serial.print(F("358 b5="));
Serial.println(data[5], HEX);
}
}
}
static void dumpAll(const __FlashStringHelper *tag) {
Serial.println(tag);
for (uint8_t i = 0; i < MAX_TRACKED_IDS; i++) {
if (tracked[i].used) {
printFrame(tracked[i].id, tracked[i].len, tracked[i].data);
}
}
}
static void snapshotAndDiff(const __FlashStringHelper *tag) {
Serial.println(tag);
Serial.println(F("Press the control now. Changed IDs follow (2s window)."));
Tracked before[MAX_TRACKED_IDS];
memcpy(before, tracked, sizeof(tracked));
unsigned long t0 = millis();
while (millis() - t0 < 2000) {
unsigned long id = 0;
uint8_t len = 0;
uint8_t data[8];
if (CAN.checkReceive() == CAN_MSGAVAIL) {
CAN.readMsgBuf(&len, data);
id = CAN.getCanId();
Tracked *slot = findOrAlloc(id);
if (slot) {
slot->id = id;
slot->len = len;
memset(slot->data, 0, 8);
memcpy(slot->data, data, len > 8 ? 8 : len);
slot->used = true;
}
}
}
Serial.println(F("DIFF:"));
bool any = false;
for (uint8_t i = 0; i < MAX_TRACKED_IDS; i++) {
if (!tracked[i].used) {
continue;
}
Tracked *old = 0;
for (uint8_t j = 0; j < MAX_TRACKED_IDS; j++) {
if (before[j].used && before[j].id == tracked[i].id) {
old = &before[j];
break;
}
}
bool changed = false;
if (old == 0) {
changed = true;
} else if (old->len != tracked[i].len ||
memcmp(old->data, tracked[i].data, 8) != 0) {
changed = true;
}
if (changed) {
any = true;
Serial.print(F(" "));
printFrame(tracked[i].id, tracked[i].len, tracked[i].data);
}
}
if (!any) {
Serial.println(F(" (none — this action is probably not on OBD CAN)"));
}
}
void setup() {
Serial.begin(SERIAL_BAUD);
while (!Serial && millis() < 2000) {
}
memset(tracked, 0, sizeof(tracked));
Serial.println(F("XTrail T31 CAN sniffer"));
Serial.println(F("keys: l=lock u=unlock f=fold o=unfold d=dump s=quiet a=all"));
if (CAN.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ) != CAN_OK) {
Serial.println(F("MCP2515 8 MHz failed, trying 16 MHz"));
if (CAN.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) != CAN_OK) {
Serial.println(F("MCP2515 init failed"));
while (true) {
delay(1000);
}
}
}
CAN.setMode(MCP_NORMAL);
Serial.println(F("CAN 500k listening"));
}
void loop() {
if (Serial.available()) {
char c = (char)Serial.read();
if (c == 'l' || c == 'L') {
snapshotAndDiff(F("CAPTURE LOCK"));
} else if (c == 'u' || c == 'U') {
snapshotAndDiff(F("CAPTURE UNLOCK"));
} else if (c == 'f' || c == 'F') {
snapshotAndDiff(F("CAPTURE FOLD BUTTON"));
} else if (c == 'o' || c == 'O') {
snapshotAndDiff(F("CAPTURE UNFOLD BUTTON"));
} else if (c == 'd' || c == 'D') {
dumpAll(F("DUMP"));
} else if (c == 's' || c == 'S') {
verbose = false;
Serial.println(F("quiet: 60D/358 only"));
} else if (c == 'a' || c == 'A') {
verbose = true;
Serial.println(F("verbose: all frames"));
}
}
unsigned long id = 0;
uint8_t len = 0;
uint8_t data[8];
if (CAN.checkReceive() != CAN_MSGAVAIL) {
return;
}
CAN.readMsgBuf(&len, data);
id = CAN.getCanId();
Tracked *slot = findOrAlloc(id);
if (slot) {
slot->id = id;
slot->len = len;
memset(slot->data, 0, 8);
memcpy(slot->data, data, len > 8 ? 8 : len);
slot->used = true;
}
if (verbose) {
printFrame(id, len, data);
} else if (id == CAN_ID_BCM_60D || id == CAN_ID_LOCK_358) {
printLockDecode(id, len, data);
}
}