Compare commits
10
Commits
main
..
34346e4112
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34346e4112 | ||
|
|
d08f66375d | ||
|
|
1044c753ae | ||
|
|
4041888681 | ||
|
|
f45a71ce3f | ||
|
|
61c4be17f4 | ||
|
|
aa66a23868 | ||
|
|
673fa82f78 | ||
|
|
1293a92622 | ||
|
|
e4ab9f7605 |
No files matched your search
+42
-688
@@ -3,8 +3,6 @@
|
|||||||
// Automatic seat controller with EEPROM settings and optional serial tools.
|
// Automatic seat controller with EEPROM settings and optional serial tools.
|
||||||
//
|
//
|
||||||
#include <EEPROM.h>
|
#include <EEPROM.h>
|
||||||
#include <SPI.h>
|
|
||||||
#include <mcp_can.h>
|
|
||||||
|
|
||||||
#define seatRelayD8 8 // HIGH = OFF, LOW = ON (up relay)
|
#define seatRelayD8 8 // HIGH = OFF, LOW = ON (up relay)
|
||||||
#define seatRelayD9 9 // HIGH = OFF, LOW = ON (down relay)
|
#define seatRelayD9 9 // HIGH = OFF, LOW = ON (down relay)
|
||||||
@@ -47,59 +45,11 @@ const unsigned long AUTO_CALIBRATION_DOWN_MS = 6000UL;
|
|||||||
const unsigned long DEBUG_INTERVAL_MS = 1000UL;
|
const unsigned long DEBUG_INTERVAL_MS = 1000UL;
|
||||||
const long MANUAL_STEP_MS = 500;
|
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.
|
// EEPROM map.
|
||||||
const int EEPROM_MAGIC_ADDR = 0;
|
const int EEPROM_MAGIC_ADDR = 0;
|
||||||
const byte EEPROM_MAGIC_VALUE = 0x64;
|
const byte EEPROM_MAGIC_VALUE = 0x64;
|
||||||
const int EEPROM_USE_COUNT_ADDR = 1;
|
const int EEPROM_USE_COUNT_ADDR = 1;
|
||||||
const int EEPROM_SEAT_POSITION_ADDR = 2; // uint16_t, low byte first
|
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_IDLE = 0;
|
||||||
const byte CALIBRATION_UP = 1;
|
const byte CALIBRATION_UP = 1;
|
||||||
@@ -142,38 +92,6 @@ bool serialIntroPrinted = false;
|
|||||||
bool debugEnabled = false;
|
bool debugEnabled = false;
|
||||||
bool manualStepStatusPending = 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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.setTimeout(25);
|
Serial.setTimeout(25);
|
||||||
@@ -185,19 +103,12 @@ void setup() {
|
|||||||
|
|
||||||
applySeatAction(0);
|
applySeatAction(0);
|
||||||
seat(0);
|
seat(0);
|
||||||
setupChaegeonCan();
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
measurement();
|
measurement();
|
||||||
confirmedDriveMode = getRawDriveMode();
|
confirmedDriveMode = getRawDriveMode();
|
||||||
pendingDriveMode = confirmedDriveMode;
|
pendingDriveMode = confirmedDriveMode;
|
||||||
targetPosMs = getTargetForConfirmedMode();
|
targetPosMs = getTargetForConfirmedMode();
|
||||||
if (confirmedDriveMode == HIGH) {
|
|
||||||
// USB serial reconnect can reset the board while the car is already out of P.
|
|
||||||
// In that case, assume the seat is already at the saved driving position.
|
|
||||||
currentPosMs = storedSeatPositionMs;
|
|
||||||
targetPosMs = storedSeatPositionMs;
|
|
||||||
}
|
|
||||||
if (ignitionState == LOW) {
|
if (ignitionState == LOW) {
|
||||||
ignitionOffStartTime = millis();
|
ignitionOffStartTime = millis();
|
||||||
}
|
}
|
||||||
@@ -220,7 +131,6 @@ void loop() {
|
|||||||
registerIgnitionOffUse(currentTime);
|
registerIgnitionOffUse(currentTime);
|
||||||
}
|
}
|
||||||
prevIgnitionState = ignitionState;
|
prevIgnitionState = ignitionState;
|
||||||
updateChaegeonCan(currentTime);
|
|
||||||
|
|
||||||
if (autoCalibrationState != CALIBRATION_IDLE) {
|
if (autoCalibrationState != CALIBRATION_IDLE) {
|
||||||
updateAutoCalibration(currentTime);
|
updateAutoCalibration(currentTime);
|
||||||
@@ -520,7 +430,10 @@ void processSerialCommand(String command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool requiresParkingForCommand(char name) {
|
bool requiresParkingForCommand(char name) {
|
||||||
return name == 'a' ||
|
return name == 'p' ||
|
||||||
|
name == 'u' ||
|
||||||
|
name == 'd' ||
|
||||||
|
name == 'a' ||
|
||||||
name == 'c' ||
|
name == 'c' ||
|
||||||
name == 'r';
|
name == 'r';
|
||||||
}
|
}
|
||||||
@@ -599,7 +512,7 @@ void updateDebugOutput(unsigned long currentTime) {
|
|||||||
|
|
||||||
if (lastDebugPrintTime == 0 || currentTime - lastDebugPrintTime >= DEBUG_INTERVAL_MS) {
|
if (lastDebugPrintTime == 0 || currentTime - lastDebugPrintTime >= DEBUG_INTERVAL_MS) {
|
||||||
lastDebugPrintTime = currentTime;
|
lastDebugPrintTime = currentTime;
|
||||||
printDebugLine();
|
printStatus(F("DEBUG"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,8 +535,6 @@ void loadSettings() {
|
|||||||
storedSeatPositionMs = SEAT_HARD_LIMIT_UP_MS;
|
storedSeatPositionMs = SEAT_HARD_LIMIT_UP_MS;
|
||||||
writeUint16(EEPROM_SEAT_POSITION_ADDR, storedSeatPositionMs);
|
writeUint16(EEPROM_SEAT_POSITION_ADDR, storedSeatPositionMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadConsoleSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t readUint16(int addr) {
|
uint16_t readUint16(int addr) {
|
||||||
@@ -644,7 +555,7 @@ void printIntro() {
|
|||||||
Serial.println(F("+--------------------------------------------------+"));
|
Serial.println(F("+--------------------------------------------------+"));
|
||||||
Serial.println(F("| Units: milliseconds. 0 = bottom. 6000 = top cap. |"));
|
Serial.println(F("| Units: milliseconds. 0 = bottom. 6000 = top cap. |"));
|
||||||
Serial.println(F("| Normal auto move target is saved in EEPROM. |"));
|
Serial.println(F("| Normal auto move target is saved in EEPROM. |"));
|
||||||
Serial.println(F("| a/c/r commands require gear P for safety. |"));
|
Serial.println(F("| Action commands require gear P for safety. |"));
|
||||||
Serial.println(F("| Serial output is quiet unless a command is used. |"));
|
Serial.println(F("| Serial output is quiet unless a command is used. |"));
|
||||||
Serial.println(F("+--------------------------------------------------+"));
|
Serial.println(F("+--------------------------------------------------+"));
|
||||||
Serial.println(F("Commands:"));
|
Serial.println(F("Commands:"));
|
||||||
@@ -668,602 +579,45 @@ void printStatus(const __FlashStringHelper *tag) {
|
|||||||
Serial.print(F("["));
|
Serial.print(F("["));
|
||||||
Serial.print(tag);
|
Serial.print(tag);
|
||||||
Serial.println(F("]"));
|
Serial.println(F("]"));
|
||||||
printUptime();
|
Serial.print(F("raw="));
|
||||||
printNamedInt(F("parkingRawValue"), parkingRawValue);
|
Serial.print(parkingRawValue);
|
||||||
printNamedBool(F("ignition"), ignitionState == HIGH);
|
Serial.print(F(" ig="));
|
||||||
printNamedBool(F("parking"), parkingState == HIGH);
|
Serial.print(ignitionState == HIGH ? 1 : 0);
|
||||||
printNamedBool(F("rawDriveMode"), getRawDriveMode() == HIGH);
|
Serial.print(F(" p="));
|
||||||
printNamedBool(F("confirmedDriveMode"), confirmedDriveMode == HIGH);
|
Serial.print(parkingState == HIGH ? 1 : 0);
|
||||||
printNamedLong(F("currentPositionMs"), currentPosMs);
|
Serial.print(F(" rawDrive="));
|
||||||
printNamedLong(F("targetPositionMs"), targetPosMs);
|
Serial.print(getRawDriveMode() == HIGH ? 1 : 0);
|
||||||
printNamedInt(F("savedPositionMs"), storedSeatPositionMs);
|
Serial.print(F(" confirmedDrive="));
|
||||||
printNamedInt(F("hardLimitPositionMs"), SEAT_HARD_LIMIT_UP_MS);
|
Serial.println(confirmedDriveMode == HIGH ? 1 : 0);
|
||||||
printNamedInt(F("requestedRelayAction"), requestedSeatAction);
|
|
||||||
printNamedInt(F("actualRelayAction"), currentSeatAction);
|
Serial.print(F("currentMs="));
|
||||||
printNamedBool(F("manualMode"), manualMode);
|
Serial.print(currentPosMs);
|
||||||
printNamedBool(F("stabilizing"), isStabilizing);
|
Serial.print(F(" targetMs="));
|
||||||
|
Serial.print(targetPosMs);
|
||||||
|
Serial.print(F(" savedPositionMs="));
|
||||||
|
Serial.print(storedSeatPositionMs);
|
||||||
|
Serial.print(F(" hardLimitMs="));
|
||||||
|
Serial.println(SEAT_HARD_LIMIT_UP_MS);
|
||||||
|
|
||||||
|
Serial.print(F("requestedRelay="));
|
||||||
|
Serial.print(requestedSeatAction);
|
||||||
|
Serial.print(F(" actualRelay="));
|
||||||
|
Serial.print(currentSeatAction);
|
||||||
|
Serial.print(F(" manualMode="));
|
||||||
|
Serial.print(manualMode ? 1 : 0);
|
||||||
|
Serial.print(F(" stabilizing="));
|
||||||
|
Serial.println(isStabilizing ? 1 : 0);
|
||||||
|
|
||||||
Serial.print(F("useCount="));
|
Serial.print(F("useCount="));
|
||||||
Serial.print(useCount);
|
Serial.print(useCount);
|
||||||
Serial.print(F("/"));
|
Serial.print(F("/"));
|
||||||
Serial.println(USES_BEFORE_AUTO_CALIBRATION);
|
Serial.print(USES_BEFORE_AUTO_CALIBRATION);
|
||||||
printNamedBool(F("autoCalibrationPending"), autoCalibrationPending);
|
Serial.print(F(" autoPending="));
|
||||||
printNamedBool(F("forceCalibration"), forceCalibration);
|
Serial.print(autoCalibrationPending ? 1 : 0);
|
||||||
printNamedInt(F("autoCalibrationState"), autoCalibrationState);
|
Serial.print(F(" forceCal="));
|
||||||
printAutoCalibrationDelayStatus();
|
Serial.print(forceCalibration ? 1 : 0);
|
||||||
printConsoleStatusFields();
|
Serial.print(F(" autoCalState="));
|
||||||
}
|
Serial.println(autoCalibrationState);
|
||||||
|
|
||||||
void printDebugLine() {
|
|
||||||
Serial.println();
|
|
||||||
Serial.println(F("[DEBUG]"));
|
|
||||||
printUptime();
|
|
||||||
printNamedInt(F("parkingRawValue"), parkingRawValue);
|
|
||||||
printNamedBool(F("ignition"), ignitionState == HIGH);
|
|
||||||
printNamedBool(F("parking"), parkingState == HIGH);
|
|
||||||
printNamedBool(F("rawDriveMode"), getRawDriveMode() == HIGH);
|
|
||||||
printNamedBool(F("confirmedDriveMode"), confirmedDriveMode == HIGH);
|
|
||||||
printNamedLong(F("currentPositionMs"), currentPosMs);
|
|
||||||
printNamedLong(F("targetPositionMs"), targetPosMs);
|
|
||||||
printNamedInt(F("savedPositionMs"), storedSeatPositionMs);
|
|
||||||
printNamedInt(F("hardLimitPositionMs"), SEAT_HARD_LIMIT_UP_MS);
|
|
||||||
printNamedInt(F("requestedRelayAction"), requestedSeatAction);
|
|
||||||
printNamedInt(F("actualRelayAction"), currentSeatAction);
|
|
||||||
printNamedBool(F("manualMode"), manualMode);
|
|
||||||
printNamedBool(F("stabilizing"), isStabilizing);
|
|
||||||
Serial.print(F("useCount="));
|
|
||||||
Serial.print(useCount);
|
|
||||||
Serial.print(F("/"));
|
|
||||||
Serial.println(USES_BEFORE_AUTO_CALIBRATION);
|
|
||||||
printNamedBool(F("autoCalibrationPending"), autoCalibrationPending);
|
|
||||||
printNamedBool(F("forceCalibration"), forceCalibration);
|
|
||||||
printNamedInt(F("autoCalibrationState"), autoCalibrationState);
|
|
||||||
printAutoCalibrationDelayStatus();
|
|
||||||
printConsoleStatusFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
void printUptime() {
|
|
||||||
Serial.print(F("uptime="));
|
|
||||||
printDuration(millis());
|
|
||||||
Serial.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
void printUptimeInline() {
|
|
||||||
Serial.print(F("uptime="));
|
|
||||||
printDuration(millis());
|
|
||||||
}
|
|
||||||
|
|
||||||
void printBool(bool value) {
|
|
||||||
Serial.print(value ? F("true") : F("false"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void printNamedBool(const __FlashStringHelper *name, bool value) {
|
|
||||||
Serial.print(name);
|
|
||||||
Serial.print(F("="));
|
|
||||||
printBool(value);
|
|
||||||
Serial.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
void printNamedInt(const __FlashStringHelper *name, long value) {
|
|
||||||
Serial.print(name);
|
|
||||||
Serial.print(F("="));
|
|
||||||
Serial.println(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void printNamedLong(const __FlashStringHelper *name, long value) {
|
|
||||||
printNamedInt(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void printDuration(unsigned long totalMs) {
|
|
||||||
unsigned long hours = totalMs / 3600000UL;
|
|
||||||
totalMs %= 3600000UL;
|
|
||||||
unsigned long minutes = totalMs / 60000UL;
|
|
||||||
totalMs %= 60000UL;
|
|
||||||
unsigned long seconds = totalMs / 1000UL;
|
|
||||||
unsigned long milliseconds = totalMs % 1000UL;
|
|
||||||
|
|
||||||
Serial.print(hours);
|
|
||||||
Serial.print(F("h "));
|
|
||||||
Serial.print(minutes);
|
|
||||||
Serial.print(F("m "));
|
|
||||||
Serial.print(seconds);
|
|
||||||
Serial.print(F("s "));
|
|
||||||
Serial.print(milliseconds);
|
|
||||||
Serial.print(F("ms"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void printAutoCalibrationDelayStatusInline() {
|
|
||||||
if (useCount < USES_BEFORE_AUTO_CALIBRATION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.print(F("autoCalibrationOffTimer="));
|
|
||||||
if (ignitionState == HIGH) {
|
|
||||||
Serial.print(F("waiting_for_ig_off"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long elapsedMs = millis() - ignitionOffStartTime;
|
|
||||||
unsigned long remainingMs = 0;
|
|
||||||
if (elapsedMs < AUTO_CALIBRATION_DELAY_MS) {
|
|
||||||
remainingMs = AUTO_CALIBRATION_DELAY_MS - elapsedMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.print(F("elapsed "));
|
|
||||||
printDuration(elapsedMs);
|
|
||||||
Serial.print(F(" remaining "));
|
|
||||||
printDuration(remainingMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
void printAutoCalibrationDelayStatus() {
|
|
||||||
if (useCount < USES_BEFORE_AUTO_CALIBRATION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printAutoCalibrationDelayStatusInline();
|
|
||||||
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) {
|
void setRelay(int relayPin, boolean state) {
|
||||||
|
|||||||
@@ -1,514 +0,0 @@
|
|||||||
//
|
|
||||||
// Console_Node
|
|
||||||
// Chaegeon-CAN center-console I/O node for Arduino Mega.
|
|
||||||
//
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <mcp_can.h>
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// CAN
|
|
||||||
// ============================================================
|
|
||||||
const byte CAN_CS_PIN = 53;
|
|
||||||
const byte CAN_INT_PIN = 2;
|
|
||||||
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 MAIN_HEARTBEAT_TIMEOUT_MS = 2000UL;
|
|
||||||
const unsigned long STATUS_INTERVAL_MS = 100UL;
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// I/O
|
|
||||||
// ============================================================
|
|
||||||
const byte CHANNEL_COUNT = 7;
|
|
||||||
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;
|
|
||||||
|
|
||||||
// YYNMOS-8 style low-side MOSFET board. Change these two if the board is inverted.
|
|
||||||
const byte OUTPUT_ACTIVE_LEVEL = HIGH;
|
|
||||||
const byte OUTPUT_INACTIVE_LEVEL = LOW;
|
|
||||||
const byte OUTPUT_PINS[CHANNEL_COUNT] = {
|
|
||||||
22, 23, 24, 25, 26, 27, 28
|
|
||||||
};
|
|
||||||
|
|
||||||
// Button press detect inputs. PC817 output modules are commonly active-low.
|
|
||||||
const bool BUTTON_USE_INTERNAL_PULLUP = true;
|
|
||||||
const byte BUTTON_INPUT_PINS[CHANNEL_COUNT] = {
|
|
||||||
30, 31, 32, 33, 34, 35, 36
|
|
||||||
};
|
|
||||||
const bool BUTTON_ACTIVE_LOW[CHANNEL_COUNT] = {
|
|
||||||
true, true, true, true, true, true, true
|
|
||||||
};
|
|
||||||
const unsigned long BUTTON_DEBOUNCE_MS = 40UL;
|
|
||||||
const unsigned long OWN_PRESS_MASK_MS = 1200UL;
|
|
||||||
|
|
||||||
// STATE_LED analog inputs.
|
|
||||||
// 0..2 driver vent high/mid/low
|
|
||||||
// 3..5 driver heat high/mid/low
|
|
||||||
// 6..8 passenger vent high/mid/low
|
|
||||||
// 9..11 passenger heat high/mid/low
|
|
||||||
// 12 steering heat
|
|
||||||
// 13 front sensor
|
|
||||||
// 14 VESS
|
|
||||||
const byte LED_COUNT = 15;
|
|
||||||
const byte LED_PINS[LED_COUNT] = {
|
|
||||||
A0, A1, A2, A3, A4, A5, A6, A7,
|
|
||||||
A8, A9, A10, A11, A12, A13, A14
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5V ADC reference: 1.8V ~= 368, 3.3V ~= 675.
|
|
||||||
// Tune each channel after raw measurements. For inverted PC817 outputs, set false.
|
|
||||||
const int LED_ON_THRESHOLD[LED_COUNT] = {
|
|
||||||
300, 300, 300, 300, 300,
|
|
||||||
300, 300, 300, 300, 300,
|
|
||||||
300, 300, 300, 300, 300
|
|
||||||
};
|
|
||||||
const bool LED_ON_WHEN_ABOVE[LED_COUNT] = {
|
|
||||||
true, true, true, true, true,
|
|
||||||
true, true, true, true, true,
|
|
||||||
true, true, true, true, true
|
|
||||||
};
|
|
||||||
|
|
||||||
const byte CLIMATE_COUNT = 4;
|
|
||||||
const byte TOGGLE_COUNT = 3;
|
|
||||||
const byte CLIMATE_LED_BASE[CLIMATE_COUNT] = {
|
|
||||||
0, 3, 6, 9
|
|
||||||
};
|
|
||||||
const byte TOGGLE_LED_INDEX[TOGGLE_COUNT] = {
|
|
||||||
12, 13, 14
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Pulse execution
|
|
||||||
// ============================================================
|
|
||||||
const byte PULSE_QUEUE_SIZE = 16;
|
|
||||||
byte pulseQueueChannel[PULSE_QUEUE_SIZE] = {0};
|
|
||||||
unsigned int pulseQueueOnMs[PULSE_QUEUE_SIZE] = {0};
|
|
||||||
unsigned int pulseQueueGapMs[PULSE_QUEUE_SIZE] = {0};
|
|
||||||
byte pulseQueueHead = 0;
|
|
||||||
byte pulseQueueTail = 0;
|
|
||||||
|
|
||||||
bool pulseOnPhase = false;
|
|
||||||
bool pulseGapPhase = false;
|
|
||||||
byte activePulseChannel = CHANNEL_COUNT;
|
|
||||||
unsigned int activePulseOnMs = 0;
|
|
||||||
unsigned int activePulseGapMs = 0;
|
|
||||||
unsigned long pulsePhaseStartTime = 0;
|
|
||||||
unsigned long buttonMaskUntil[CHANNEL_COUNT] = {0};
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// State
|
|
||||||
// ============================================================
|
|
||||||
MCP_CAN consoleCan(CAN_CS_PIN);
|
|
||||||
|
|
||||||
bool canReady = false;
|
|
||||||
bool mainAlive = false;
|
|
||||||
bool mainIgnitionOn = false;
|
|
||||||
bool mainParkingOn = false;
|
|
||||||
byte mainHeartbeatSeq = 0;
|
|
||||||
byte lastAcceptedCommandSeq = 0;
|
|
||||||
bool commandSeqSeen = false;
|
|
||||||
bool pulseQueueOverflow = false;
|
|
||||||
unsigned long lastMainHeartbeatTime = 0;
|
|
||||||
unsigned long lastStatusTime = 0;
|
|
||||||
|
|
||||||
bool lastButtonRaw[CHANNEL_COUNT] = {false};
|
|
||||||
bool buttonStable[CHANNEL_COUNT] = {false};
|
|
||||||
unsigned long buttonChangeTime[CHANNEL_COUNT] = {0};
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
setupPins();
|
|
||||||
setupCan();
|
|
||||||
Serial.println(F("Console_Node I/O ready. Send h for help."));
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
unsigned long currentTime = millis();
|
|
||||||
|
|
||||||
updateCanReceive(currentTime);
|
|
||||||
updateMainAlive(currentTime);
|
|
||||||
updateButtonInputs(currentTime);
|
|
||||||
updatePulseOutput(currentTime);
|
|
||||||
updateStatusSend(currentTime);
|
|
||||||
handleSerial();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupPins() {
|
|
||||||
for (byte channel = 0; channel < CHANNEL_COUNT; channel++) {
|
|
||||||
pinMode(OUTPUT_PINS[channel], OUTPUT);
|
|
||||||
digitalWrite(OUTPUT_PINS[channel], OUTPUT_INACTIVE_LEVEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (byte channel = 0; channel < CHANNEL_COUNT; channel++) {
|
|
||||||
pinMode(BUTTON_INPUT_PINS[channel], BUTTON_USE_INTERNAL_PULLUP ? INPUT_PULLUP : INPUT);
|
|
||||||
bool pressed = readButtonPressedRaw(channel);
|
|
||||||
lastButtonRaw[channel] = pressed;
|
|
||||||
buttonStable[channel] = pressed;
|
|
||||||
buttonChangeTime[channel] = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (byte led = 0; led < LED_COUNT; led++) {
|
|
||||||
pinMode(LED_PINS[led], INPUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupCan() {
|
|
||||||
pinMode(CAN_INT_PIN, INPUT);
|
|
||||||
if (consoleCan.begin(MCP_ANY, CAN_125KBPS, MCP_8MHZ) == CAN_OK) {
|
|
||||||
consoleCan.setMode(MCP_NORMAL);
|
|
||||||
canReady = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateCanReceive(unsigned long currentTime) {
|
|
||||||
if (!canReady) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (consoleCan.checkReceive() == CAN_MSGAVAIL) {
|
|
||||||
unsigned long rxId = 0;
|
|
||||||
byte len = 0;
|
|
||||||
byte payload[8] = {0};
|
|
||||||
if (consoleCan.readMsgBuf(&rxId, &len, payload) != CAN_OK) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rxId == CAN_ID_MAIN_HEARTBEAT && len >= 3 && payload[0] == 1) {
|
|
||||||
byte flags = payload[1];
|
|
||||||
mainIgnitionOn = (flags & 0x01) != 0;
|
|
||||||
mainParkingOn = (flags & 0x02) != 0;
|
|
||||||
mainHeartbeatSeq = payload[2];
|
|
||||||
mainAlive = true;
|
|
||||||
lastMainHeartbeatTime = currentTime;
|
|
||||||
} else if (rxId == CAN_ID_CONSOLE_COMMAND && len == 8 && payload[0] == 1) {
|
|
||||||
processCommand(payload, currentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void processCommand(byte payload[8], unsigned long currentTime) {
|
|
||||||
byte command = payload[1];
|
|
||||||
byte seq = payload[7];
|
|
||||||
|
|
||||||
if (commandSeqSeen && seq == lastAcceptedCommandSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command == 1) {
|
|
||||||
byte channel = payload[2];
|
|
||||||
unsigned int onMs = (unsigned int)payload[3] | ((unsigned int)payload[4] << 8);
|
|
||||||
unsigned int gapMs = (unsigned int)payload[5] | ((unsigned int)payload[6] << 8);
|
|
||||||
if (enqueuePulse(channel, onMs, gapMs)) {
|
|
||||||
lastAcceptedCommandSeq = seq;
|
|
||||||
commandSeqSeen = true;
|
|
||||||
}
|
|
||||||
} else if (command == 2) {
|
|
||||||
clearPulseQueue();
|
|
||||||
allOutputsOff();
|
|
||||||
lastAcceptedCommandSeq = seq;
|
|
||||||
commandSeqSeen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
(void)currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateMainAlive(unsigned long currentTime) {
|
|
||||||
bool aliveNow = lastMainHeartbeatTime != 0 &&
|
|
||||||
currentTime - lastMainHeartbeatTime <= MAIN_HEARTBEAT_TIMEOUT_MS;
|
|
||||||
if (!aliveNow && mainAlive) {
|
|
||||||
clearPulseQueue();
|
|
||||||
allOutputsOff();
|
|
||||||
}
|
|
||||||
|
|
||||||
mainAlive = aliveNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateStatusSend(unsigned long currentTime) {
|
|
||||||
if (!canReady || currentTime - lastStatusTime < STATUS_INTERVAL_MS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastStatusTime = currentTime;
|
|
||||||
|
|
||||||
byte flags = 0;
|
|
||||||
if (mainAlive) {
|
|
||||||
flags |= 0x01;
|
|
||||||
}
|
|
||||||
if (isPulseBusy()) {
|
|
||||||
flags |= 0x02;
|
|
||||||
}
|
|
||||||
if (pulseQueueOverflow) {
|
|
||||||
flags |= 0x04;
|
|
||||||
}
|
|
||||||
if (mainIgnitionOn) {
|
|
||||||
flags |= 0x08;
|
|
||||||
}
|
|
||||||
if (mainParkingOn) {
|
|
||||||
flags |= 0x10;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte payload[8] = {
|
|
||||||
1,
|
|
||||||
flags,
|
|
||||||
packClimateLevels(),
|
|
||||||
packToggleStates(),
|
|
||||||
packButtonStates(),
|
|
||||||
lastAcceptedCommandSeq,
|
|
||||||
queueLength(),
|
|
||||||
mainHeartbeatSeq
|
|
||||||
};
|
|
||||||
|
|
||||||
consoleCan.sendMsgBuf(CAN_ID_CONSOLE_STATUS, 0, 8, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte packClimateLevels() {
|
|
||||||
byte packed = 0;
|
|
||||||
for (byte climate = 0; climate < CLIMATE_COUNT; climate++) {
|
|
||||||
packed |= (readClimateLevel(climate) & 0x03) << (climate * 2);
|
|
||||||
}
|
|
||||||
return packed;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte packToggleStates() {
|
|
||||||
byte packed = 0;
|
|
||||||
for (byte toggle = 0; toggle < TOGGLE_COUNT; toggle++) {
|
|
||||||
if (readLedOn(TOGGLE_LED_INDEX[toggle])) {
|
|
||||||
packed |= (1 << toggle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return packed;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte packButtonStates() {
|
|
||||||
byte packed = 0;
|
|
||||||
unsigned long currentTime = millis();
|
|
||||||
for (byte channel = 0; channel < CHANNEL_COUNT; channel++) {
|
|
||||||
if (isButtonMasked(channel, currentTime)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (buttonStable[channel]) {
|
|
||||||
packed |= (1 << channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return packed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateButtonInputs(unsigned long currentTime) {
|
|
||||||
for (byte channel = 0; channel < CHANNEL_COUNT; channel++) {
|
|
||||||
bool rawPressed = readButtonPressedRaw(channel);
|
|
||||||
if (rawPressed != lastButtonRaw[channel]) {
|
|
||||||
lastButtonRaw[channel] = rawPressed;
|
|
||||||
buttonChangeTime[channel] = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTime - buttonChangeTime[channel] >= BUTTON_DEBOUNCE_MS) {
|
|
||||||
buttonStable[channel] = rawPressed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool readButtonPressedRaw(byte channel) {
|
|
||||||
bool high = digitalRead(BUTTON_INPUT_PINS[channel]) == HIGH;
|
|
||||||
if (BUTTON_ACTIVE_LOW[channel]) {
|
|
||||||
return !high;
|
|
||||||
}
|
|
||||||
return high;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isButtonMasked(byte channel, unsigned long currentTime) {
|
|
||||||
return (long)(buttonMaskUntil[channel] - currentTime) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte readClimateLevel(byte climate) {
|
|
||||||
byte base = CLIMATE_LED_BASE[climate];
|
|
||||||
if (readLedOn(base)) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
if (readLedOn(base + 1)) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
if (readLedOn(base + 2)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool readLedOn(byte ledIndex) {
|
|
||||||
int raw = analogRead(LED_PINS[ledIndex]);
|
|
||||||
bool above = raw >= LED_ON_THRESHOLD[ledIndex];
|
|
||||||
return LED_ON_WHEN_ABOVE[ledIndex] ? above : !above;
|
|
||||||
}
|
|
||||||
|
|
||||||
int readLedRaw(byte ledIndex) {
|
|
||||||
return analogRead(LED_PINS[ledIndex]);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool enqueuePulse(byte channel, unsigned int onMs, unsigned int gapMs) {
|
|
||||||
byte nextTail = nextQueueIndex(pulseQueueTail);
|
|
||||||
if (channel >= CHANNEL_COUNT || nextTail == pulseQueueHead) {
|
|
||||||
pulseQueueOverflow = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pulseQueueChannel[pulseQueueTail] = channel;
|
|
||||||
pulseQueueOnMs[pulseQueueTail] = onMs;
|
|
||||||
pulseQueueGapMs[pulseQueueTail] = gapMs;
|
|
||||||
pulseQueueTail = nextTail;
|
|
||||||
|
|
||||||
unsigned long maskUntil = millis() + onMs + gapMs + OWN_PRESS_MASK_MS;
|
|
||||||
if ((long)(maskUntil - buttonMaskUntil[channel]) > 0) {
|
|
||||||
buttonMaskUntil[channel] = maskUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updatePulseOutput(unsigned long currentTime) {
|
|
||||||
if (!pulseOnPhase && !pulseGapPhase) {
|
|
||||||
startNextPulse(currentTime);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pulseOnPhase && currentTime - pulsePhaseStartTime >= activePulseOnMs) {
|
|
||||||
setOutput(activePulseChannel, false);
|
|
||||||
pulseOnPhase = false;
|
|
||||||
pulseGapPhase = true;
|
|
||||||
pulsePhaseStartTime = currentTime;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pulseGapPhase && currentTime - pulsePhaseStartTime >= activePulseGapMs) {
|
|
||||||
pulseGapPhase = false;
|
|
||||||
activePulseChannel = CHANNEL_COUNT;
|
|
||||||
startNextPulse(currentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startNextPulse(unsigned long currentTime) {
|
|
||||||
if (pulseQueueHead == pulseQueueTail) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activePulseChannel = pulseQueueChannel[pulseQueueHead];
|
|
||||||
activePulseOnMs = pulseQueueOnMs[pulseQueueHead];
|
|
||||||
activePulseGapMs = pulseQueueGapMs[pulseQueueHead];
|
|
||||||
pulseQueueHead = nextQueueIndex(pulseQueueHead);
|
|
||||||
|
|
||||||
setOutput(activePulseChannel, true);
|
|
||||||
pulseOnPhase = true;
|
|
||||||
pulsePhaseStartTime = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOutput(byte channel, bool active) {
|
|
||||||
if (channel >= CHANNEL_COUNT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
digitalWrite(OUTPUT_PINS[channel], active ? OUTPUT_ACTIVE_LEVEL : OUTPUT_INACTIVE_LEVEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void allOutputsOff() {
|
|
||||||
for (byte channel = 0; channel < CHANNEL_COUNT; channel++) {
|
|
||||||
setOutput(channel, false);
|
|
||||||
}
|
|
||||||
pulseOnPhase = false;
|
|
||||||
pulseGapPhase = false;
|
|
||||||
activePulseChannel = CHANNEL_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearPulseQueue() {
|
|
||||||
pulseQueueHead = pulseQueueTail;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPulseBusy() {
|
|
||||||
return pulseOnPhase || pulseGapPhase || pulseQueueHead != pulseQueueTail;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte queueLength() {
|
|
||||||
if (pulseQueueTail >= pulseQueueHead) {
|
|
||||||
return pulseQueueTail - pulseQueueHead;
|
|
||||||
}
|
|
||||||
return PULSE_QUEUE_SIZE - pulseQueueHead + pulseQueueTail;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte nextQueueIndex(byte index) {
|
|
||||||
index++;
|
|
||||||
if (index >= PULSE_QUEUE_SIZE) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSerial() {
|
|
||||||
while (Serial.available()) {
|
|
||||||
char command = Serial.read();
|
|
||||||
if (command == '\r' || command == '\n') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command == 'h') {
|
|
||||||
printHelp();
|
|
||||||
} else if (command == 's') {
|
|
||||||
printStatus();
|
|
||||||
} else if (command == 'x') {
|
|
||||||
clearPulseQueue();
|
|
||||||
allOutputsOff();
|
|
||||||
Serial.println(F("ACK: outputs off"));
|
|
||||||
} else {
|
|
||||||
Serial.println(F("ERR: h/s/x only"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void printHelp() {
|
|
||||||
Serial.println();
|
|
||||||
Serial.println(F("+------------------------------------------------------+"));
|
|
||||||
Serial.println(F("| Console_Node I/O |"));
|
|
||||||
Serial.println(F("+------------------------------------------------------+"));
|
|
||||||
Serial.println(F("| h = help |"));
|
|
||||||
Serial.println(F("| s = status and raw LED values |"));
|
|
||||||
Serial.println(F("| x = stop outputs and clear pulse queue |"));
|
|
||||||
Serial.println(F("| Memory and automation live in Auto_Seat Nano. |"));
|
|
||||||
Serial.println(F("+------------------------------------------------------+"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void printStatus() {
|
|
||||||
Serial.println();
|
|
||||||
Serial.println(F("[CONSOLE_IO_STATUS]"));
|
|
||||||
printNamedBool(F("canReady"), canReady);
|
|
||||||
printNamedBool(F("mainAlive"), mainAlive);
|
|
||||||
printNamedBool(F("mainIgnitionOn"), mainIgnitionOn);
|
|
||||||
printNamedBool(F("mainParkingOn"), mainParkingOn);
|
|
||||||
printNamedBool(F("pulseBusy"), isPulseBusy());
|
|
||||||
printNamedBool(F("pulseQueueOverflow"), pulseQueueOverflow);
|
|
||||||
printNamedInt(F("queueLength"), queueLength());
|
|
||||||
printNamedInt(F("lastAcceptedCommandSeq"), lastAcceptedCommandSeq);
|
|
||||||
printNamedInt(F("driverVentLevel"), readClimateLevel(CH_DRIVER_VENT));
|
|
||||||
printNamedInt(F("driverHeatLevel"), readClimateLevel(CH_DRIVER_HEAT));
|
|
||||||
printNamedInt(F("passengerVentLevel"), readClimateLevel(CH_PASSENGER_VENT));
|
|
||||||
printNamedInt(F("passengerHeatLevel"), readClimateLevel(CH_PASSENGER_HEAT));
|
|
||||||
printNamedBool(F("steeringHeatOn"), readLedOn(12));
|
|
||||||
printNamedBool(F("frontSensorOn"), readLedOn(13));
|
|
||||||
printNamedBool(F("vessOn"), readLedOn(14));
|
|
||||||
for (byte led = 0; led < LED_COUNT; led++) {
|
|
||||||
Serial.print(F("ledRaw"));
|
|
||||||
Serial.print(led);
|
|
||||||
Serial.print(F("="));
|
|
||||||
Serial.println(readLedRaw(led));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void printNamedBool(const __FlashStringHelper *name, bool value) {
|
|
||||||
Serial.print(name);
|
|
||||||
Serial.print(F("="));
|
|
||||||
Serial.println(value ? F("true") : F("false"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void printNamedInt(const __FlashStringHelper *name, long value) {
|
|
||||||
Serial.print(name);
|
|
||||||
Serial.print(F("="));
|
|
||||||
Serial.println(value);
|
|
||||||
}
|
|
||||||
@@ -1,479 +1,55 @@
|
|||||||
# Auto Seat
|
# Auto_Seat
|
||||||
|
|
||||||
차량 운전석 자동 시트 위치 제어와 센터콘솔 메모리 기능을 관리하는 Arduino 프로젝트입니다.
|
## 개요
|
||||||
|
|
||||||
운전석 아래의 Arduino Nano가 메인 컨트롤러입니다. 기존 시트 상승/하강 제어, EEPROM 저장값, 시리얼 콘솔, IG1/P단 판단을 담당합니다. 센터콘솔 쪽 Arduino Mega는 단순 I/O 노드로 동작하며, CAN 명령을 받아 MOSFET 출력으로 버튼을 누르고 버튼/STATE_LED 상태를 Nano에 보고합니다.
|
차량 시트 자동 제어 프로젝트입니다. EEPROM 설정, 점화/주차 조건, 릴레이 출력, 자동 보정, 시리얼 명령 처리를 포함합니다.
|
||||||
|
|
||||||
## 목적
|
분류: AutoSeat_UL, Auto_Seat_LL과 같은 자동 시트 제어 계열의 기준 프로젝트입니다.
|
||||||
|
|
||||||
- P단이 아닌 주행 상태가 확인되면 시트를 EEPROM에 저장된 주행 위치까지 상승시킵니다.
|
## 코드 구성 요약
|
||||||
- P단 또는 비주행 상태가 확인되면 시트를 하단 `0ms` 위치까지 하강시킵니다.
|
|
||||||
- 실제 위치 센서 없이 릴레이 구동 시간으로 현재 위치를 추정합니다.
|
|
||||||
- 운전석/조수석 열선과 통풍, 핸들 열선, 전방센서, VESS 보조 기능을 센터콘솔 노드로 제어합니다.
|
|
||||||
- 모든 메모리와 판단은 Nano가 담당하고, Mega는 신호 감지와 출력 수행만 담당합니다.
|
|
||||||
|
|
||||||
## 저장소 구성
|
- 분석한 파일 수: 3
|
||||||
|
- 사용 라이브러리/include: EEPROM.h
|
||||||
|
- 코드에서 확인되는 주요 모듈: EEPROM, Serial
|
||||||
|
- 코드에서 확인되는 핀/입출력 단서: ignitionPin:INPUT, parkingPin:INPUT, seatRelayD8:OUTPUT, seatRelayD9:OUTPUT
|
||||||
|
|
||||||
```text
|
## 파일별 설명
|
||||||
Auto_Seat.ino
|
|
||||||
Console_Node/Console_Node.ino
|
|
||||||
upload.ps1
|
|
||||||
report.html
|
|
||||||
README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
- `Auto_Seat.ino`: 운전석 아래 Nano용 메인 스케치입니다.
|
### `Auto_Seat.ino`
|
||||||
- `Console_Node/Console_Node.ino`: 센터콘솔 Mega용 I/O 노드 스케치입니다.
|
|
||||||
- `upload.ps1`: Nano 업로드용 PowerShell 스크립트입니다.
|
|
||||||
- `report.html`: 현재 동작 시뮬레이션과 운영 보고서입니다.
|
|
||||||
|
|
||||||
## 메인 Nano 하드웨어
|
- 역할: Arduino의 `setup()`/`loop()`를 포함한 실행 스케치입니다.
|
||||||
|
- include/의존성: EEPROM.h
|
||||||
|
- 주요 함수: applySeatAction, clampPosition, getTargetForConfirmedMode, handleRelativeStep, handleSerial, isSeatCommandBusy, loadSettings, loop, measurement, normalizeMotionState, printIntro, printStatus, processSerialCommand, readUint16, registerIgnitionOffUse, requiresParkingForCommand, saveCurrentPositionCommand, seat, setRelay, setup, startAutoCalibration, startDownEdgeMargin, updateAutoCalibration, updateConfirmedDriveMode, updateDebugOutput, updateManualStepStatus, updateMovement, updateRelayOutput, writeUint16
|
||||||
|
- 주요 전역값/설정값: AUTO_CALIBRATION_DELAY_MS=300000UL, AUTO_CALIBRATION_DOWN_MS=6000UL, AUTO_CALIBRATION_UP_MS=7000UL, DEBUG_INTERVAL_MS=1000UL, DOWN_EDGE_MARGIN_MS=300UL, DOWN_SPEED_DEN=10, DOWN_SPEED_NUM=11, DRIVE_CONFIRM_MS=80UL, EEPROM_MAGIC_ADDR=0, EEPROM_MAGIC_VALUE=0x64, IGNITION_STABILIZE_MS=1000UL, MANUAL_STEP_MS=500, PARK_CONFIRM_MS=250UL, PARKING_OFF_THRESHOLD=940, PARKING_ON_THRESHOLD=950, RELAY_DEADTIME_MS=120UL, SEAT_HARD_LIMIT_UP_MS=6000, USES_BEFORE_AUTO_CALIBRATION=10
|
||||||
|
- 핀/입출력 설정: ignitionPin:INPUT, parkingPin:INPUT, seatRelayD8:OUTPUT, seatRelayD9:OUTPUT
|
||||||
|
- 입출력/통신 호출: analogRead, digitalRead, EEPROM.read, EEPROM.update, Serial.available, Serial.begin, Serial.print, Serial.println
|
||||||
|
|
||||||
- MCU: Arduino Nano ATmega328P
|
### `report.html`
|
||||||
- 상승 릴레이: `D8`, active-low, `LOW = ON`
|
|
||||||
- 하강 릴레이: `D9`, active-low, `LOW = ON`
|
|
||||||
- P단 입력: `A0`, analog raw `0..1023`
|
|
||||||
- IG1 입력: `D7`, `HIGH = IG1 ON`, `LOW = IG1 OFF`
|
|
||||||
- CAN 모듈: MCP2515 + TJA1050, 8MHz crystal
|
|
||||||
- CAN CS: `D10`
|
|
||||||
- CAN SPI: `D11 MOSI`, `D12 MISO`, `D13 SCK`
|
|
||||||
|
|
||||||
Nano의 기존 시트 제어 핀과 MCP2515 SPI 핀은 충돌하지 않습니다.
|
- 역할: 프로젝트 보고서 또는 설명을 담은 HTML 문서입니다.
|
||||||
|
- include/의존성: -
|
||||||
|
- 주요 함수: -
|
||||||
|
- 주요 전역값/설정값: -
|
||||||
|
- 핀/입출력 설정: -
|
||||||
|
- 입출력/통신 호출: EEPROM.update
|
||||||
|
|
||||||
## 센터콘솔 Mega 하드웨어
|
### `upload.ps1`
|
||||||
|
|
||||||
- MCU: Arduino Mega
|
- 역할: 업로드/자동화에 사용되는 PowerShell 스크립트입니다.
|
||||||
- CAN 모듈: MCP2515 + TJA1050, 8MHz crystal
|
- include/의존성: -
|
||||||
- CAN CS: `D53`
|
- 주요 함수: -
|
||||||
- CAN INT: `D2`
|
- 주요 전역값/설정값: -
|
||||||
- CAN SPI: `D50 MISO`, `D51 MOSI`, `D52 SCK`
|
- 핀/입출력 설정: -
|
||||||
- MOSFET 출력 7채널: `D22..D28`
|
- 입출력/통신 호출: -
|
||||||
- 버튼 눌림 감지 7채널: `D30..D36`
|
|
||||||
- STATE_LED analog 입력 15채널: `A0..A14`
|
|
||||||
|
|
||||||
MOSFET 출력은 YYNMOS-8 같은 low-side 보드를 기준으로 합니다. 현재 설정은 `HIGH = ON`, `LOW = OFF`입니다. 보드가 반전형이면 `OUTPUT_ACTIVE_LEVEL`, `OUTPUT_INACTIVE_LEVEL`만 바꾸면 됩니다.
|
## 동작 흐름
|
||||||
|
|
||||||
PC817 출력 모듈은 보통 active-low입니다. 버튼 감지 쪽은 기본값이 `INPUT_PULLUP`, `BUTTON_ACTIVE_LOW = true`입니다. 실제 모듈 출력이 반대면 배열 값만 바꿉니다.
|
1. `setup()`에서 시리얼, 센서, 통신 모듈, LCD/릴레이/핀 모드 등 초기 설정을 수행합니다.
|
||||||
|
2. `loop()`에서 센서 측정, 입력 확인, 상태 계산, 출력 제어, 통신 전송 또는 화면 갱신을 반복합니다.
|
||||||
|
3. 보조 함수들은 측정값 변환, 값 변화 감지, 릴레이/모터 제어, 시간 표시, 네트워크 응답 같은 세부 동작을 나눠 담당합니다.
|
||||||
|
|
||||||
## 센터콘솔 채널
|
## 빌드 및 사용 메모
|
||||||
|
|
||||||
```text
|
- Arduino IDE 또는 PlatformIO에서 폴더명과 같은 대표 스케치를 열어 빌드합니다.
|
||||||
0 = 운전석 통풍
|
- 코드에 포함된 네트워크 주소, Wi-Fi 정보, DB 정보, 장치 핀 번호는 실제 하드웨어 구성에 맞춰 확인해야 합니다.
|
||||||
1 = 운전석 열선
|
- 공개 저장소에 올릴 때는 비밀번호, 토큰, 차량별 민감 정보가 포함되지 않았는지 확인합니다.
|
||||||
2 = 조수석 통풍
|
|
||||||
3 = 조수석 열선
|
|
||||||
4 = 핸들 열선
|
|
||||||
5 = 전방센서
|
|
||||||
6 = VESS
|
|
||||||
```
|
|
||||||
|
|
||||||
열선/통풍 4채널은 단계형입니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
0 -> 3 -> 2 -> 1 -> 0
|
|
||||||
```
|
|
||||||
|
|
||||||
일반 클릭은 `300ms`입니다. 열선/통풍 OFF 기준화는 해당 버튼을 `2100ms` 길게 눌러 처리합니다.
|
|
||||||
|
|
||||||
## STATE_LED 입력
|
|
||||||
|
|
||||||
Mega는 STATE_LED 15개를 analogRead로 읽고, 단계와 ON/OFF 상태만 Nano에 보고합니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
A0..A2 운전석 통풍 High/Mid/Low
|
|
||||||
A3..A5 운전석 열선 High/Mid/Low
|
|
||||||
A6..A8 조수석 통풍 High/Mid/Low
|
|
||||||
A9..A11 조수석 열선 High/Mid/Low
|
|
||||||
A12 핸들 열선
|
|
||||||
A13 전방센서
|
|
||||||
A14 VESS
|
|
||||||
```
|
|
||||||
|
|
||||||
기본 임계값은 전 채널 `300`입니다. 5V ADC 기준으로 1.8V는 약 `368`, 3.3V는 약 `675`입니다. 실제 배선 후 Mega 시리얼의 `s` 명령으로 `ledRaw0..ledRaw14` 값을 확인하고 `LED_ON_THRESHOLD`를 조정합니다.
|
|
||||||
|
|
||||||
PC817 출력처럼 LED ON일 때 analog 값이 낮아지는 채널은 `LED_ON_WHEN_ABOVE`를 `false`로 바꿉니다.
|
|
||||||
|
|
||||||
## 시트 동작 로직
|
|
||||||
|
|
||||||
- `IG1 HIGH`이고 P단 신호가 꺼져 있으면 주행 후보 상태로 봅니다.
|
|
||||||
- 주행 후보가 `80ms` 유지되면 주행 모드로 확정합니다.
|
|
||||||
- P단 또는 비주행 후보가 `250ms` 유지되면 비주행 모드로 확정합니다.
|
|
||||||
- 시동 직후 `1000ms` 동안은 IG1/P 신호 흔들림을 무시하고 릴레이를 정지합니다.
|
|
||||||
- 주행 모드에서는 EEPROM에 저장된 시트 위치로 상승합니다.
|
|
||||||
- 비주행 모드에서는 `0ms` 위치로 하강합니다.
|
|
||||||
- 하강 속도는 상승보다 `1.1배` 빠른 것으로 계산합니다.
|
|
||||||
- `0ms` 도달 시 하단 밀착을 위해 하강 릴레이를 추가 `300ms` 유지합니다.
|
|
||||||
- 릴레이 방향 전환 사이에는 `120ms` dead time을 둡니다.
|
|
||||||
|
|
||||||
시트 위치는 실제 센서값이 아니라 시간 기반 추정값입니다. 전원 차단 후 다시 켜지면 기본적으로 하단 `0ms`에서 시작한다고 봅니다.
|
|
||||||
|
|
||||||
## USB Serial Reset 보호
|
|
||||||
|
|
||||||
Arduino Nano는 USB 시리얼 연결 시 DTR 때문에 리셋될 수 있습니다.
|
|
||||||
|
|
||||||
보드가 부팅된 순간 `IG1 HIGH`이고 P단이 아니면, 이미 주행 중 USB 재연결이 발생한 것으로 보고 현재 위치와 목표 위치를 EEPROM 저장 주행 위치로 맞춥니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
currentPositionMs = savedPositionMs
|
|
||||||
targetPositionMs = savedPositionMs
|
|
||||||
relay = OFF
|
|
||||||
```
|
|
||||||
|
|
||||||
이렇게 해서 주행 중 시리얼 연결만으로 시트가 불필요하게 상승하는 상황을 막습니다. 이후 사용자가 P단으로 이동하면 저장 위치 기준으로 `0ms`까지 하강합니다.
|
|
||||||
|
|
||||||
## 시트 EEPROM
|
|
||||||
|
|
||||||
```text
|
|
||||||
0 = magic
|
|
||||||
1 = 10회 보정 useCount
|
|
||||||
2..3 = 시트 주행 위치 uint16_t
|
|
||||||
```
|
|
||||||
|
|
||||||
시트 위치 허용 범위는 `0..6000ms`입니다. EEPROM magic이 없거나 저장값이 유효하지 않으면 기본 주행 위치를 `6000ms`로 설정합니다.
|
|
||||||
|
|
||||||
## 센터콘솔 EEPROM
|
|
||||||
|
|
||||||
센터콘솔 목표값은 Mega가 아니라 Nano EEPROM에 저장합니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
16 = console magic
|
|
||||||
17 = 운전석 통풍 목표 단계 0..3
|
|
||||||
18 = 운전석 열선 목표 단계 0..3
|
|
||||||
19 = 조수석 통풍 목표 단계 0..3
|
|
||||||
20 = 조수석 열선 목표 단계 0..3
|
|
||||||
21 = 핸들 열선 목표 0/1
|
|
||||||
22 = 전방센서 목표 0/1
|
|
||||||
23 = VESS 목표 0/1
|
|
||||||
```
|
|
||||||
|
|
||||||
운전석 통풍이 켜지면 운전석 열선 메모리는 `0`으로 지웁니다. 운전석 열선이 켜지면 운전석 통풍 메모리는 `0`으로 지웁니다. 조수석도 같은 규칙을 적용합니다.
|
|
||||||
|
|
||||||
## 센터콘솔 자동 적용
|
|
||||||
|
|
||||||
- Mega가 CAN 상태를 보내고 Nano가 alive로 인식하면 센터콘솔 기능을 사용할 수 있습니다.
|
|
||||||
- 부팅 후 `3000ms`가 지나고 Mega가 준비되어 있으면 저장된 열선/통풍 목표를 적용합니다.
|
|
||||||
- 핸들 열선, 전방센서, VESS는 IG1이 켜진 뒤에만 목표값을 적용합니다.
|
|
||||||
- 열선/통풍 목표가 `1..3`이면 약 `19분 30초`마다 저장 목표를 다시 적용합니다.
|
|
||||||
- 핸들 열선 목표가 ON이고 IG1이 ON이면 약 `19분 30초`마다 OFF 클릭 후 다시 ON 클릭을 수행합니다.
|
|
||||||
- Mega가 바쁘거나 CAN 응답이 없으면 Nano는 새 명령을 보내지 않습니다.
|
|
||||||
|
|
||||||
사용자가 센터콘솔 버튼을 직접 누르면 Mega가 눌림/릴리즈를 Nano에 보고합니다. Nano는 릴리즈 후 `700ms` 뒤 실제 LED 상태를 읽어 EEPROM 목표값을 갱신합니다.
|
|
||||||
|
|
||||||
## CAN 프로토콜
|
|
||||||
|
|
||||||
속도는 `125kbps`, MCP2515 crystal은 `8MHz`로 설정합니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
0x100 Nano -> Mega heartbeat
|
|
||||||
0x101 Nano -> Mega command
|
|
||||||
0x200 Mega -> Nano status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 0x100 Heartbeat
|
|
||||||
|
|
||||||
```text
|
|
||||||
byte0 = protocol version, 1
|
|
||||||
byte1 = flags
|
|
||||||
byte2 = sequence
|
|
||||||
byte3 = useCount
|
|
||||||
byte4 = parking raw low
|
|
||||||
byte5 = parking raw high
|
|
||||||
byte6 = uptime seconds low
|
|
||||||
byte7 = uptime seconds high
|
|
||||||
```
|
|
||||||
|
|
||||||
flags:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bit0 = IG1 ON
|
|
||||||
bit1 = P ON
|
|
||||||
bit2 = raw drive mode
|
|
||||||
bit3 = confirmed drive mode
|
|
||||||
bit4 = ignition stabilizing
|
|
||||||
bit5 = seat moving
|
|
||||||
bit6 = auto calibration pending/running
|
|
||||||
```
|
|
||||||
|
|
||||||
### 0x101 Command
|
|
||||||
|
|
||||||
현재 명령은 pulse 실행입니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
byte0 = protocol version, 1
|
|
||||||
byte1 = command, 1 pulse
|
|
||||||
byte2 = channel 0..6
|
|
||||||
byte3 = onMs low
|
|
||||||
byte4 = onMs high
|
|
||||||
byte5 = gapMs low
|
|
||||||
byte6 = gapMs high
|
|
||||||
byte7 = command sequence
|
|
||||||
```
|
|
||||||
|
|
||||||
Mega는 같은 sequence를 중복 수신하면 무시합니다. 명령을 수락하면 status 프레임의 `lastAcceptedCommandSeq`로 돌려줍니다.
|
|
||||||
|
|
||||||
### 0x200 Status
|
|
||||||
|
|
||||||
```text
|
|
||||||
byte0 = protocol version, 1
|
|
||||||
byte1 = flags
|
|
||||||
byte2 = packed climate levels
|
|
||||||
byte3 = toggle bits
|
|
||||||
byte4 = button bits
|
|
||||||
byte5 = lastAcceptedCommandSeq
|
|
||||||
byte6 = Mega pulse queue length
|
|
||||||
byte7 = last main heartbeat sequence
|
|
||||||
```
|
|
||||||
|
|
||||||
flags:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bit0 = main alive
|
|
||||||
bit1 = Mega pulse busy
|
|
||||||
bit2 = Mega pulse queue overflow
|
|
||||||
bit3 = main IG1 ON
|
|
||||||
bit4 = main P ON
|
|
||||||
```
|
|
||||||
|
|
||||||
packed climate levels:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bits 0..1 = 운전석 통풍 0..3
|
|
||||||
bits 2..3 = 운전석 열선 0..3
|
|
||||||
bits 4..5 = 조수석 통풍 0..3
|
|
||||||
bits 6..7 = 조수석 열선 0..3
|
|
||||||
```
|
|
||||||
|
|
||||||
toggle bits:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bit0 = 핸들 열선
|
|
||||||
bit1 = 전방센서
|
|
||||||
bit2 = VESS
|
|
||||||
```
|
|
||||||
|
|
||||||
button bits:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bit0..bit6 = 센터콘솔 버튼 눌림 상태
|
|
||||||
```
|
|
||||||
|
|
||||||
Mega가 MOSFET으로 직접 누른 버튼은 일정 시간 마스킹하여 사용자 직접 조작으로 오인하지 않습니다.
|
|
||||||
|
|
||||||
### 실제 패킷 예시
|
|
||||||
|
|
||||||
멀티바이트 시간값은 little-endian입니다. 예를 들어 `300ms`는 `0x012C`이므로 payload에는 `2C 01` 순서로 들어갑니다.
|
|
||||||
|
|
||||||
주행 중 Nano heartbeat 예시:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ID : 0x100
|
|
||||||
DLC : 8
|
|
||||||
DATA: 01 2D 2A 05 14 00 7B 00
|
|
||||||
```
|
|
||||||
|
|
||||||
해석:
|
|
||||||
|
|
||||||
```text
|
|
||||||
byte0 = 0x01 protocol version 1
|
|
||||||
byte1 = 0x2D IG1 ON, raw drive ON, confirmed drive ON, seat moving
|
|
||||||
byte2 = 0x2A heartbeat sequence 42
|
|
||||||
byte3 = 0x05 useCount 5
|
|
||||||
byte4 = 0x14 parking raw low
|
|
||||||
byte5 = 0x00 parking raw high, parkingRawValue = 20
|
|
||||||
byte6 = 0x7B uptime seconds low
|
|
||||||
byte7 = 0x00 uptime seconds high, uptime = 123s
|
|
||||||
```
|
|
||||||
|
|
||||||
운전석 통풍 버튼을 300ms 클릭하라는 명령 예시:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ID : 0x101
|
|
||||||
DLC : 8
|
|
||||||
DATA: 01 01 00 2C 01 C2 01 31
|
|
||||||
```
|
|
||||||
|
|
||||||
해석:
|
|
||||||
|
|
||||||
```text
|
|
||||||
byte0 = 0x01 protocol version 1
|
|
||||||
byte1 = 0x01 pulse command
|
|
||||||
byte2 = 0x00 channel 0, driver vent
|
|
||||||
byte3 = 0x2C onMs low
|
|
||||||
byte4 = 0x01 onMs high, onMs = 300
|
|
||||||
byte5 = 0xC2 gapMs low
|
|
||||||
byte6 = 0x01 gapMs high, gapMs = 450
|
|
||||||
byte7 = 0x31 command sequence 49
|
|
||||||
```
|
|
||||||
|
|
||||||
운전석 열선을 길게 눌러 OFF 기준화하는 명령 예시:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ID : 0x101
|
|
||||||
DLC : 8
|
|
||||||
DATA: 01 01 01 34 08 BC 02 32
|
|
||||||
```
|
|
||||||
|
|
||||||
해석:
|
|
||||||
|
|
||||||
```text
|
|
||||||
channel = 1, driver heat
|
|
||||||
onMs = 0x0834 = 2100ms
|
|
||||||
gapMs = 0x02BC = 700ms
|
|
||||||
sequence = 0x32
|
|
||||||
```
|
|
||||||
|
|
||||||
핸들 열선 20분 유지 재시작은 두 개의 pulse 명령으로 보냅니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
ID : 0x101
|
|
||||||
DATA: 01 01 04 2C 01 B0 04 33
|
|
||||||
|
|
||||||
ID : 0x101
|
|
||||||
DATA: 01 01 04 2C 01 C2 01 34
|
|
||||||
```
|
|
||||||
|
|
||||||
해석:
|
|
||||||
|
|
||||||
```text
|
|
||||||
첫 번째 frame: channel 4를 300ms 누르고 1200ms 대기
|
|
||||||
두 번째 frame: channel 4를 300ms 다시 누르고 450ms 대기
|
|
||||||
```
|
|
||||||
|
|
||||||
Mega status 예시:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ID : 0x200
|
|
||||||
DLC : 8
|
|
||||||
DATA: 01 0B 83 05 01 31 02 2A
|
|
||||||
```
|
|
||||||
|
|
||||||
해석:
|
|
||||||
|
|
||||||
```text
|
|
||||||
byte0 = 0x01 protocol version 1
|
|
||||||
byte1 = 0x0B main alive, Mega busy, main IG1 ON
|
|
||||||
byte2 = 0x83 driver vent 3, driver heat 0, passenger vent 0, passenger heat 2
|
|
||||||
byte3 = 0x05 steering heat ON, front sensor OFF, VESS ON
|
|
||||||
byte4 = 0x01 channel 0 button pressed
|
|
||||||
byte5 = 0x31 last accepted command sequence 49
|
|
||||||
byte6 = 0x02 Mega pulse queue length 2
|
|
||||||
byte7 = 0x2A last received Nano heartbeat sequence 42
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nano Serial Console
|
|
||||||
|
|
||||||
Nano 시리얼은 기본적으로 조용합니다.
|
|
||||||
|
|
||||||
1. `115200` baud로 연결합니다.
|
|
||||||
2. 빈 Enter를 한 번 보내면 help가 출력되고 콘솔이 열립니다.
|
|
||||||
3. 이후에는 한 글자 명령만 받습니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
s 현재 상태 출력
|
|
||||||
u 수동 상승 +500ms
|
|
||||||
d 수동 하강 -500ms
|
|
||||||
p 현재 추정 위치를 EEPROM에 저장
|
|
||||||
g 1초 간격 debug 출력 시작
|
|
||||||
x debug 모드만 종료
|
|
||||||
a 기어 상태 기반 자동 목표로 복귀
|
|
||||||
c 보정 실행: 상승 7초, 하강 6초, 현재 위치 0 설정
|
|
||||||
r 10회 자동 보정 카운터 초기화
|
|
||||||
h help 출력
|
|
||||||
```
|
|
||||||
|
|
||||||
`a`, `c`, `r`은 안전을 위해 P단에서만 실행됩니다.
|
|
||||||
|
|
||||||
상태 출력에는 센터콘솔 상태도 함께 포함됩니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
consoleCanReady=true
|
|
||||||
consoleAlive=true
|
|
||||||
consoleBusy=false
|
|
||||||
consolePulseQueueLength=0
|
|
||||||
consoleDriverVentTarget=0
|
|
||||||
consoleDriverVentActual=0
|
|
||||||
consoleSteeringHeatTarget=false
|
|
||||||
consoleSteeringHeatActual=false
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mega Serial Console
|
|
||||||
|
|
||||||
Mega 시리얼은 I/O 노드 디버그용입니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
h help 출력
|
|
||||||
s 상태와 ledRaw0..ledRaw14 출력
|
|
||||||
x 모든 출력 OFF, pulse queue 비움
|
|
||||||
```
|
|
||||||
|
|
||||||
Mega에는 EEPROM 메모리나 자동 판단 로직이 없습니다. Mega가 리셋되어도 Nano가 다음 상태 보고를 받은 뒤 필요한 명령을 다시 보냅니다.
|
|
||||||
|
|
||||||
## 자동 보정
|
|
||||||
|
|
||||||
- `IG1 HIGH -> LOW` 전환이 발생할 때마다 EEPROM useCount를 1 올립니다.
|
|
||||||
- 10회가 되면 자동 보정을 예약합니다.
|
|
||||||
- IG1 OFF 상태가 5분 유지되면 자동 보정을 실행합니다.
|
|
||||||
|
|
||||||
```text
|
|
||||||
up 7000ms
|
|
||||||
down 6000ms
|
|
||||||
currentPositionMs = 0
|
|
||||||
targetPositionMs = 0
|
|
||||||
useCount = 0
|
|
||||||
```
|
|
||||||
|
|
||||||
예약 상태에서 IG1이 다시 켜지면 보정은 대기 상태로 남고, 다음 IG1 OFF 후 5분 조건을 다시 봅니다. 수동 `c` 보정은 P단에서만 가능하고, 시작되면 끝까지 수행합니다.
|
|
||||||
|
|
||||||
## 빌드와 업로드
|
|
||||||
|
|
||||||
Nano 컴파일:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
& "C:\Program Files\Arduino CLI\arduino-cli.exe" compile --fqbn arduino:avr:nano:cpu=atmega328 --warnings all .
|
|
||||||
```
|
|
||||||
|
|
||||||
Nano 업로드:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\upload.ps1 -Port COM3
|
|
||||||
```
|
|
||||||
|
|
||||||
Mega 컴파일:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
& "C:\Program Files\Arduino CLI\arduino-cli.exe" compile --fqbn arduino:avr:mega .\Console_Node --warnings all
|
|
||||||
```
|
|
||||||
|
|
||||||
Mega 업로드 예시:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
& "C:\Program Files\Arduino CLI\arduino-cli.exe" upload --fqbn arduino:avr:mega --port COM4 .\Console_Node
|
|
||||||
```
|
|
||||||
|
|
||||||
## 운영 메모
|
|
||||||
|
|
||||||
- 커밋 전 `git status --short --branch`로 의도하지 않은 파일이 섞였는지 확인합니다.
|
|
||||||
- MCP2515 모듈의 crystal은 현재 `8MHz` 기준입니다. 16MHz 모듈이면 `MCP_8MHZ`를 바꿔야 합니다.
|
|
||||||
- CAN 종단저항은 노드가 2개일 때 양 끝 120Ω 구성을 기준으로 합니다.
|
|
||||||
- 센터콘솔 CAN 상태가 2초 이상 끊기면 Mega는 출력 OFF와 큐 초기화를 수행합니다.
|
|
||||||
- Nano가 Mega 상태를 2초 이상 받지 못하면 콘솔 명령 큐를 비우고 자동 적용을 다시 대기합니다.
|
|
||||||
- STATE_LED 임계값은 차량 실측 raw 값을 기준으로 조정해야 합니다.
|
|
||||||
- 시트 위치는 센서 기반이 아니라 시간 기반 추정이므로 전원 차단, 수동 조작, 릴레이 지연에 따라 실제 위치와 차이가 날 수 있습니다.
|
|
||||||
|
|
||||||
## 현재 기준값
|
|
||||||
|
|
||||||
```text
|
|
||||||
SEAT_HARD_LIMIT_UP_MS = 6000
|
|
||||||
DOWN_SPEED_NUM / DOWN_SPEED_DEN = 11 / 10
|
|
||||||
IGNITION_STABILIZE_MS = 1000
|
|
||||||
DRIVE_CONFIRM_MS = 80
|
|
||||||
PARK_CONFIRM_MS = 250
|
|
||||||
RELAY_DEADTIME_MS = 120
|
|
||||||
DOWN_EDGE_MARGIN_MS = 300
|
|
||||||
AUTO_CALIBRATION_DELAY_MS = 300000
|
|
||||||
AUTO_CALIBRATION_UP_MS = 7000
|
|
||||||
AUTO_CALIBRATION_DOWN_MS = 6000
|
|
||||||
CAN_HEARTBEAT_INTERVAL_MS = 250
|
|
||||||
CONSOLE_MEMORY_REFRESH_INTERVAL_MS = 1170000
|
|
||||||
CONSOLE_STEERING_RESTART_INTERVAL_MS = 1170000
|
|
||||||
```
|
|
||||||
+5
-15
@@ -187,13 +187,6 @@ targetPosMs = storedSeatPositionMs
|
|||||||
relay = UP until currentPosMs reaches targetPosMs</pre>
|
relay = UP until currentPosMs reaches targetPosMs</pre>
|
||||||
<p>P단 외 상태가 80ms 유지되면 EEPROM에 저장된 위치까지 상승합니다.</p>
|
<p>P단 외 상태가 80ms 유지되면 EEPROM에 저장된 위치까지 상승합니다.</p>
|
||||||
|
|
||||||
<h3>3.4.1 P단 외 상태에서 USB/시리얼 리셋</h3>
|
|
||||||
<pre>boot with IG1 = HIGH and P = LOW:
|
|
||||||
currentPosMs = storedSeatPositionMs
|
|
||||||
targetPosMs = storedSeatPositionMs
|
|
||||||
relay = OFF</pre>
|
|
||||||
<p>USB 시리얼 연결로 보드가 리셋되더라도 이미 주행 위치에 있다고 가정합니다. 따라서 리셋 직후 오상승하지 않고, 이후 P단으로 바뀌면 저장 위치 기준으로 하강합니다.</p>
|
|
||||||
|
|
||||||
<h3>3.5 P단 복귀 또는 시동 OFF</h3>
|
<h3>3.5 P단 복귀 또는 시동 OFF</h3>
|
||||||
<pre>confirmedDriveMode = LOW
|
<pre>confirmedDriveMode = LOW
|
||||||
targetPosMs = 0
|
targetPosMs = 0
|
||||||
@@ -205,7 +198,7 @@ when currentPosMs <= 0:
|
|||||||
<p>하강은 1.1배 속도로 위치를 추정하고, 바닥 도달 후 300ms 추가 하강으로 바닥 밀착감을 줍니다.</p>
|
<p>하강은 1.1배 속도로 위치를 추정하고, 바닥 도달 후 300ms 추가 하강으로 바닥 밀착감을 줍니다.</p>
|
||||||
|
|
||||||
<h2>4. 한 글자 시리얼 명령</h2>
|
<h2>4. 한 글자 시리얼 명령</h2>
|
||||||
<p>시리얼 연결 후 처음에는 잠금 상태입니다. 빈 Enter를 한 번 입력하면 도움말과 현재 상태를 출력하고, 그때부터 한 글자 명령을 사용할 수 있습니다. 잠금 해제 후 Enter 단독 입력은 아무 동작도 하지 않습니다. 운행 중에도 <code>u/d/p</code> 수동 위치 조정 및 저장은 가능하며, <code>a/c/r</code> 관리 명령은 P단에서만 실행됩니다.</p>
|
<p>시리얼 연결 후 처음에는 잠금 상태입니다. 빈 Enter를 한 번 입력하면 도움말과 현재 상태를 출력하고, 그때부터 한 글자 명령을 사용할 수 있습니다. 잠금 해제 후 Enter 단독 입력은 아무 동작도 하지 않습니다. 안전을 위해 <code>p/u/d/a/c/r</code> 명령은 P단에서만 실행됩니다.</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>명령</th><th>동작</th><th>응답</th></tr>
|
<tr><th>명령</th><th>동작</th><th>응답</th></tr>
|
||||||
@@ -224,9 +217,6 @@ when currentPosMs <= 0:
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p><span class="warn">주의:</span> 명령은 한 글자만 받습니다. <code>debug</code>, <code>down</code>, <code>position 2000</code>처럼 긴 문자열은 실행하지 않습니다.</p>
|
<p><span class="warn">주의:</span> 명령은 한 글자만 받습니다. <code>debug</code>, <code>down</code>, <code>position 2000</code>처럼 긴 문자열은 실행하지 않습니다.</p>
|
||||||
<p>모든 상태 출력은 항목별 한 줄 형식이며, 부팅 후 경과 시간이 <code>uptime=0h 2m 13s 457ms</code> 형식으로 포함됩니다. debug 출력에서도 1초마다 같은 형식으로 확인할 수 있습니다.</p>
|
|
||||||
<p>사용 횟수가 10회에 도달한 경우에만 보정 대기 타이머가 <code>autoCalibrationOffTimer=elapsed 0h 1m 20s 123ms remaining 0h 3m 39s 877ms</code> 형식으로 추가 출력됩니다. IG1 ON 상태에서는 <code>autoCalibrationOffTimer=waiting_for_ig_off</code>로 표시됩니다.</p>
|
|
||||||
<p>debug 출력은 1초마다 항목별 한 줄 형식으로 표시되며, <code>ignition=true</code>, <code>parking=false</code>, <code>autoCalibrationPending=false</code>처럼 bool 값은 <code>true</code>/<code>false</code>로 출력됩니다.</p>
|
|
||||||
|
|
||||||
<h2>5. 수동 위치 세팅 시뮬레이션</h2>
|
<h2>5. 수동 위치 세팅 시뮬레이션</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -271,7 +261,7 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
|
|||||||
<h2>8. 현재 주의 사항</h2>
|
<h2>8. 현재 주의 사항</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>시트 위치는 실제 센서가 아니라 시간 기반 추정값입니다.</li>
|
<li>시트 위치는 실제 센서가 아니라 시간 기반 추정값입니다.</li>
|
||||||
<li>전원 차단 후 P단/IG1 OFF 상태로 재부팅하면 <code>currentPosMs</code>는 0에서 시작합니다. 단, IG1 ON + P단 외 상태에서 재부팅하면 저장 위치에서 시작한 것으로 간주합니다.</li>
|
<li>전원 차단 후 재부팅하면 <code>currentPosMs</code>는 0에서 시작합니다.</li>
|
||||||
<li><code>u</code>/<code>d</code>는 위치 조절만 하고 EEPROM 저장은 하지 않습니다. 저장은 반드시 <code>p</code>로 합니다.</li>
|
<li><code>u</code>/<code>d</code>는 위치 조절만 하고 EEPROM 저장은 하지 않습니다. 저장은 반드시 <code>p</code>로 합니다.</li>
|
||||||
<li><code>d</code>는 하강, <code>g</code>는 debug입니다. debug 충돌을 피하기 위해 <code>d</code>를 debug로 쓰지 않습니다.</li>
|
<li><code>d</code>는 하강, <code>g</code>는 debug입니다. debug 충돌을 피하기 위해 <code>d</code>를 debug로 쓰지 않습니다.</li>
|
||||||
<li>debug 상태에서는 <code>x</code>만 작동하며, <code>r</code>, <code>a</code>, <code>s</code>, <code>u</code>, <code>d</code>, <code>p</code>, <code>c</code>는 모두 무시됩니다.</li>
|
<li>debug 상태에서는 <code>x</code>만 작동하며, <code>r</code>, <code>a</code>, <code>s</code>, <code>u</code>, <code>d</code>, <code>p</code>, <code>c</code>는 모두 무시됩니다.</li>
|
||||||
@@ -289,9 +279,9 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
|
|||||||
<td>연속 입력으로 500ms 단위가 흐트러지는 문제 방지</td>
|
<td>연속 입력으로 500ms 단위가 흐트러지는 문제 방지</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P단이 아닌 상태에서 <code>a/c/r</code> 입력</td>
|
<td>P단이 아닌 상태에서 <code>p/u/d/a/c/r</code> 입력</td>
|
||||||
<td><code>ERR: check gear state. Command requires P.</code> 출력</td>
|
<td><code>ERR: check gear state. Command requires P.</code> 출력</td>
|
||||||
<td>자동모드 복귀, 보정, 카운터 초기화가 P단 외 상태에서 실행되는 문제 방지</td>
|
<td>수동 이동, 저장, 보정, EEPROM 변경이 P단 외 상태에서 실행되는 문제 방지</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>수동 이동 완료 전 기어 상태 변경</td>
|
<td>수동 이동 완료 전 기어 상태 변경</td>
|
||||||
@@ -300,7 +290,7 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>내부 위치값 범위 이탈</td>
|
<td>내부 위치값 범위 이탈</td>
|
||||||
<td><code>currentPositionMs</code>, <code>targetPositionMs</code>를 0..6000으로 보정</td>
|
<td><code>currentMs</code>, <code>targetMs</code>를 0..6000으로 보정</td>
|
||||||
<td>시간 계산 오차나 상태 꼬임으로 인한 비정상 목표 방지</td>
|
<td>시간 계산 오차나 상태 꼬임으로 인한 비정상 목표 방지</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in new issue
Block a user