Compare commits
2
Commits
main
..
fbac234807
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbac234807 | ||
|
|
f59d63ae83 |
No files matched your search
@@ -1,7 +0,0 @@
|
||||
build/
|
||||
*.hex
|
||||
*.elf
|
||||
*.eep
|
||||
*.map
|
||||
*.lst
|
||||
*.o
|
||||
+42
-688
@@ -3,8 +3,6 @@
|
||||
// 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)
|
||||
@@ -47,59 +45,11 @@ 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;
|
||||
@@ -142,38 +92,6 @@ 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);
|
||||
@@ -185,19 +103,12 @@ void setup() {
|
||||
|
||||
applySeatAction(0);
|
||||
seat(0);
|
||||
setupChaegeonCan();
|
||||
loadSettings();
|
||||
|
||||
measurement();
|
||||
confirmedDriveMode = getRawDriveMode();
|
||||
pendingDriveMode = confirmedDriveMode;
|
||||
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) {
|
||||
ignitionOffStartTime = millis();
|
||||
}
|
||||
@@ -220,7 +131,6 @@ void loop() {
|
||||
registerIgnitionOffUse(currentTime);
|
||||
}
|
||||
prevIgnitionState = ignitionState;
|
||||
updateChaegeonCan(currentTime);
|
||||
|
||||
if (autoCalibrationState != CALIBRATION_IDLE) {
|
||||
updateAutoCalibration(currentTime);
|
||||
@@ -520,7 +430,10 @@ void processSerialCommand(String command) {
|
||||
}
|
||||
|
||||
bool requiresParkingForCommand(char name) {
|
||||
return name == 'a' ||
|
||||
return name == 'p' ||
|
||||
name == 'u' ||
|
||||
name == 'd' ||
|
||||
name == 'a' ||
|
||||
name == 'c' ||
|
||||
name == 'r';
|
||||
}
|
||||
@@ -599,7 +512,7 @@ void updateDebugOutput(unsigned long currentTime) {
|
||||
|
||||
if (lastDebugPrintTime == 0 || currentTime - lastDebugPrintTime >= DEBUG_INTERVAL_MS) {
|
||||
lastDebugPrintTime = currentTime;
|
||||
printDebugLine();
|
||||
printStatus(F("DEBUG"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,8 +535,6 @@ void loadSettings() {
|
||||
storedSeatPositionMs = SEAT_HARD_LIMIT_UP_MS;
|
||||
writeUint16(EEPROM_SEAT_POSITION_ADDR, storedSeatPositionMs);
|
||||
}
|
||||
|
||||
loadConsoleSettings();
|
||||
}
|
||||
|
||||
uint16_t readUint16(int addr) {
|
||||
@@ -644,7 +555,7 @@ void printIntro() {
|
||||
Serial.println(F("+--------------------------------------------------+"));
|
||||
Serial.println(F("| Units: milliseconds. 0 = bottom. 6000 = top cap. |"));
|
||||
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.println(F("Commands:"));
|
||||
@@ -668,602 +579,45 @@ void printStatus(const __FlashStringHelper *tag) {
|
||||
Serial.print(F("["));
|
||||
Serial.print(tag);
|
||||
Serial.println(F("]"));
|
||||
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("raw="));
|
||||
Serial.print(parkingRawValue);
|
||||
Serial.print(F(" ig="));
|
||||
Serial.print(ignitionState == HIGH ? 1 : 0);
|
||||
Serial.print(F(" p="));
|
||||
Serial.print(parkingState == HIGH ? 1 : 0);
|
||||
Serial.print(F(" rawDrive="));
|
||||
Serial.print(getRawDriveMode() == HIGH ? 1 : 0);
|
||||
Serial.print(F(" confirmedDrive="));
|
||||
Serial.println(confirmedDriveMode == HIGH ? 1 : 0);
|
||||
|
||||
Serial.print(F("currentMs="));
|
||||
Serial.print(currentPosMs);
|
||||
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(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 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);
|
||||
Serial.print(USES_BEFORE_AUTO_CALIBRATION);
|
||||
Serial.print(F(" autoPending="));
|
||||
Serial.print(autoCalibrationPending ? 1 : 0);
|
||||
Serial.print(F(" forceCal="));
|
||||
Serial.print(forceCalibration ? 1 : 0);
|
||||
Serial.print(F(" autoCalState="));
|
||||
Serial.println(autoCalibrationState);
|
||||
}
|
||||
|
||||
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용 메인 스케치입니다.
|
||||
- `Console_Node/Console_Node.ino`: 센터콘솔 Mega용 I/O 노드 스케치입니다.
|
||||
- `upload.ps1`: Nano 업로드용 PowerShell 스크립트입니다.
|
||||
- `report.html`: 현재 동작 시뮬레이션과 운영 보고서입니다.
|
||||
### `Auto_Seat.ino`
|
||||
|
||||
## 메인 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
|
||||
- 상승 릴레이: `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`
|
||||
### `report.html`
|
||||
|
||||
Nano의 기존 시트 제어 핀과 MCP2515 SPI 핀은 충돌하지 않습니다.
|
||||
- 역할: 프로젝트 보고서 또는 설명을 담은 HTML 문서입니다.
|
||||
- include/의존성: -
|
||||
- 주요 함수: -
|
||||
- 주요 전역값/설정값: -
|
||||
- 핀/입출력 설정: -
|
||||
- 입출력/통신 호출: EEPROM.update
|
||||
|
||||
## 센터콘솔 Mega 하드웨어
|
||||
### `upload.ps1`
|
||||
|
||||
- MCU: Arduino Mega
|
||||
- CAN 모듈: MCP2515 + TJA1050, 8MHz crystal
|
||||
- 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`
|
||||
- 역할: 업로드/자동화에 사용되는 PowerShell 스크립트입니다.
|
||||
- include/의존성: -
|
||||
- 주요 함수: -
|
||||
- 주요 전역값/설정값: -
|
||||
- 핀/입출력 설정: -
|
||||
- 입출력/통신 호출: -
|
||||
|
||||
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
|
||||
0 = 운전석 통풍
|
||||
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
|
||||
```
|
||||
- Arduino IDE 또는 PlatformIO에서 폴더명과 같은 대표 스케치를 열어 빌드합니다.
|
||||
- 코드에 포함된 네트워크 주소, Wi-Fi 정보, DB 정보, 장치 핀 번호는 실제 하드웨어 구성에 맞춰 확인해야 합니다.
|
||||
- 민감한 값이 코드에 포함될 수 있으므로 저장소는 private로 유지합니다.
|
||||
+5
-15
@@ -187,13 +187,6 @@ targetPosMs = storedSeatPositionMs
|
||||
relay = UP until currentPosMs reaches targetPosMs</pre>
|
||||
<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>
|
||||
<pre>confirmedDriveMode = LOW
|
||||
targetPosMs = 0
|
||||
@@ -205,7 +198,7 @@ when currentPosMs <= 0:
|
||||
<p>하강은 1.1배 속도로 위치를 추정하고, 바닥 도달 후 300ms 추가 하강으로 바닥 밀착감을 줍니다.</p>
|
||||
|
||||
<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>
|
||||
<thead>
|
||||
<tr><th>명령</th><th>동작</th><th>응답</th></tr>
|
||||
@@ -224,9 +217,6 @@ when currentPosMs <= 0:
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
<table>
|
||||
@@ -271,7 +261,7 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
|
||||
<h2>8. 현재 주의 사항</h2>
|
||||
<ul>
|
||||
<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>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>
|
||||
@@ -289,9 +279,9 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
|
||||
<td>연속 입력으로 500ms 단위가 흐트러지는 문제 방지</td>
|
||||
</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>자동모드 복귀, 보정, 카운터 초기화가 P단 외 상태에서 실행되는 문제 방지</td>
|
||||
<td>수동 이동, 저장, 보정, EEPROM 변경이 P단 외 상태에서 실행되는 문제 방지</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>수동 이동 완료 전 기어 상태 변경</td>
|
||||
@@ -300,7 +290,7 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
|
||||
</tr>
|
||||
<tr>
|
||||
<td>내부 위치값 범위 이탈</td>
|
||||
<td><code>currentPositionMs</code>, <code>targetPositionMs</code>를 0..6000으로 보정</td>
|
||||
<td><code>currentMs</code>, <code>targetMs</code>를 0..6000으로 보정</td>
|
||||
<td>시간 계산 오차나 상태 꼬임으로 인한 비정상 목표 방지</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
Reference in new issue
Block a user