Add T31 CAN fold-on-lock Arduino firmware.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
f182df290f
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.pio/
|
||||
.vscode/
|
||||
*.elf
|
||||
*.hex
|
||||
*.bin
|
||||
*.map
|
||||
build/
|
||||
*.bak
|
||||
Thumbs.db
|
||||
80
README.md
Normal file
80
README.md
Normal file
@ -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 с.
|
||||
230
firmware/CanSniffer/CanSniffer.ino
Normal file
230
firmware/CanSniffer/CanSniffer.ino
Normal file
@ -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 <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);
|
||||
}
|
||||
}
|
||||
8
firmware/CanSniffer/can_ids.h
Normal file
8
firmware/CanSniffer/can_ids.h
Normal file
@ -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;
|
||||
5
firmware/CanSniffer/config.h
Normal file
5
firmware/CanSniffer/config.h
Normal file
@ -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;
|
||||
247
firmware/XTrailMirrors/XTrailMirrors.ino
Normal file
247
firmware/XTrailMirrors/XTrailMirrors.ino
Normal file
@ -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 <SPI.h>
|
||||
#include <string.h>
|
||||
#include <mcp_can.h>
|
||||
#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();
|
||||
}
|
||||
45
firmware/XTrailMirrors/can_ids.h
Normal file
45
firmware/XTrailMirrors/can_ids.h
Normal file
@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
39
firmware/XTrailMirrors/config.h
Normal file
39
firmware/XTrailMirrors/config.h
Normal file
@ -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;
|
||||
41
firmware/shared/can_ids.h
Normal file
41
firmware/shared/can_ids.h
Normal file
@ -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.
|
||||
*/
|
||||
Loading…
Reference in New Issue
Block a user