248 lines
6.2 KiB
C++
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();
|
|
}
|