Add console CAN controller node

This commit is contained in:
seo-amugae committed 2026-08-23 19:27:12 +09:00
1 parent a130726907
commit 99a1850a21
3 files changed
+1366 -99

No files matched your search

+551
View File
@@ -3,6 +3,8 @@
// Automatic seat controller with EEPROM settings and optional serial tools.
//
#include <EEPROM.h>
#include <SPI.h>
#include <mcp_can.h>
#define seatRelayD8 8 // HIGH = OFF, LOW = ON (up relay)
#define seatRelayD9 9 // HIGH = OFF, LOW = ON (down relay)
@@ -45,11 +47,59 @@ const unsigned long AUTO_CALIBRATION_DOWN_MS = 6000UL;
const unsigned long DEBUG_INTERVAL_MS = 1000UL;
const long MANUAL_STEP_MS = 500;
// Chaegeon-CAN heartbeat. MCP2515 module uses an 8MHz crystal.
const byte CAN_CS_PIN = 10;
const unsigned long CAN_ID_MAIN_HEARTBEAT = 0x100UL;
const unsigned long CAN_ID_CONSOLE_COMMAND = 0x101UL;
const unsigned long CAN_ID_CONSOLE_STATUS = 0x200UL;
const unsigned long CAN_HEARTBEAT_INTERVAL_MS = 250UL;
// Center-console node protocol. Nano stores memory and sends pulse commands.
const unsigned long CONSOLE_CAN_TIMEOUT_MS = 2000UL;
const unsigned long CONSOLE_COMMAND_HOLD_MS = 2500UL;
const unsigned long CONSOLE_MANUAL_LEARN_SETTLE_MS = 700UL;
const unsigned long CONSOLE_STARTUP_DELAY_MS = 3000UL;
const unsigned long CONSOLE_MEMORY_REFRESH_INTERVAL_MS = 1170000UL; // 19m 30s
const unsigned long CONSOLE_STEERING_RESTART_INTERVAL_MS = 1170000UL;
const unsigned int CONSOLE_SHORT_PRESS_MS = 300;
const unsigned int CONSOLE_SHORT_PRESS_GAP_MS = 450;
const unsigned int CONSOLE_CLIMATE_LONG_OFF_MS = 2100;
const unsigned int CONSOLE_CLIMATE_LONG_OFF_GAP_MS = 700;
const unsigned int CONSOLE_STEERING_RESTART_GAP_MS = 1200;
const byte CONSOLE_CHANNEL_COUNT = 7;
const byte CONSOLE_CLIMATE_COUNT = 4;
const byte CONSOLE_TOGGLE_COUNT = 3;
const byte CONSOLE_PULSE_QUEUE_SIZE = 24;
const byte CH_DRIVER_VENT = 0;
const byte CH_DRIVER_HEAT = 1;
const byte CH_PASSENGER_VENT = 2;
const byte CH_PASSENGER_HEAT = 3;
const byte CH_STEERING_HEAT = 4;
const byte CH_FRONT_SENSOR = 5;
const byte CH_VESS = 6;
const byte TG_STEERING_HEAT = 0;
const byte TG_FRONT_SENSOR = 1;
const byte TG_VESS = 2;
const byte CONSOLE_OPPOSITE_CLIMATE[CONSOLE_CLIMATE_COUNT] = {
CH_DRIVER_HEAT,
CH_DRIVER_VENT,
CH_PASSENGER_HEAT,
CH_PASSENGER_VENT
};
// EEPROM map.
const int EEPROM_MAGIC_ADDR = 0;
const byte EEPROM_MAGIC_VALUE = 0x64;
const int EEPROM_USE_COUNT_ADDR = 1;
const int EEPROM_SEAT_POSITION_ADDR = 2; // uint16_t, low byte first
const int EEPROM_CONSOLE_MAGIC_ADDR = 16;
const byte EEPROM_CONSOLE_MAGIC_VALUE = 0xC2;
const int EEPROM_CONSOLE_CLIMATE_TARGET_ADDR = 17; // 4 bytes, driver vent/heat/passenger vent/heat
const int EEPROM_CONSOLE_TOGGLE_TARGET_ADDR = 21; // 3 bytes, steering/front sensor/VESS
const byte CALIBRATION_IDLE = 0;
const byte CALIBRATION_UP = 1;
@@ -92,6 +142,38 @@ bool serialIntroPrinted = false;
bool debugEnabled = false;
bool manualStepStatusPending = false;
MCP_CAN chaegeonCan(CAN_CS_PIN);
bool chaegeonCanReady = false;
byte chaegeonCanSeq = 0;
unsigned long lastCanHeartbeatTime = 0;
bool consoleAlive = false;
bool consoleBusy = false;
bool consoleStartupApplyDone = false;
bool previousConsoleIgnitionOn = false;
bool consoleIgnitionApplyPending = false;
bool consoleCommandInFlight = false;
byte consoleCommandSeq = 0;
byte consoleCommandInFlightSeq = 0;
byte consoleLastAcceptedSeq = 0;
byte consoleActualClimatePack = 0;
byte consoleActualToggleBits = 0;
byte consoleButtonBits = 0;
byte consolePreviousButtonBits = 0;
byte consoleClimateTarget[CONSOLE_CLIMATE_COUNT] = {0, 0, 0, 0};
bool consoleToggleTarget[CONSOLE_TOGGLE_COUNT] = {false, false, false};
bool consoleManualLearnPending[CONSOLE_CHANNEL_COUNT] = {false};
unsigned long consoleManualLearnTime[CONSOLE_CHANNEL_COUNT] = {0};
unsigned long lastConsoleStatusTime = 0;
unsigned long lastConsoleCommandTime = 0;
unsigned long lastConsoleClimateRefreshTime = 0;
unsigned long lastConsoleSteeringRestartTime = 0;
byte consolePulseQueueChannel[CONSOLE_PULSE_QUEUE_SIZE] = {0};
unsigned int consolePulseQueueOnMs[CONSOLE_PULSE_QUEUE_SIZE] = {0};
unsigned int consolePulseQueueGapMs[CONSOLE_PULSE_QUEUE_SIZE] = {0};
byte consolePulseQueueHead = 0;
byte consolePulseQueueTail = 0;
void setup() {
Serial.begin(115200);
Serial.setTimeout(25);
@@ -103,6 +185,7 @@ void setup() {
applySeatAction(0);
seat(0);
setupChaegeonCan();
loadSettings();
measurement();
@@ -137,6 +220,7 @@ void loop() {
registerIgnitionOffUse(currentTime);
}
prevIgnitionState = ignitionState;
updateChaegeonCan(currentTime);
if (autoCalibrationState != CALIBRATION_IDLE) {
updateAutoCalibration(currentTime);
@@ -538,6 +622,8 @@ void loadSettings() {
storedSeatPositionMs = SEAT_HARD_LIMIT_UP_MS;
writeUint16(EEPROM_SEAT_POSITION_ADDR, storedSeatPositionMs);
}
loadConsoleSettings();
}
uint16_t readUint16(int addr) {
@@ -604,6 +690,7 @@ void printStatus(const __FlashStringHelper *tag) {
printNamedBool(F("forceCalibration"), forceCalibration);
printNamedInt(F("autoCalibrationState"), autoCalibrationState);
printAutoCalibrationDelayStatus();
printConsoleStatusFields();
}
void printDebugLine() {
@@ -631,6 +718,7 @@ void printDebugLine() {
printNamedBool(F("forceCalibration"), forceCalibration);
printNamedInt(F("autoCalibrationState"), autoCalibrationState);
printAutoCalibrationDelayStatus();
printConsoleStatusFields();
}
void printUptime() {
@@ -715,6 +803,469 @@ void printAutoCalibrationDelayStatus() {
Serial.println();
}
void printConsoleStatusFields() {
printNamedBool(F("consoleCanReady"), chaegeonCanReady);
printNamedBool(F("consoleAlive"), consoleAlive);
printNamedBool(F("consoleBusy"), consoleBusy);
printNamedInt(F("consolePulseQueueLength"), consolePulseQueueLength());
printNamedInt(F("consoleDriverVentTarget"), consoleClimateTarget[CH_DRIVER_VENT]);
printNamedInt(F("consoleDriverHeatTarget"), consoleClimateTarget[CH_DRIVER_HEAT]);
printNamedInt(F("consolePassengerVentTarget"), consoleClimateTarget[CH_PASSENGER_VENT]);
printNamedInt(F("consolePassengerHeatTarget"), consoleClimateTarget[CH_PASSENGER_HEAT]);
printNamedBool(F("consoleSteeringHeatTarget"), consoleToggleTarget[TG_STEERING_HEAT]);
printNamedBool(F("consoleFrontSensorTarget"), consoleToggleTarget[TG_FRONT_SENSOR]);
printNamedBool(F("consoleVessTarget"), consoleToggleTarget[TG_VESS]);
printNamedInt(F("consoleDriverVentActual"), getConsoleClimateActual(CH_DRIVER_VENT));
printNamedInt(F("consoleDriverHeatActual"), getConsoleClimateActual(CH_DRIVER_HEAT));
printNamedInt(F("consolePassengerVentActual"), getConsoleClimateActual(CH_PASSENGER_VENT));
printNamedInt(F("consolePassengerHeatActual"), getConsoleClimateActual(CH_PASSENGER_HEAT));
printNamedBool(F("consoleSteeringHeatActual"), getConsoleToggleActual(TG_STEERING_HEAT));
printNamedBool(F("consoleFrontSensorActual"), getConsoleToggleActual(TG_FRONT_SENSOR));
printNamedBool(F("consoleVessActual"), getConsoleToggleActual(TG_VESS));
}
void updateChaegeonCan(unsigned long currentTime) {
updateChaegeonCanReceive(currentTime);
updateConsoleAlive(currentTime);
updateChaegeonCanHeartbeat(currentTime);
updateConsoleManualLearn(currentTime);
updateConsoleAutomation(currentTime);
updateConsoleCommandSender(currentTime);
}
void updateChaegeonCanReceive(unsigned long currentTime) {
if (!chaegeonCanReady) {
return;
}
while (chaegeonCan.checkReceive() == CAN_MSGAVAIL) {
unsigned long rxId = 0;
byte len = 0;
byte payload[8] = {0};
if (chaegeonCan.readMsgBuf(&rxId, &len, payload) != CAN_OK) {
return;
}
if (rxId == CAN_ID_CONSOLE_STATUS && len >= 6 && payload[0] == 1) {
consoleBusy = (payload[1] & 0x02) != 0;
consoleActualClimatePack = payload[2];
consoleActualToggleBits = payload[3];
consoleLastAcceptedSeq = payload[5];
lastConsoleStatusTime = currentTime;
consoleAlive = true;
if (consoleCommandInFlight &&
consoleLastAcceptedSeq == consoleCommandInFlightSeq) {
consoleCommandInFlight = false;
}
handleConsoleButtonBits(payload[4], currentTime);
}
}
}
void updateConsoleAlive(unsigned long currentTime) {
bool aliveNow = chaegeonCanReady &&
lastConsoleStatusTime != 0 &&
currentTime - lastConsoleStatusTime <= CONSOLE_CAN_TIMEOUT_MS;
if (!aliveNow && consoleAlive) {
consoleStartupApplyDone = false;
consoleCommandInFlight = false;
clearConsolePulseQueue();
}
consoleAlive = aliveNow;
}
void handleConsoleButtonBits(byte newButtonBits, unsigned long currentTime) {
consolePreviousButtonBits = consoleButtonBits;
consoleButtonBits = newButtonBits;
for (byte channel = 0; channel < CONSOLE_CHANNEL_COUNT; channel++) {
bool wasPressed = (consolePreviousButtonBits & (1 << channel)) != 0;
bool isPressed = (consoleButtonBits & (1 << channel)) != 0;
if (wasPressed && !isPressed) {
consoleManualLearnPending[channel] = true;
consoleManualLearnTime[channel] = currentTime + CONSOLE_MANUAL_LEARN_SETTLE_MS;
}
}
}
void updateConsoleManualLearn(unsigned long currentTime) {
for (byte channel = 0; channel < CONSOLE_CHANNEL_COUNT; channel++) {
if (!consoleManualLearnPending[channel]) {
continue;
}
if ((long)(currentTime - consoleManualLearnTime[channel]) < 0) {
continue;
}
consoleManualLearnPending[channel] = false;
learnConsoleManualChannel(channel);
}
}
void learnConsoleManualChannel(byte channel) {
if (channel <= CH_PASSENGER_HEAT) {
setConsoleClimateTarget(channel, getConsoleClimateActual(channel));
return;
}
byte toggleIndex = channel - CH_STEERING_HEAT;
if (toggleIndex < CONSOLE_TOGGLE_COUNT) {
setConsoleToggleTarget(toggleIndex, getConsoleToggleActual(toggleIndex));
}
}
void updateConsoleAutomation(unsigned long currentTime) {
bool ignitionOn = ignitionState == HIGH;
if (ignitionOn && !previousConsoleIgnitionOn) {
consoleIgnitionApplyPending = true;
}
previousConsoleIgnitionOn = ignitionOn;
if (!consoleAlive || !consoleReadyForNewSequence()) {
return;
}
if (!consoleStartupApplyDone && currentTime >= CONSOLE_STARTUP_DELAY_MS) {
applyConsoleClimateTargets(true);
if (ignitionOn) {
applyConsoleIgnitionTargets();
consoleIgnitionApplyPending = false;
}
consoleStartupApplyDone = true;
lastConsoleClimateRefreshTime = currentTime;
lastConsoleSteeringRestartTime = currentTime;
return;
}
if (ignitionOn && consoleIgnitionApplyPending) {
applyConsoleIgnitionTargets();
consoleIgnitionApplyPending = false;
lastConsoleSteeringRestartTime = currentTime;
previousConsoleIgnitionOn = ignitionOn;
return;
}
if (currentTime - lastConsoleClimateRefreshTime >= CONSOLE_MEMORY_REFRESH_INTERVAL_MS) {
applyConsoleClimateTargets(true);
if (ignitionOn) {
applyConsoleIgnitionTargets();
consoleIgnitionApplyPending = false;
}
lastConsoleClimateRefreshTime = currentTime;
return;
}
if (ignitionOn &&
consoleToggleTarget[TG_STEERING_HEAT] &&
currentTime - lastConsoleSteeringRestartTime >= CONSOLE_STEERING_RESTART_INTERVAL_MS) {
restartConsoleSteeringHeat();
lastConsoleSteeringRestartTime = currentTime;
}
}
bool consoleReadyForNewSequence() {
return !consoleBusy &&
!consoleCommandInFlight &&
consolePulseQueueHead == consolePulseQueueTail;
}
void applyConsoleClimateTargets(bool force) {
for (byte climate = 0; climate < CONSOLE_CLIMATE_COUNT; climate++) {
ensureConsoleClimateTarget(climate, force);
}
}
void applyConsoleIgnitionTargets() {
for (byte toggle = 0; toggle < CONSOLE_TOGGLE_COUNT; toggle++) {
ensureConsoleToggleTarget(toggle);
}
}
void ensureConsoleClimateTarget(byte climate, bool force) {
byte target = consoleClimateTarget[climate];
byte actual = getConsoleClimateActual(climate);
if (target == 0) {
if (actual != 0) {
enqueueConsolePulse(climate, CONSOLE_CLIMATE_LONG_OFF_MS, CONSOLE_CLIMATE_LONG_OFF_GAP_MS);
}
return;
}
if (!force && actual == target) {
return;
}
enqueueConsoleClimateSet(climate, target);
}
void ensureConsoleToggleTarget(byte toggle) {
if (getConsoleToggleActual(toggle) == consoleToggleTarget[toggle]) {
return;
}
enqueueConsolePulse(toggle + CH_STEERING_HEAT,
CONSOLE_SHORT_PRESS_MS,
CONSOLE_SHORT_PRESS_GAP_MS);
}
void restartConsoleSteeringHeat() {
if (getConsoleToggleActual(TG_STEERING_HEAT)) {
enqueueConsolePulse(CH_STEERING_HEAT,
CONSOLE_SHORT_PRESS_MS,
CONSOLE_STEERING_RESTART_GAP_MS);
enqueueConsolePulse(CH_STEERING_HEAT,
CONSOLE_SHORT_PRESS_MS,
CONSOLE_SHORT_PRESS_GAP_MS);
} else {
enqueueConsolePulse(CH_STEERING_HEAT,
CONSOLE_SHORT_PRESS_MS,
CONSOLE_SHORT_PRESS_GAP_MS);
}
}
void enqueueConsoleClimateSet(byte climate, byte targetLevel) {
enqueueConsolePulse(climate, CONSOLE_CLIMATE_LONG_OFF_MS, CONSOLE_CLIMATE_LONG_OFF_GAP_MS);
byte shortPresses = getConsoleShortPressCount(targetLevel);
for (byte i = 0; i < shortPresses; i++) {
enqueueConsolePulse(climate, CONSOLE_SHORT_PRESS_MS, CONSOLE_SHORT_PRESS_GAP_MS);
}
}
byte getConsoleShortPressCount(byte level) {
if (level == 3) {
return 1;
}
if (level == 2) {
return 2;
}
if (level == 1) {
return 3;
}
return 0;
}
byte getConsoleClimateActual(byte climate) {
return (consoleActualClimatePack >> (climate * 2)) & 0x03;
}
bool getConsoleToggleActual(byte toggle) {
return (consoleActualToggleBits & (1 << toggle)) != 0;
}
void setConsoleClimateTarget(byte climate, byte level) {
if (level > 3) {
level = 3;
}
bool changed = false;
if (consoleClimateTarget[climate] != level) {
consoleClimateTarget[climate] = level;
changed = true;
}
if (level > 0) {
byte opposite = CONSOLE_OPPOSITE_CLIMATE[climate];
if (consoleClimateTarget[opposite] != 0) {
consoleClimateTarget[opposite] = 0;
changed = true;
}
}
if (changed) {
saveConsoleSettings();
}
}
void setConsoleToggleTarget(byte toggle, bool enabled) {
if (consoleToggleTarget[toggle] == enabled) {
return;
}
consoleToggleTarget[toggle] = enabled;
saveConsoleSettings();
}
void updateConsoleCommandSender(unsigned long currentTime) {
if (!chaegeonCanReady || !consoleAlive || consoleBusy) {
return;
}
if (consoleCommandInFlight) {
if (currentTime - lastConsoleCommandTime >= CONSOLE_COMMAND_HOLD_MS) {
consoleCommandInFlight = false;
}
return;
}
if (consolePulseQueueHead == consolePulseQueueTail) {
return;
}
byte channel = consolePulseQueueChannel[consolePulseQueueHead];
unsigned int onMs = consolePulseQueueOnMs[consolePulseQueueHead];
unsigned int gapMs = consolePulseQueueGapMs[consolePulseQueueHead];
byte seq = nextConsoleCommandSeq();
byte payload[8] = {
1,
1,
channel,
(byte)(onMs & 0xFF),
(byte)(onMs >> 8),
(byte)(gapMs & 0xFF),
(byte)(gapMs >> 8),
seq
};
if (chaegeonCan.sendMsgBuf(CAN_ID_CONSOLE_COMMAND, 0, 8, payload) == CAN_OK) {
consolePulseQueueHead = nextConsoleQueueIndex(consolePulseQueueHead);
consoleCommandInFlight = true;
consoleCommandInFlightSeq = seq;
lastConsoleCommandTime = currentTime;
}
}
byte nextConsoleCommandSeq() {
consoleCommandSeq++;
if (consoleCommandSeq == 0) {
consoleCommandSeq = 1;
}
return consoleCommandSeq;
}
void enqueueConsolePulse(byte channel, unsigned int onMs, unsigned int gapMs) {
byte nextTail = nextConsoleQueueIndex(consolePulseQueueTail);
if (nextTail == consolePulseQueueHead || channel >= CONSOLE_CHANNEL_COUNT) {
return;
}
consolePulseQueueChannel[consolePulseQueueTail] = channel;
consolePulseQueueOnMs[consolePulseQueueTail] = onMs;
consolePulseQueueGapMs[consolePulseQueueTail] = gapMs;
consolePulseQueueTail = nextTail;
}
void clearConsolePulseQueue() {
consolePulseQueueHead = consolePulseQueueTail;
}
byte consolePulseQueueLength() {
if (consolePulseQueueTail >= consolePulseQueueHead) {
return consolePulseQueueTail - consolePulseQueueHead;
}
return CONSOLE_PULSE_QUEUE_SIZE - consolePulseQueueHead + consolePulseQueueTail;
}
byte nextConsoleQueueIndex(byte index) {
index++;
if (index >= CONSOLE_PULSE_QUEUE_SIZE) {
index = 0;
}
return index;
}
void loadConsoleSettings() {
if (EEPROM.read(EEPROM_CONSOLE_MAGIC_ADDR) != EEPROM_CONSOLE_MAGIC_VALUE) {
EEPROM.update(EEPROM_CONSOLE_MAGIC_ADDR, EEPROM_CONSOLE_MAGIC_VALUE);
for (byte i = 0; i < CONSOLE_CLIMATE_COUNT; i++) {
EEPROM.update(EEPROM_CONSOLE_CLIMATE_TARGET_ADDR + i, 0);
}
for (byte i = 0; i < CONSOLE_TOGGLE_COUNT; i++) {
EEPROM.update(EEPROM_CONSOLE_TOGGLE_TARGET_ADDR + i, 0);
}
}
for (byte i = 0; i < CONSOLE_CLIMATE_COUNT; i++) {
byte value = EEPROM.read(EEPROM_CONSOLE_CLIMATE_TARGET_ADDR + i);
consoleClimateTarget[i] = value <= 3 ? value : 0;
}
for (byte i = 0; i < CONSOLE_TOGGLE_COUNT; i++) {
consoleToggleTarget[i] = EEPROM.read(EEPROM_CONSOLE_TOGGLE_TARGET_ADDR + i) != 0;
}
if (consoleClimateTarget[CH_DRIVER_VENT] > 0 &&
consoleClimateTarget[CH_DRIVER_HEAT] > 0) {
consoleClimateTarget[CH_DRIVER_HEAT] = 0;
}
if (consoleClimateTarget[CH_PASSENGER_VENT] > 0 &&
consoleClimateTarget[CH_PASSENGER_HEAT] > 0) {
consoleClimateTarget[CH_PASSENGER_HEAT] = 0;
}
saveConsoleSettings();
}
void saveConsoleSettings() {
for (byte i = 0; i < CONSOLE_CLIMATE_COUNT; i++) {
EEPROM.update(EEPROM_CONSOLE_CLIMATE_TARGET_ADDR + i, consoleClimateTarget[i]);
}
for (byte i = 0; i < CONSOLE_TOGGLE_COUNT; i++) {
EEPROM.update(EEPROM_CONSOLE_TOGGLE_TARGET_ADDR + i, consoleToggleTarget[i] ? 1 : 0);
}
}
void setupChaegeonCan() {
if (chaegeonCan.begin(MCP_ANY, CAN_125KBPS, MCP_8MHZ) == CAN_OK) {
chaegeonCan.setMode(MCP_NORMAL);
chaegeonCanReady = true;
}
}
void updateChaegeonCanHeartbeat(unsigned long currentTime) {
if (!chaegeonCanReady) {
return;
}
if (currentTime - lastCanHeartbeatTime < CAN_HEARTBEAT_INTERVAL_MS) {
return;
}
lastCanHeartbeatTime = currentTime;
byte flags = 0;
if (ignitionState == HIGH) {
flags |= 0x01;
}
if (parkingState == HIGH) {
flags |= 0x02;
}
if (getRawDriveMode() == HIGH) {
flags |= 0x04;
}
if (confirmedDriveMode == HIGH) {
flags |= 0x08;
}
if (isStabilizing) {
flags |= 0x10;
}
if (currentSeatAction != 0 || requestedSeatAction != 0) {
flags |= 0x20;
}
if (autoCalibrationPending || autoCalibrationState != CALIBRATION_IDLE) {
flags |= 0x40;
}
uint16_t raw = (uint16_t)parkingRawValue;
uint16_t uptimeSeconds = (uint16_t)(currentTime / 1000UL);
byte payload[8] = {
1,
flags,
chaegeonCanSeq++,
useCount,
(byte)(raw & 0xFF),
(byte)(raw >> 8),
(byte)(uptimeSeconds & 0xFF),
(byte)(uptimeSeconds >> 8)
};
chaegeonCan.sendMsgBuf(CAN_ID_MAIN_HEARTBEAT, 0, 8, payload);
}
void setRelay(int relayPin, boolean state) {
digitalWrite(relayPin, state ? LOW : HIGH);
}