diff --git a/Auto_Seat.ino b/Auto_Seat.ino index d89d326..4132657 100644 --- a/Auto_Seat.ino +++ b/Auto_Seat.ino @@ -1,104 +1,535 @@ +// // Auto_Seat -// EEPROM-backed automatic seat controller. +// Automatic seat controller with EEPROM settings and optional serial tools. +// #include -#define seatRelayD8 8 -#define seatRelayD9 9 -#define parkingPin A0 -#define ignitionPin 7 +#define seatRelayD8 8 // HIGH = OFF, LOW = ON (up relay) +#define seatRelayD9 9 // HIGH = OFF, LOW = ON (down relay) +#define parkingPin A0 // PARKING raw analog value, 0~1023 +#define ignitionPin 7 // HIGH = IG1 ON, LOW = IG1 OFF + +// ============================================================ +// !!! SAFETY LIMIT !!! +// +// Physical maximum travel time from bottom to the highest allowed point. +// User settings saved in EEPROM must stay inside 0..6000ms. +// ============================================================ const uint16_t SEAT_HARD_LIMIT_UP_MS = 6000; + +// Parking raw threshold hysteresis. +// P turns ON at 950 or higher, and turns OFF at 940 or lower. const int PARKING_ON_THRESHOLD = 950; const int PARKING_OFF_THRESHOLD = 940; + +// Down movement is estimated as 1.1x faster than up movement. const int DOWN_SPEED_NUM = 11; const int DOWN_SPEED_DEN = 10; + +// Input and relay protection. +const unsigned long IGNITION_STABILIZE_MS = 1000UL; const unsigned long DRIVE_CONFIRM_MS = 80UL; const unsigned long PARK_CONFIRM_MS = 250UL; +const unsigned long RELAY_DEADTIME_MS = 120UL; +const unsigned long DOWN_EDGE_MARGIN_MS = 300UL; + +// After 10 ignition OFF events, wait 5 minutes while ignition is OFF, +// then auto-calibrate: up 7 seconds -> down 6 seconds -> set position to bottom. +const byte USES_BEFORE_AUTO_CALIBRATION = 10; +const unsigned long AUTO_CALIBRATION_DELAY_MS = 300000UL; +const unsigned long AUTO_CALIBRATION_UP_MS = 7000UL; +const unsigned long AUTO_CALIBRATION_DOWN_MS = 6000UL; + +// Serial debug output. +const unsigned long DEBUG_INTERVAL_MS = 1000UL; +const long MANUAL_STEP_MS = 500; + +// EEPROM map. const int EEPROM_MAGIC_ADDR = 0; const byte EEPROM_MAGIC_VALUE = 0x64; -const int EEPROM_SEAT_POSITION_ADDR = 2; +const int EEPROM_USE_COUNT_ADDR = 1; +const int EEPROM_SEAT_POSITION_ADDR = 2; // uint16_t, low byte first + +const byte CALIBRATION_IDLE = 0; +const byte CALIBRATION_UP = 1; +const byte CALIBRATION_DOWN = 2; boolean ignitionState = LOW; boolean parkingState = LOW; +boolean prevIgnitionState = LOW; +boolean isStabilizing = false; boolean confirmedDriveMode = LOW; boolean pendingDriveMode = LOW; + +byte useCount = 0; +byte autoCalibrationState = CALIBRATION_IDLE; +bool autoCalibrationPending = false; +bool forceCalibration = false; +bool manualMode = false; + int parkingRawValue = 0; uint16_t storedSeatPositionMs = 0; long currentPosMs = 0; long targetPosMs = 0; long downSpeedRemainder = 0; +unsigned long downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; +bool downEdgeMarginActive = false; + unsigned long lastTime = 0; +unsigned long stabilizeStartTime = 0; +unsigned long ignitionOffStartTime = 0; +unsigned long calibrationPhaseStartTime = 0; unsigned long pendingDriveModeStartTime = 0; -int seatAction = 0; +unsigned long relayDeadtimeStartTime = 0; +unsigned long lastDebugPrintTime = 0; + +int currentSeatAction = 0; +int requestedSeatAction = 0; +int lastStoppedSeatAction = 0; + +bool serialIntroPrinted = false; +bool debugEnabled = false; +bool manualStepStatusPending = false; void setup() { + Serial.begin(115200); + Serial.setTimeout(25); + pinMode(parkingPin, INPUT); pinMode(ignitionPin, INPUT); pinMode(seatRelayD8, OUTPUT); pinMode(seatRelayD9, OUTPUT); + applySeatAction(0); + seat(0); loadSettings(); + measurement(); confirmedDriveMode = getRawDriveMode(); pendingDriveMode = confirmedDriveMode; targetPosMs = getTargetForConfirmedMode(); + if (ignitionState == LOW) { + ignitionOffStartTime = millis(); + } + lastTime = millis(); } void loop() { measurement(); - unsigned long now = millis(); - unsigned long deltaTime = now - lastTime; - lastTime = now; - updateConfirmedDriveMode(now); - targetPosMs = getTargetForConfirmedMode(); + handleSerial(); + + unsigned long currentTime = millis(); + unsigned long deltaTime = currentTime - lastTime; + lastTime = currentTime; + + if (prevIgnitionState == LOW && ignitionState == HIGH) { + isStabilizing = true; + stabilizeStartTime = currentTime; + } else if (prevIgnitionState == HIGH && ignitionState == LOW) { + registerIgnitionOffUse(currentTime); + } + prevIgnitionState = ignitionState; + + if (autoCalibrationState != CALIBRATION_IDLE) { + updateAutoCalibration(currentTime); + updateRelayOutput(currentTime); + updateDebugOutput(currentTime); + return; + } + + if (autoCalibrationPending && ignitionState == LOW && + currentTime - ignitionOffStartTime >= AUTO_CALIBRATION_DELAY_MS) { + startAutoCalibration(currentTime); + updateRelayOutput(currentTime); + updateDebugOutput(currentTime); + return; + } + + if (isStabilizing) { + if (currentTime - stabilizeStartTime < IGNITION_STABILIZE_MS) { + seat(0); + updateRelayOutput(currentTime); + updateDebugOutput(currentTime); + return; + } + isStabilizing = false; + } + + updateConfirmedDriveMode(currentTime); + if (!manualMode) { + targetPosMs = getTargetForConfirmedMode(); + } + normalizeMotionState(); updateMovement(deltaTime); - applySeatAction(seatAction); + normalizeMotionState(); + updateRelayOutput(currentTime); + updateManualStepStatus(); + updateDebugOutput(currentTime); } void measurement() { ignitionState = digitalRead(ignitionPin); + parkingRawValue = analogRead(parkingPin); - if (parkingState == LOW && parkingRawValue >= PARKING_ON_THRESHOLD) parkingState = HIGH; - else if (parkingState == HIGH && parkingRawValue <= PARKING_OFF_THRESHOLD) parkingState = LOW; + if (parkingState == LOW && parkingRawValue >= PARKING_ON_THRESHOLD) { + parkingState = HIGH; + } else if (parkingState == HIGH && parkingRawValue <= PARKING_OFF_THRESHOLD) { + parkingState = LOW; + } } -boolean getRawDriveMode() { return ignitionState == HIGH && parkingState == LOW; } -long getTargetForConfirmedMode() { return confirmedDriveMode == HIGH ? storedSeatPositionMs : 0; } +boolean getRawDriveMode() { + if (ignitionState == HIGH && parkingState == LOW) { + return HIGH; + } + return LOW; +} -void updateConfirmedDriveMode(unsigned long now) { +long getTargetForConfirmedMode() { + if (confirmedDriveMode == HIGH) { + return storedSeatPositionMs; + } + return 0; +} + +void updateConfirmedDriveMode(unsigned long currentTime) { boolean rawDriveMode = getRawDriveMode(); + if (rawDriveMode != pendingDriveMode) { pendingDriveMode = rawDriveMode; - pendingDriveModeStartTime = now; + pendingDriveModeStartTime = currentTime; } + unsigned long confirmMs = pendingDriveMode == HIGH ? DRIVE_CONFIRM_MS : PARK_CONFIRM_MS; - if (pendingDriveMode != confirmedDriveMode && now - pendingDriveModeStartTime >= confirmMs) { + if (pendingDriveMode != confirmedDriveMode && + currentTime - pendingDriveModeStartTime >= confirmMs) { confirmedDriveMode = pendingDriveMode; + manualMode = false; + manualStepStatusPending = false; } } void updateMovement(unsigned long deltaTime) { + if (downEdgeMarginActive) { + if (currentPosMs == 0 && targetPosMs == 0) { + seat(-1); + downEdgeMarginTime += deltaTime; + if (downEdgeMarginTime >= DOWN_EDGE_MARGIN_MS) { + downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; + downEdgeMarginActive = false; + seat(0); + } + return; + } + + downEdgeMarginActive = false; + downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; + } + if (currentPosMs < targetPosMs) { - seatAction = 1; + seat(1); currentPosMs += deltaTime; - if (currentPosMs >= targetPosMs) { currentPosMs = targetPosMs; seatAction = 0; } + if (currentPosMs >= targetPosMs) { + currentPosMs = targetPosMs; + seat(0); + } } else if (currentPosMs > targetPosMs) { - seatAction = -1; - long scaled = deltaTime * DOWN_SPEED_NUM + downSpeedRemainder; - currentPosMs -= scaled / DOWN_SPEED_DEN; - downSpeedRemainder = scaled % DOWN_SPEED_DEN; - if (currentPosMs <= targetPosMs) { currentPosMs = targetPosMs; downSpeedRemainder = 0; seatAction = 0; } + seat(-1); + long downMoveScaled = deltaTime * DOWN_SPEED_NUM + downSpeedRemainder; + currentPosMs -= downMoveScaled / DOWN_SPEED_DEN; + downSpeedRemainder = downMoveScaled % DOWN_SPEED_DEN; + if (currentPosMs <= targetPosMs) { + currentPosMs = targetPosMs; + downSpeedRemainder = 0; + if (targetPosMs == 0) { + startDownEdgeMargin(); + } else { + seat(0); + } + } } else { - seatAction = 0; + seat(0); + } +} + +void startDownEdgeMargin() { + downEdgeMarginActive = true; + downEdgeMarginTime = 0; + seat(-1); +} + +void registerIgnitionOffUse(unsigned long currentTime) { + ignitionOffStartTime = currentTime; + + if (useCount < USES_BEFORE_AUTO_CALIBRATION) { + useCount++; + EEPROM.update(EEPROM_USE_COUNT_ADDR, useCount); + } + + if (useCount >= USES_BEFORE_AUTO_CALIBRATION) { + autoCalibrationPending = true; + } +} + +void startAutoCalibration(unsigned long currentTime) { + manualMode = false; + manualStepStatusPending = false; + downSpeedRemainder = 0; + downEdgeMarginActive = false; + downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; + autoCalibrationState = CALIBRATION_UP; + calibrationPhaseStartTime = currentTime; + seat(1); +} + +void updateAutoCalibration(unsigned long currentTime) { + if (ignitionState == HIGH && !forceCalibration) { + seat(0); + autoCalibrationState = CALIBRATION_IDLE; + manualStepStatusPending = false; + downSpeedRemainder = 0; + downEdgeMarginActive = false; + downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; + ignitionOffStartTime = currentTime; + return; + } + + if (autoCalibrationState == CALIBRATION_UP) { + if (currentTime - calibrationPhaseStartTime >= AUTO_CALIBRATION_UP_MS) { + autoCalibrationState = CALIBRATION_DOWN; + calibrationPhaseStartTime = currentTime; + seat(-1); + } else { + seat(1); + } + } else if (autoCalibrationState == CALIBRATION_DOWN) { + if (currentTime - calibrationPhaseStartTime >= AUTO_CALIBRATION_DOWN_MS) { + seat(0); + autoCalibrationState = CALIBRATION_IDLE; + autoCalibrationPending = false; + forceCalibration = false; + useCount = 0; + EEPROM.update(EEPROM_USE_COUNT_ADDR, useCount); + currentPosMs = 0; + targetPosMs = 0; + downSpeedRemainder = 0; + downEdgeMarginActive = false; + downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; + } else { + seat(-1); + } + } +} + +void handleSerial() { + if (!Serial.available()) { + return; + } + + String command = Serial.readStringUntil('\n'); + command.trim(); + + if (debugEnabled) { + command.toLowerCase(); + if (command == "x") { + debugEnabled = false; + Serial.println(F("DEBUG_STOPPED")); + printStatus(F("STATUS")); + } + return; + } + + if (!serialIntroPrinted) { + if (command.length() == 0) { + printIntro(); + serialIntroPrinted = true; + } + return; + } + + if (command.length() == 0) { + return; + } + + processSerialCommand(command); +} + +bool isSeatCommandBusy() { + return manualStepStatusPending || + autoCalibrationState != CALIBRATION_IDLE || + downEdgeMarginActive || + currentPosMs != targetPosMs || + requestedSeatAction != 0 || + currentSeatAction != 0; +} + +void processSerialCommand(String command) { + command.trim(); + command.toLowerCase(); + if (command.length() == 0) { + printStatus(F("EMPTY_INPUT_STATUS")); + return; + } + + if (command.length() != 1) { + Serial.println(F("ERR: one-character command only. Send h for help.")); + printStatus(F("ACK")); + return; + } + + char name = command.charAt(0); + + if (requiresParkingForCommand(name) && parkingState != HIGH) { + Serial.println(F("ERR: check gear state. Command requires P.")); + printStatus(F("GEAR_CHECK_FAILED")); + return; + } + + if (isSeatCommandBusy()) { + Serial.println(F("ERR: busy. Wait until seat movement is done.")); + printStatus(F("BUSY")); + return; + } + + if (name == 'h') { + printIntro(); + } else if (name == 's') { + printStatus(F("STATUS")); + } else if (name == 'p') { + saveCurrentPositionCommand(); + } else if (name == 'u') { + handleRelativeStep(1); + } else if (name == 'd') { + handleRelativeStep(-1); + } else if (name == 'g') { + debugEnabled = true; + lastDebugPrintTime = 0; + printStatus(F("DEBUG_STARTED_X_TO_STOP")); + } else if (name == 'a') { + manualStepStatusPending = false; + manualMode = false; + targetPosMs = getTargetForConfirmedMode(); + printStatus(F("AUTO_MODE_RESTORED")); + } else if (name == 'c') { + manualStepStatusPending = false; + forceCalibration = true; + autoCalibrationPending = false; + startAutoCalibration(millis()); + printStatus(F("CALIBRATION_STARTED_UP7_DOWN6")); + } else if (name == 'r') { + useCount = 0; + autoCalibrationPending = false; + EEPROM.update(EEPROM_USE_COUNT_ADDR, useCount); + printStatus(F("USE_COUNT_RESET")); + } else { + Serial.println(F("Unknown command. Send h for help.")); + printStatus(F("ACK")); + } +} + +bool requiresParkingForCommand(char name) { + return name == 'p' || + name == 'u' || + name == 'd' || + name == 'a' || + name == 'c' || + name == 'r'; +} + +void saveCurrentPositionCommand() { + currentPosMs = clampPosition(currentPosMs); + targetPosMs = currentPosMs; + + storedSeatPositionMs = (uint16_t)currentPosMs; + writeUint16(EEPROM_SEAT_POSITION_ADDR, storedSeatPositionMs); + manualMode = true; + targetPosMs = currentPosMs; + printStatus(F("CURRENT_POSITION_SAVED")); +} + +void handleRelativeStep(int direction) { + long nextTarget = currentPosMs + (direction * MANUAL_STEP_MS); + if (nextTarget < 0 || nextTarget > SEAT_HARD_LIMIT_UP_MS) { + Serial.print(F("WARN: ignored. Requested target ")); + Serial.print(nextTarget); + Serial.print(F("ms is outside 0..")); + Serial.print(SEAT_HARD_LIMIT_UP_MS); + Serial.println(F("ms.")); + printStatus(F("ACK")); + return; + } + + manualMode = true; + targetPosMs = nextTarget; + manualStepStatusPending = true; +} + +void updateManualStepStatus() { + if (!manualStepStatusPending) { + return; + } + + if (currentPosMs == targetPosMs && requestedSeatAction == 0 && currentSeatAction == 0) { + manualStepStatusPending = false; + printStatus(F("MANUAL_STEP_DONE")); + } +} + +long clampPosition(long value) { + if (value < 0) { + return 0; + } + if (value > SEAT_HARD_LIMIT_UP_MS) { + return SEAT_HARD_LIMIT_UP_MS; + } + return value; +} + +void normalizeMotionState() { + currentPosMs = clampPosition(currentPosMs); + targetPosMs = clampPosition(targetPosMs); + + if (storedSeatPositionMs > SEAT_HARD_LIMIT_UP_MS) { + storedSeatPositionMs = SEAT_HARD_LIMIT_UP_MS; + } + + if (targetPosMs != 0 && downEdgeMarginActive) { + downEdgeMarginActive = false; + downEdgeMarginTime = DOWN_EDGE_MARGIN_MS; + } + + if (currentPosMs <= targetPosMs) { + downSpeedRemainder = 0; + } +} + +void updateDebugOutput(unsigned long currentTime) { + if (!debugEnabled) { + return; + } + + if (lastDebugPrintTime == 0 || currentTime - lastDebugPrintTime >= DEBUG_INTERVAL_MS) { + lastDebugPrintTime = currentTime; + printStatus(F("DEBUG")); } } void loadSettings() { if (EEPROM.read(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC_VALUE) { EEPROM.update(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE); + EEPROM.update(EEPROM_USE_COUNT_ADDR, 0); writeUint16(EEPROM_SEAT_POSITION_ADDR, SEAT_HARD_LIMIT_UP_MS); } + + useCount = EEPROM.read(EEPROM_USE_COUNT_ADDR); + if (useCount > USES_BEFORE_AUTO_CALIBRATION) { + useCount = 0; + EEPROM.update(EEPROM_USE_COUNT_ADDR, useCount); + } + autoCalibrationPending = (useCount >= USES_BEFORE_AUTO_CALIBRATION); + storedSeatPositionMs = readUint16(EEPROM_SEAT_POSITION_ADDR); if (storedSeatPositionMs > SEAT_HARD_LIMIT_UP_MS) { storedSeatPositionMs = SEAT_HARD_LIMIT_UP_MS; @@ -106,11 +537,129 @@ void loadSettings() { } } -uint16_t readUint16(int addr) { return (uint16_t)EEPROM.read(addr) | ((uint16_t)EEPROM.read(addr + 1) << 8); } -void writeUint16(int addr, uint16_t value) { EEPROM.update(addr, value & 0xFF); EEPROM.update(addr + 1, (value >> 8) & 0xFF); } -void setRelay(int relayPin, boolean state) { digitalWrite(relayPin, state ? LOW : HIGH); } -void applySeatAction(int action) { - if (action == 1) { setRelay(seatRelayD8, 1); setRelay(seatRelayD9, 0); } - else if (action == -1) { setRelay(seatRelayD8, 0); setRelay(seatRelayD9, 1); } - else { setRelay(seatRelayD8, 0); setRelay(seatRelayD9, 0); } +uint16_t readUint16(int addr) { + byte lowByte = EEPROM.read(addr); + byte highByte = EEPROM.read(addr + 1); + return (uint16_t)lowByte | ((uint16_t)highByte << 8); +} + +void writeUint16(int addr, uint16_t value) { + EEPROM.update(addr, value & 0xFF); + EEPROM.update(addr + 1, (value >> 8) & 0xFF); +} + +void printIntro() { + Serial.println(); + Serial.println(F("+--------------------------------------------------+")); + Serial.println(F("| Auto Seat Serial Console |")); + 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("| 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:")); + Serial.println(F(" s Print current state once")); + Serial.println(F(" p Save current position to EEPROM")); + Serial.println(F(" u Manual up +500ms if inside 0..6000")); + Serial.println(F(" d Manual down -500ms if inside 0..6000")); + Serial.println(F(" g Debug every 1s; only x works in debug")); + Serial.println(F(" x Stop debug mode only")); + Serial.println(F(" d=down, g=debug to avoid command overlap")); + Serial.println(F(" a Return to gear-based automatic target")); + Serial.println(F(" c Run calibration now: up 7s, down 6s, set 0")); + Serial.println(F(" r Clear 10-use auto calibration counter")); + Serial.println(F(" h Print this help")); + Serial.println(F("+--------------------------------------------------+")); + printStatus(F("CURRENT_SETTINGS")); +} + +void printStatus(const __FlashStringHelper *tag) { + Serial.println(); + Serial.print(F("[")); + Serial.print(tag); + Serial.println(F("]")); + 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.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) { + digitalWrite(relayPin, state ? LOW : HIGH); +} + +void seat(int action) { + requestedSeatAction = action; +} + +void updateRelayOutput(unsigned long currentTime) { + if (requestedSeatAction == currentSeatAction) { + applySeatAction(currentSeatAction); + return; + } + + if (currentSeatAction != 0) { + lastStoppedSeatAction = currentSeatAction; + currentSeatAction = 0; + relayDeadtimeStartTime = currentTime; + applySeatAction(0); + return; + } + + bool sameDirectionResume = requestedSeatAction != 0 && requestedSeatAction == lastStoppedSeatAction; + bool deadtimeDone = currentTime - relayDeadtimeStartTime >= RELAY_DEADTIME_MS; + if (sameDirectionResume || deadtimeDone) { + currentSeatAction = requestedSeatAction; + } + + applySeatAction(currentSeatAction); +} + +void applySeatAction(int action) { + if (action == 1) { + setRelay(seatRelayD8, 1); + setRelay(seatRelayD9, 0); + } else if (action == -1) { + setRelay(seatRelayD8, 0); + setRelay(seatRelayD9, 1); + } else { + setRelay(seatRelayD8, 0); + setRelay(seatRelayD9, 0); + } }