// // Auto_Seat // Automatic seat controller with EEPROM settings and optional serial tools. // #include #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_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; 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(); 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); 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; } } boolean getRawDriveMode() { if (ignitionState == HIGH && parkingState == LOW) { return HIGH; } return LOW; } long getTargetForConfirmedMode() { if (confirmedDriveMode == HIGH) { return storedSeatPositionMs; } return 0; } void updateConfirmedDriveMode(unsigned long currentTime) { boolean rawDriveMode = getRawDriveMode(); if (rawDriveMode != pendingDriveMode) { pendingDriveMode = rawDriveMode; pendingDriveModeStartTime = currentTime; } unsigned long confirmMs = pendingDriveMode == HIGH ? DRIVE_CONFIRM_MS : PARK_CONFIRM_MS; 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) { seat(1); currentPosMs += deltaTime; if (currentPosMs >= targetPosMs) { currentPosMs = targetPosMs; seat(0); } } else if (currentPosMs > targetPosMs) { 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 { 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; writeUint16(EEPROM_SEAT_POSITION_ADDR, storedSeatPositionMs); } } 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); } }