AndruinoXTrail/firmware/XTrailMirrors/XTrailMirrors.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

248 lines
6.2 KiB
C++

/*
* 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();
}