From f182df290f6dafa73823f76835d917e881b18716 Mon Sep 17 00:00:00 2001 From: kykaevv Date: Tue, 1 Sep 2026 13:26:49 +0300 Subject: [PATCH] Add T31 CAN fold-on-lock Arduino firmware. Co-authored-by: Cursor --- .gitignore | 9 + README.md | 80 ++++++++ firmware/CanSniffer/CanSniffer.ino | 230 +++++++++++++++++++++ firmware/CanSniffer/can_ids.h | 8 + firmware/CanSniffer/config.h | 5 + firmware/XTrailMirrors/XTrailMirrors.ino | 247 +++++++++++++++++++++++ firmware/XTrailMirrors/can_ids.h | 45 +++++ firmware/XTrailMirrors/config.h | 39 ++++ firmware/shared/can_ids.h | 41 ++++ 9 files changed, 704 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 firmware/CanSniffer/CanSniffer.ino create mode 100644 firmware/CanSniffer/can_ids.h create mode 100644 firmware/CanSniffer/config.h create mode 100644 firmware/XTrailMirrors/XTrailMirrors.ino create mode 100644 firmware/XTrailMirrors/can_ids.h create mode 100644 firmware/XTrailMirrors/config.h create mode 100644 firmware/shared/can_ids.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67d84dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.pio/ +.vscode/ +*.elf +*.hex +*.bin +*.map +build/ +*.bak +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..6285bad --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# AndruinoXTrail — Nissan X-Trail T31 + +Складывание зеркал при закрытии машины. Arduino + MCP2515, шина CAN COMM 500 кбит/с. + +## Кузов T31 + +T31 (2007–2014, в том числе 2014 рестайл) — та же CAN-семья, что Qashqai J10: одна основная шина **CAN COMM**, 500 кбит/с, 11-битные ID. Закрытие дверей BCM публикует в циклических кадрах. Команды «сложить зеркала» на этой шине нет: штатная кнопка в двери меняет полярность моторов. + +Схема: + +1. Слушаем **закрытие** по CAN (`0x60D` / `0x358`). +2. Складываем зеркала **реле**, параллельно штатной кнопке. +3. Replay кадра кнопки по CAN на T31 почти никогда не нужен: кнопки на OBD нет. + +Колодка кнопки складывания у дорестайла и рестайла T31 разная. Реле ставить параллельно **вашей** кнопке. + +## Кадры CAN COMM + +| ID | Назначение | Полезные байты | +|---|---|---| +| `0x60D` | BCM: двери, зажигание, **замок** | `data[2] & 0x18` — закрыто. `data[1] & 0x0C`: `00` OFF, `04` ACC, `0C` ON, `08` START | +| `0x358` | Детализация замка | `data[5]` bit0 водитель, bit1 остальные двери | +| `0x745` / `0x765` | Диагностика BCM | Не используется | + +Прошивка **не** шлёт команды закрытия/открытия дверей. + +## Куда врезаться + +- OBD: pin 6 CAN-H, pin 14 CAN-L +- или BCM за бардачком, зелёный 40-пин: **pin 20** CAN-H (синий), **pin 40** CAN-L (розовый) + +На MCP2515 **снять** перемычку 120 Ом. + +## Железо + +- Arduino Nano или Uno +- MCP2515 (часто кварц **8 МГц**) +- Два реле 5 В на контакты штатной кнопки складывания + +``` +Arduino MCP2515 +D10 -> CS +D11 -> SI (MOSI) +D12 -> SO (MISO) +D13 -> SCK +D2 -> INT +5V/GND -> VCC/GND + +MCP2515 CAN-H -> OBD 6 (или BCM pin 20) +MCP2515 CAN-L -> OBD 14 (или BCM pin 40) +общий GND с кузовом + +D7 -> реле СЛОЖИТЬ +D8 -> реле РАЗЛОЖИТЬ +``` + +Питание Arduino — OBD pin 16 (+12 В) через стабилизатор 5 В, либо постоянная 12 В с предохранителем. + +Реле **параллельно** штатной кнопке, не вместо неё. Моторы зеркал с Arduino не питать. + +## Прошивки + +Библиотека: [coryjfowler/MCP_CAN_lib](https://github.com/coryjfowler/MCP_CAN_lib) + +1. `firmware/CanSniffer/CanSniffer.ino` — Serial 115200. Закрыли машину → `60D lock=1`, открыли → `lock=0`. Клавиша `f` + кнопка зеркал: если `DIFF: (none)`, оставляйте реле. +2. `firmware/XTrailMirrors/XTrailMirrors.ino` — в `config.h` по умолчанию `FOLD_VIA_RELAY`. + +Если кристалл MCP2515 16 МГц, прошивка сама пробует второй вариант. + +## Поведение + +- Старт: текущий замок запоминается, зеркала не трогаем. +- Замок 0→1 и зажигание OFF → складывание ~8 с. +- Замок 1→0 → раскладывание ~8 с. +- При ON/START автоматика молчит. +- Пауза 12 с между циклами. + +## Безопасность + +Подключайтесь на заглушенном авто, с предохранителем. Сначала только сниффер. Реле не держать дольше ~8–10 с. diff --git a/firmware/CanSniffer/CanSniffer.ino b/firmware/CanSniffer/CanSniffer.ino new file mode 100644 index 0000000..2329460 --- /dev/null +++ b/firmware/CanSniffer/CanSniffer.ino @@ -0,0 +1,230 @@ +/* + * 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 +#include +#include +#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); + } +} diff --git a/firmware/CanSniffer/can_ids.h b/firmware/CanSniffer/can_ids.h new file mode 100644 index 0000000..e00f9d1 --- /dev/null +++ b/firmware/CanSniffer/can_ids.h @@ -0,0 +1,8 @@ +#pragma once + +// Nissan X-Trail T31 — IDs highlighted by the sniffer. +static const unsigned long CAN_ID_BCM_60D = 0x60D; +static const unsigned long CAN_ID_LOCK_358 = 0x358; + +static const uint8_t BCM60D_B1_IGN_MASK = 0x0C; +static const uint8_t BCM60D_B2_LOCKED = 0x18; diff --git a/firmware/CanSniffer/config.h b/firmware/CanSniffer/config.h new file mode 100644 index 0000000..3c705ca --- /dev/null +++ b/firmware/CanSniffer/config.h @@ -0,0 +1,5 @@ +#pragma once + +static const uint8_t MCP_CS_PIN = 10; +static const unsigned long SERIAL_BAUD = 115200; +static const uint8_t MAX_TRACKED_IDS = 48; diff --git a/firmware/XTrailMirrors/XTrailMirrors.ino b/firmware/XTrailMirrors/XTrailMirrors.ino new file mode 100644 index 0000000..114f35e --- /dev/null +++ b/firmware/XTrailMirrors/XTrailMirrors.ino @@ -0,0 +1,247 @@ +/* + * Nissan X-Trail T31 — fold mirrors on lock. + * + * Listens on CAN COMM (500 kbit/s, Qashqai J10 family) for BCM lock status + * (0x60D / 0x358). On a stable lock-with-ignition-off it folds the mirrors. + * Unlock unfolds them. + * + * T31 has no cyclic "fold mirrors" CAN frame. The door fold switch reverses + * motor polarity. Default actuator is a relay across that switch. + * + * Library: https://github.com/coryjfowler/MCP_CAN_lib + * + * Wiring: + * MCP2515 CS -> D10 + * MCP2515 INT -> D2 + * MCP2515 SI -> D11 + * MCP2515 SO -> D12 + * MCP2515 SCK -> D13 + * MCP2515 VCC -> 5V, GND -> GND + * MCP2515 CAN-H / CAN-L -> OBD pin 6 / 14 + * or BCM green 40-pin: pin 20 CAN-H (blue), pin 40 CAN-L (pink) + * Remove the 120 ohm jumper on the MCP2515 board. + * + * Relay FOLD -> D7 (parallels factory fold switch, fold direction) + * Relay UNFOLD -> D8 (unfold direction) + */ + +#include +#include +#include +#include "config.h" + +static const uint8_t MCP_CS_PIN = 10; +MCP_CAN CAN(MCP_CS_PIN); + +enum MirrorAction { + MIRROR_IDLE = 0, + MIRROR_FOLD, + MIRROR_UNFOLD +}; + +static bool saw60d = false; +static bool saw358 = false; +static bool locked60d = false; +static bool locked358 = false; +static bool ignOn = false; + +static bool stableLocked = false; +static bool prevStableLocked = false; +static bool lockArmed = false; +static unsigned long lockEdgeMs = 0; + +static MirrorAction action = MIRROR_IDLE; +static unsigned long actionStartMs = 0; +static unsigned long lastActionMs = 0; +static uint8_t canBurstLeft = 0; +static unsigned long nextCanMs = 0; + +static void relayWrite(uint8_t pin, bool on) { + if (RELAY_ACTIVE_LOW) { + digitalWrite(pin, on ? LOW : HIGH); + } else { + digitalWrite(pin, on ? HIGH : LOW); + } +} + +static void relaysOff() { + relayWrite(PIN_RELAY_FOLD, false); + relayWrite(PIN_RELAY_UNFOLD, false); +} + +static bool canStartAction(MirrorAction next) { + unsigned long now = millis(); + if (action != MIRROR_IDLE) { + return false; + } + if (now - lastActionMs < ACTION_COOLDOWN_MS && lastActionMs != 0) { + return false; + } + action = next; + actionStartMs = now; + lastActionMs = now; + +#if FOLD_ACTUATOR == FOLD_VIA_RELAY + relaysOff(); + if (next == MIRROR_FOLD) { + relayWrite(PIN_RELAY_FOLD, true); + } else { + relayWrite(PIN_RELAY_UNFOLD, true); + } + Serial.println(next == MIRROR_FOLD ? F("RELAY fold") : F("RELAY unfold")); +#else + if (FOLD_CAN_FRAME_COUNT == 0 || FOLD_CAN_ID == 0) { + Serial.println(F("CAN replay not configured — capture frames first")); + action = MIRROR_IDLE; + return false; + } + canBurstLeft = FOLD_CAN_FRAME_COUNT; + nextCanMs = now; + Serial.println(next == MIRROR_FOLD ? F("CAN fold") : F("CAN unfold")); +#endif + return true; +} + +static void serviceAction() { + if (action == MIRROR_IDLE) { + return; + } + unsigned long now = millis(); + +#if FOLD_ACTUATOR == FOLD_VIA_RELAY + if (now - actionStartMs >= MIRROR_PULSE_MS) { + relaysOff(); + action = MIRROR_IDLE; + Serial.println(F("RELAY done")); + } +#else + if (canBurstLeft > 0 && now >= nextCanMs) { + const unsigned long id = (action == MIRROR_FOLD) ? FOLD_CAN_ID : UNFOLD_CAN_ID; + const uint8_t len = (action == MIRROR_FOLD) ? FOLD_CAN_LEN : UNFOLD_CAN_LEN; + uint8_t buf[8]; + const uint8_t *src = (action == MIRROR_FOLD) ? FOLD_CAN_DATA : UNFOLD_CAN_DATA; + memcpy(buf, src, 8); + CAN.sendMsgBuf(id, 0, len, buf); + canBurstLeft--; + nextCanMs = now + FOLD_CAN_INTERVAL_MS; + } + if (canBurstLeft == 0 && now - actionStartMs >= 500) { + action = MIRROR_IDLE; + Serial.println(F("CAN replay done")); + } +#endif +} + +static bool currentlyLocked() { + if (saw60d && saw358) { + return locked60d && locked358; + } + if (saw60d) { + return locked60d; + } + if (saw358) { + return locked358; + } + return false; +} + +static void onLockState(bool locked) { + unsigned long now = millis(); + if (!lockArmed) { + stableLocked = locked; + prevStableLocked = locked; + lockArmed = true; + Serial.print(F("baseline LOCK=")); + Serial.println(stableLocked ? 1 : 0); + return; + } + if (locked != stableLocked) { + if (lockEdgeMs == 0) { + lockEdgeMs = now; + } + if (now - lockEdgeMs >= LOCK_STABLE_MS) { + prevStableLocked = stableLocked; + stableLocked = locked; + lockEdgeMs = 0; + + Serial.print(F("LOCK=")); + Serial.print(stableLocked ? 1 : 0); + Serial.print(F(" IGN=")); + Serial.println(ignOn ? 1 : 0); + + if (REQUIRE_IGN_OFF && ignOn) { + return; + } + + if (stableLocked && !prevStableLocked) { + canStartAction(MIRROR_FOLD); + } else if (!stableLocked && prevStableLocked) { + canStartAction(MIRROR_UNFOLD); + } + } + } else { + lockEdgeMs = 0; + } +} + +static void parseFrame(unsigned long id, uint8_t len, const uint8_t *data) { + if (id == CAN_ID_BCM_60D && len >= 3) { + saw60d = true; + locked60d = bcm60dLocked(data[2]); + ignOn = bcm60dIgnOn(data[1]); + onLockState(currentlyLocked()); + } else if (id == CAN_ID_LOCK_358 && len >= 6) { + saw358 = true; + locked358 = lock358FullyLocked(data[5]); + onLockState(currentlyLocked()); + } +} + +void setup() { + pinMode(PIN_RELAY_FOLD, OUTPUT); + pinMode(PIN_RELAY_UNFOLD, OUTPUT); + relaysOff(); + + Serial.begin(SERIAL_BAUD); + while (!Serial && millis() < 2000) { + } + + Serial.println(F("XTrail T31 fold-on-lock")); + + // If init fails, try MCP_16MHZ — some MCP2515 boards use a 16 MHz crystal. + if (CAN.begin(MCP_STDEXT, CAN_500KBPS, MCP_8MHZ) != CAN_OK) { + Serial.println(F("MCP2515 8 MHz failed, trying 16 MHz")); + if (CAN.begin(MCP_STDEXT, CAN_500KBPS, MCP_16MHZ) != CAN_OK) { + Serial.println(F("MCP2515 init failed")); + while (true) { + delay(1000); + } + } + } + + CAN.init_Mask(0, 0, 0x07FF); + CAN.init_Filt(0, 0, CAN_ID_BCM_60D); + CAN.init_Filt(1, 0, CAN_ID_LOCK_358); + CAN.init_Mask(1, 0, 0x07FF); + CAN.init_Filt(2, 0, CAN_ID_BCM_60D); + CAN.init_Filt(3, 0, CAN_ID_LOCK_358); + CAN.init_Filt(4, 0, CAN_ID_BCM_60D); + CAN.init_Filt(5, 0, CAN_ID_LOCK_358); + CAN.setMode(MCP_NORMAL); + + Serial.println(F("CAN 500k OK, waiting for 0x60D / 0x358")); +} + +void loop() { + unsigned long id = 0; + uint8_t len = 0; + uint8_t data[8]; + + if (CAN.checkReceive() == CAN_MSGAVAIL) { + CAN.readMsgBuf(&len, data); + id = CAN.getCanId(); + parseFrame(id, len, data); + } + + serviceAction(); +} diff --git a/firmware/XTrailMirrors/can_ids.h b/firmware/XTrailMirrors/can_ids.h new file mode 100644 index 0000000..e6af717 --- /dev/null +++ b/firmware/XTrailMirrors/can_ids.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +// Nissan X-Trail T31 (2007–2014, including 2014 facelift). +// Same CAN COMM family as Qashqai J10: 500 kbit/s, 11-bit IDs. +// Tap: OBD pin 6/14, or BCM behind the glovebox (green 40-pin: +// pin 20 CAN-H blue, pin 40 CAN-L pink). +// +// Byte 0 = first data byte. Bit 0 = LSB of that byte. + +static const unsigned long CAN_ID_BCM_60D = 0x60D; + +static const uint8_t BCM60D_B0_DRIVER_OPEN = 0x10; +static const uint8_t BCM60D_B0_PASSENGER_OPEN = 0x20; +static const uint8_t BCM60D_B0_REAR_LH_OPEN = 0x40; +static const uint8_t BCM60D_B0_REAR_RH_OPEN = 0x80; + +// data[1] bits 2–3: 00 OFF, 01 ACC, 11 ON, 10 START +static const uint8_t BCM60D_B1_IGN_MASK = 0x0C; +static const uint8_t BCM60D_B1_IGN_OFF = 0x00; +static const uint8_t BCM60D_B1_IGN_ACC = 0x04; +static const uint8_t BCM60D_B1_IGN_START = 0x08; +static const uint8_t BCM60D_B1_IGN_ON = 0x0C; + +// data[2] C.4 / C.5 — any door locked +static const uint8_t BCM60D_B2_LOCKED = 0x18; + +static const unsigned long CAN_ID_LOCK_358 = 0x358; +static const uint8_t LOCK358_B5_DRIVER_LOCKED = 0x01; +static const uint8_t LOCK358_B5_OTHER_LOCKED = 0x02; + +inline bool bcm60dIgnOn(uint8_t b1) { + uint8_t ign = b1 & BCM60D_B1_IGN_MASK; + return ign == BCM60D_B1_IGN_ON || ign == BCM60D_B1_IGN_START; +} + +inline bool bcm60dLocked(uint8_t b2) { + return (b2 & BCM60D_B2_LOCKED) != 0; +} + +inline bool lock358FullyLocked(uint8_t b5) { + return (b5 & (LOCK358_B5_DRIVER_LOCKED | LOCK358_B5_OTHER_LOCKED)) == + (LOCK358_B5_DRIVER_LOCKED | LOCK358_B5_OTHER_LOCKED); +} diff --git a/firmware/XTrailMirrors/config.h b/firmware/XTrailMirrors/config.h new file mode 100644 index 0000000..5be8e85 --- /dev/null +++ b/firmware/XTrailMirrors/config.h @@ -0,0 +1,39 @@ +#pragma once + +#include "can_ids.h" + +// Fold actuator: +// FOLD_VIA_RELAY — emulate the factory fold switch (T31: analog in the door) +// FOLD_VIA_CAN — replay frames captured with CanSniffer while pressing +// the door fold button. On T31 that button is not on OBD CAN. +#define FOLD_VIA_RELAY 1 +#define FOLD_VIA_CAN 2 +#define FOLD_ACTUATOR FOLD_VIA_RELAY + +// Relay pins. Typical Arduino relay module is active LOW. +static const uint8_t PIN_RELAY_FOLD = 7; +static const uint8_t PIN_RELAY_UNFOLD = 8; +static const bool RELAY_ACTIVE_LOW = true; + +// Mirror motors need ~8 s. Do not overlap fold/unfold. +static const uint16_t MIRROR_PULSE_MS = 8000; +static const uint16_t LOCK_STABLE_MS = 400; +static const uint16_t ACTION_COOLDOWN_MS = 12000; + +// Fold only after ignition is off (do not fold while driving). +static const bool REQUIRE_IGN_OFF = true; + +static const unsigned long SERIAL_BAUD = 115200; + +// Fill these after a successful CanSniffer capture of the fold button. +// Leave FOLD_CAN_FRAME_COUNT = 0 until you have real payloads from THIS car. +static const unsigned long FOLD_CAN_ID = 0x000; +static const uint8_t FOLD_CAN_LEN = 8; +static const uint8_t FOLD_CAN_DATA[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + +static const unsigned long UNFOLD_CAN_ID = 0x000; +static const uint8_t UNFOLD_CAN_LEN = 8; +static const uint8_t UNFOLD_CAN_DATA[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + +static const uint8_t FOLD_CAN_FRAME_COUNT = 0; // set to 10+ after capture +static const uint16_t FOLD_CAN_INTERVAL_MS = 40; diff --git a/firmware/shared/can_ids.h b/firmware/shared/can_ids.h new file mode 100644 index 0000000..5abbdd9 --- /dev/null +++ b/firmware/shared/can_ids.h @@ -0,0 +1,41 @@ +/* + * Nissan X-Trail T31 CAN map — lock, doors, ignition, mirrors. + * + * Platform: T31 (2007–2014). Same CAN COMM as Qashqai J10. + * Bus: 500 kbit/s, 11-bit IDs. + * + * Tap: + * OBD-II pin 6 CAN-H, pin 14 CAN-L + * or BCM (behind glovebox), green 40-pin: pin 20 CAN-H blue, pin 40 CAN-L pink + * Do not fit extra 120 ohm — the bus is already terminated. + * + * T31 has electric folding mirrors on some trims, but BCM does not publish + * a cyclic "fold mirrors" frame. The door switch reverses motor polarity. + */ + +#pragma once + +#define CAN_XTRAIL_BCM_STATUS 0x60D +/* + * 0x60D data[0]: door-open bits (driver bit4 … rear RH bit7) + * 0x60D data[1] bits 2-3 ignition: 00 OFF, 01 ACC, 11 ON, 10 START + * 0x60D data[2] bits 3-4 locked (mask 0x18) + */ + +#define CAN_XTRAIL_LOCK_DETAIL 0x358 +/* + * 0x358 data[5]: bit0 driver locked, bit1 other doors locked + */ + +#define CAN_XTRAIL_BCM_DIAG_REQ 0x745 +#define CAN_XTRAIL_BCM_DIAG_RES 0x765 +/* + * CONSULT ISO-TP. Not used for lock/unlock or fold in this project. + */ + +/* + * Mirror fold/unfold is analog on T31 (switch in the door). + * Facelift T31 (≈2010–2014) uses a different fold-switch connector + * than pre-facelift. Parallel the switch with relays, do not drive + * motors from the Arduino. + */