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