Initial project import

This commit is contained in:
seo
2026-06-07 01:08:04 +09:00
commit f59d63ae83
4 changed files with 1008 additions and 0 deletions
+665
View File
@@ -0,0 +1,665 @@
//
// Auto_Seat
// Automatic seat controller with EEPROM settings and optional serial tools.
//
#include <EEPROM.h>
#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);
}
}
+25
View File
@@ -0,0 +1,25 @@
# Auto_Seat
## Overview
- Type: Arduino project
- Source directory: `Auto_Seat`
- Files: 3
- Related classification: AutoSeat_UL, Auto_Seat_LL 계열과 관련된 자동 시트 프로젝트입니다.
- Report/reference file: `report.html`
## Arduino Sketches
- `Auto_Seat.ino`
## Root Files
- `Auto_Seat.ino`
- `report.html`
- `upload.ps1`
## Notes
- This repository is intended to be stored as a private project repository on `git.chaegeon.com`.
- Sensitive configuration may exist in source files and is kept private by repository visibility.
- Exact duplicate top-level project folders were checked before repository preparation.
+305
View File
@@ -0,0 +1,305 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Auto Seat 동작 시뮬레이션 보고서</title>
<style>
:root {
--ink: #17202a;
--muted: #5f6b77;
--line: #d8e0e8;
--soft: #f5f7fa;
--ok: #0b7a3b;
--warn: #9a5b00;
--bad: #a22020;
}
body {
margin: 0;
font-family: Arial, "Noto Sans KR", sans-serif;
color: var(--ink);
line-height: 1.55;
background: #fff;
}
header {
padding: 28px 36px;
border-bottom: 1px solid var(--line);
background: var(--soft);
}
main {
max-width: 1080px;
margin: 0 auto;
padding: 28px 24px 48px;
}
h1 {
margin: 0 0 6px;
font-size: 28px;
}
h2 {
margin-top: 34px;
padding-bottom: 8px;
border-bottom: 1px solid var(--line);
font-size: 21px;
}
h3 {
margin-top: 22px;
font-size: 17px;
}
.meta {
color: var(--muted);
font-size: 14px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
}
.card {
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px 16px;
background: #fff;
}
.card strong {
display: block;
margin-bottom: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 12px 0 18px;
font-size: 14px;
}
th, td {
border: 1px solid var(--line);
padding: 8px 10px;
vertical-align: top;
text-align: left;
}
th {
background: var(--soft);
}
code, pre {
font-family: Consolas, Monaco, "Courier New", monospace;
}
code {
background: #eef2f7;
padding: 1px 4px;
border-radius: 4px;
}
pre {
overflow-x: auto;
background: #101820;
color: #e8eef4;
padding: 14px 16px;
border-radius: 8px;
font-size: 13px;
}
.ok { color: var(--ok); font-weight: 700; }
.warn { color: var(--warn); font-weight: 700; }
.bad { color: var(--bad); font-weight: 700; }
</style>
</head>
<body>
<header>
<h1>Auto Seat 동작 시뮬레이션 보고서</h1>
<div class="meta">대상 파일: <code>Auto_Seat.ino</code> / 현재 코드 기준</div>
</header>
<main>
<h2>1. 현재 핵심 설정</h2>
<div class="grid">
<div class="card">
<strong>최대 상승 가능 위치</strong>
<code>SEAT_HARD_LIMIT_UP_MS = 6000</code>
<p>모든 자동/수동 위치 목표는 0..6000ms 범위 안에서만 허용됩니다.</p>
</div>
<div class="card">
<strong>자동 상승 위치</strong>
<code>storedSeatPositionMs = EEPROM</code>
<p>하드코딩 기본 위치는 사용하지 않고 EEPROM에서 읽습니다. EEPROM 초기화 시 6000ms로 설정됩니다.</p>
</div>
<div class="card">
<strong>하강 속도 추정</strong>
<code>11 / 10</code>
<p>일반 자동 하강과 수동 하강 위치 계산은 상승보다 1.1배 빠른 것으로 추정합니다.</p>
</div>
<div class="card">
<strong>수동 스텝</strong>
<code>MANUAL_STEP_MS = 500</code>
<p><code>u</code>는 +500ms, <code>d</code>는 -500ms 이동 목표를 설정합니다.</p>
</div>
<div class="card">
<strong>자동 보정</strong>
<code>UP 7000ms -> DOWN 6000ms</code>
<p>10회 사용 후 시동 OFF 상태 5분 경과 시 자동 보정합니다. <code>c</code> 명령은 즉시 보정합니다.</p>
</div>
<div class="card">
<strong>릴레이 보호</strong>
<code>RELAY_DEADTIME_MS = 120</code>
<p>상승/하강 반전 시 릴레이를 잠깐 OFF로 두어 접점 부담을 줄입니다.</p>
</div>
</div>
<h2>2. 입력 신호 판정</h2>
<table>
<thead>
<tr><th>항목</th><th>조건</th><th>결과</th></tr>
</thead>
<tbody>
<tr><td>P ON</td><td><code>parkingRawValue >= 950</code></td><td><code>parkingState = HIGH</code></td></tr>
<tr><td>P OFF</td><td><code>parkingRawValue <= 940</code></td><td><code>parkingState = LOW</code></td></tr>
<tr><td>주행 raw</td><td><code>IG1 HIGH && P LOW</code></td><td>시트 상승 목표 상태</td></tr>
<tr><td>주행 확정</td><td>주행 raw가 80ms 유지</td><td><code>confirmedDriveMode = HIGH</code></td></tr>
<tr><td>P/비주행 확정</td><td>비주행 raw가 250ms 유지</td><td><code>confirmedDriveMode = LOW</code></td></tr>
<tr><td>시동 직후 안정화</td><td>IG1 LOW -> HIGH 후 1000ms</td><td>릴레이 OFF, P 신호 튐 무시</td></tr>
</tbody>
</table>
<h2>3. 장시간 주차 후 전원 인가부터 시뮬레이션</h2>
<h3>3.1 잠금 해제 직후</h3>
<pre>IG1 = LOW
P = LOW
confirmedDriveMode = LOW
targetPosMs = 0
relay = OFF</pre>
<p>사용자 설명 기준으로 잠금 해제 직후에는 IG1 OFF, P OFF입니다. 코드는 주행 조건을 <code>IG1 HIGH && P LOW</code>로 보므로 상승하지 않습니다.</p>
<h3>3.2 브레이크 후 시동 ON 직후</h3>
<pre>IG1 LOW -> HIGH
isStabilizing = true
for 1000ms:
seat(0)
relay = OFF</pre>
<p>시동 직후 P가 약 1초 동안 ON/OFF/ON으로 흔들려도 안정화 구간에서 릴레이는 동작하지 않습니다.</p>
<h3>3.3 P단 유지</h3>
<pre>IG1 = HIGH
P = HIGH
confirmedDriveMode = LOW
targetPosMs = 0</pre>
<p>P단에서는 목표 위치가 0입니다. 현재 추정 위치가 0보다 높으면 하강합니다.</p>
<h3>3.4 P단 외 이동</h3>
<pre>P = LOW for 80ms
confirmedDriveMode = HIGH
targetPosMs = storedSeatPositionMs
relay = UP until currentPosMs reaches targetPosMs</pre>
<p>P단 외 상태가 80ms 유지되면 EEPROM에 저장된 위치까지 상승합니다.</p>
<h3>3.5 P단 복귀 또는 시동 OFF</h3>
<pre>confirmedDriveMode = LOW
targetPosMs = 0
currentPosMs -= deltaTime * 11 / 10
when currentPosMs <= 0:
currentPosMs = 0
DOWN relay extra 300ms
relay = OFF</pre>
<p>하강은 1.1배 속도로 위치를 추정하고, 바닥 도달 후 300ms 추가 하강으로 바닥 밀착감을 줍니다.</p>
<h2>4. 한 글자 시리얼 명령</h2>
<p>시리얼 연결 후 처음에는 잠금 상태입니다. 빈 Enter를 한 번 입력하면 도움말과 현재 상태를 출력하고, 그때부터 한 글자 명령을 사용할 수 있습니다. 잠금 해제 후 Enter 단독 입력은 아무 동작도 하지 않습니다. 안전을 위해 <code>p/u/d/a/c/r</code> 명령은 P단에서만 실행됩니다.</p>
<table>
<thead>
<tr><th>명령</th><th>동작</th><th>응답</th></tr>
</thead>
<tbody>
<tr><td><code>s</code></td><td>현재 상태 1회 출력</td><td>상태 ACK 출력</td></tr>
<tr><td><code>u</code></td><td>현재 추정 위치에서 +500ms 목표 설정</td><td>현재 위치/목표 출력</td></tr>
<tr><td><code>d</code></td><td>현재 추정 위치에서 -500ms 목표 설정</td><td>현재 위치/목표 출력</td></tr>
<tr><td><code>p</code></td><td>현재 추정 위치를 EEPROM 자동 상승 위치로 저장</td><td>저장된 위치 출력</td></tr>
<tr><td><code>g</code></td><td>1초마다 debug 출력 시작</td><td>debug 중에는 <code>x</code>만 작동</td></tr>
<tr><td><code>x</code></td><td>debug 모드 종료 전용</td><td>debug 종료 후 상태 출력</td></tr>
<tr><td><code>a</code></td><td>수동 모드 해제, 기어 기반 자동 목표로 복귀</td><td>상태 출력</td></tr>
<tr><td><code>c</code></td><td>즉시 보정: 상승 7초, 하강 6초, 위치 0 설정</td><td>상태 출력</td></tr>
<tr><td><code>r</code></td><td>10회 자동 보정 카운터 초기화</td><td>상태 출력</td></tr>
<tr><td><code>h</code></td><td>도움말 출력</td><td>도움말 + 현재 상태 출력</td></tr>
</tbody>
</table>
<p><span class="warn">주의:</span> 명령은 한 글자만 받습니다. <code>debug</code>, <code>down</code>, <code>position 2000</code>처럼 긴 문자열은 실행하지 않습니다.</p>
<h2>5. 수동 위치 세팅 시뮬레이션</h2>
<table>
<thead>
<tr><th>현재 추정 위치</th><th>입력</th><th>계산</th><th>결과</th></tr>
</thead>
<tbody>
<tr><td>2000</td><td><code>u</code></td><td>2000 + 500 = 2500</td><td>목표 2500으로 상승, 상태 출력</td></tr>
<tr><td>2500</td><td><code>u</code></td><td>2500 + 500 = 3000</td><td>목표 3000으로 상승, 상태 출력</td></tr>
<tr><td>3000</td><td><code>p</code></td><td>현재 위치 3000 저장</td><td>EEPROM 저장 위치가 3000으로 변경</td></tr>
<tr><td>3000</td><td>P단 복귀</td><td>목표 0</td><td>하강</td></tr>
<tr><td>0</td><td>P단 외 이동</td><td>목표 EEPROM 3000</td><td>3000까지 자동 상승</td></tr>
</tbody>
</table>
<h2>6. 범위 초과 시뮬레이션</h2>
<table>
<thead>
<tr><th>현재 추정 위치</th><th>입력</th><th>계산</th><th>결과</th></tr>
</thead>
<tbody>
<tr><td>5800</td><td><code>u</code></td><td>6300</td><td>0..6000 범위 초과, 경고 출력, 목표 변경 없음</td></tr>
<tr><td>200</td><td><code>d</code></td><td>-300</td><td>0..6000 범위 초과, 경고 출력, 목표 변경 없음</td></tr>
<tr><td>임의</td><td><code>abc</code></td><td>한 글자 아님</td><td>실행 거절, ACK 상태 출력</td></tr>
</tbody>
</table>
<h2>7. 10회 사용 후 자동 보정 시뮬레이션</h2>
<pre>each IG1 HIGH -> LOW:
useCount++
EEPROM.update(useCount)
if useCount >= 10 and IG1 LOW for 5 minutes:
UP relay ON for 7000ms
DOWN relay ON for 6000ms
relay OFF
currentPosMs = 0
targetPosMs = 0
useCount = 0</pre>
<p>자동 보정 중 시동이 켜지면 중단합니다. 단, <code>c</code>로 시작한 강제 보정은 IG1 ON 상태에서도 끝까지 진행합니다.</p>
<h2>8. 현재 주의 사항</h2>
<ul>
<li>시트 위치는 실제 센서가 아니라 시간 기반 추정값입니다.</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>
</ul>
<h2>9. 추가 예외처리 기준</h2>
<table>
<thead>
<tr><th>예외 상황</th><th>처리</th><th>목적</th></tr>
</thead>
<tbody>
<tr>
<td>시트 이동 중 키 입력</td>
<td><code>ERR: busy</code> 출력 후 명령 무시</td>
<td>연속 입력으로 500ms 단위가 흐트러지는 문제 방지</td>
</tr>
<tr>
<td>P단이 아닌 상태에서 <code>p/u/d/a/c/r</code> 입력</td>
<td><code>ERR: check gear state. Command requires P.</code> 출력</td>
<td>수동 이동, 저장, 보정, EEPROM 변경이 P단 외 상태에서 실행되는 문제 방지</td>
</tr>
<tr>
<td>수동 이동 완료 전 기어 상태 변경</td>
<td>수동 완료 대기 플래그 해제</td>
<td>자동 제어로 넘어간 뒤 잘못된 <code>MANUAL_STEP_DONE</code> 출력 방지</td>
</tr>
<tr>
<td>내부 위치값 범위 이탈</td>
<td><code>currentMs</code>, <code>targetMs</code>를 0..6000으로 보정</td>
<td>시간 계산 오차나 상태 꼬임으로 인한 비정상 목표 방지</td>
</tr>
<tr>
<td>보정 시작/중단</td>
<td>수동 이동 플래그, 하강 잔여값, 하단 추가 하강 상태 초기화</td>
<td>보정과 일반 이동 상태가 섞이는 문제 방지</td>
</tr>
</tbody>
</table>
</main>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
param(
[string]$Port = "COM3",
[string]$Fqbn = "arduino:avr:nano:cpu=atmega328"
)
$ArduinoCli = "arduino-cli"
$InstalledCli = "C:\Program Files\Arduino CLI\arduino-cli.exe"
if (Test-Path $InstalledCli) {
$ArduinoCli = $InstalledCli
}
& $ArduinoCli upload --fqbn $Fqbn --port $Port .