Compare commits

...

7 Commits

Author SHA1 Message Date
seo-amugae c1d29b4157 Expand serial status field names 2026-06-07 13:33:15 +09:00
seo-amugae bef37b792f Format all status output by field 2026-06-07 13:27:29 +09:00
seo-amugae 9ebe8656fb Format debug output by field 2026-06-07 13:25:13 +09:00
seo-amugae 4a68bd39c6 Show calibration wait timer in debug output 2026-06-07 13:15:29 +09:00
seo-amugae c6e80e0618 Add uptime to serial status output 2026-06-07 13:09:27 +09:00
seo-amugae 2f383fe791 Allow manual position commands while driving 2026-06-07 13:01:46 +09:00
seo-amugae 582311ed47 Handle USB reconnect while out of park 2026-06-07 12:14:43 +09:00
3 changed files with 306 additions and 86 deletions
+137 -42
View File
@@ -109,6 +109,12 @@ void setup() {
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();
}
@@ -430,10 +436,7 @@ void processSerialCommand(String command) {
}
bool requiresParkingForCommand(char name) {
return name == 'p' ||
name == 'u' ||
name == 'd' ||
name == 'a' ||
return name == 'a' ||
name == 'c' ||
name == 'r';
}
@@ -512,7 +515,7 @@ void updateDebugOutput(unsigned long currentTime) {
if (lastDebugPrintTime == 0 || currentTime - lastDebugPrintTime >= DEBUG_INTERVAL_MS) {
lastDebugPrintTime = currentTime;
printStatus(F("DEBUG"));
printDebugLine();
}
}
@@ -555,7 +558,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("| Action commands require gear P for safety. |"));
Serial.println(F("| a/c/r 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:"));
@@ -579,45 +582,137 @@ void printStatus(const __FlashStringHelper *tag) {
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);
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.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);
Serial.println(USES_BEFORE_AUTO_CALIBRATION);
printNamedBool(F("autoCalibrationPending"), autoCalibrationPending);
printNamedBool(F("forceCalibration"), forceCalibration);
printNamedInt(F("autoCalibrationState"), autoCalibrationState);
printAutoCalibrationDelayStatus();
}
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();
}
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 setRelay(int relayPin, boolean state) {
+154 -39
View File
@@ -1,55 +1,170 @@
# Auto_Seat
# Auto_Seat
## 개요
Arduino Nano based automatic seat controller.
차량 시트 자동 제어 프로젝트입니다. EEPROM 설정, 점화/주차 조건, 릴레이 출력, 자동 보정, 시리얼 명령 처리를 포함합니다.
## Hardware
분류: AutoSeat_UL, Auto_Seat_LL과 같은 자동 시트 제어 계열의 기준 프로젝트입니다.
- Board: Arduino Nano ATmega328P
- Up relay: D8, active-low
- Down relay: D9, active-low
- Parking raw input: A0
- IG1 input: D7
## 코드 구성 요약
## Main Behavior
- 분석한 파일 수: 3
- 사용 라이브러리/include: EEPROM.h
- 코드에서 확인되는 주요 모듈: EEPROM, Serial
- 코드에서 확인되는 핀/입출력 단서: ignitionPin:INPUT, parkingPin:INPUT, seatRelayD8:OUTPUT, seatRelayD9:OUTPUT
- If `IG1 HIGH` and parking signal is OFF, drive mode is confirmed after 80ms.
- In confirmed drive mode, the seat moves up to the EEPROM saved position.
- If parking/non-drive mode is confirmed for 250ms, the seat moves down to 0.
- Down movement is estimated at 1.1x the up movement speed.
- When the estimated position reaches 0, down relay stays on for an extra 300ms.
- On IG1 startup, relay output is held off for 1000ms to ignore temporary signal bounce.
## 파일별 설명
## Position Setting
### `Auto_Seat.ino`
- Hard limit: `0..6000ms`
- EEPROM saved position is used as the automatic driving position.
- EEPROM initializes saved position to `6000ms` if the magic value is missing or invalid.
- Serial command `p` saves the current estimated position to EEPROM.
- Calibration command `c` does not change the saved position.
- 역할: 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
## USB / Serial Reset Handling
### `report.html`
Opening a USB serial connection can reset an Arduino Nano through DTR.
- 역할: 프로젝트 보고서 또는 설명을 담은 HTML 문서입니다.
- include/의존성: -
- 주요 함수: -
- 주요 전역값/설정값: -
- 핀/입출력 설정: -
- 입출력/통신 호출: EEPROM.update
If the board boots while `IG1 HIGH` and parking is OFF, the controller assumes the seat is already at the saved driving position:
### `upload.ps1`
```text
currentPositionMs = savedPositionMs
targetPositionMs = savedPositionMs
relay = OFF
```
- 역할: 업로드/자동화에 사용되는 PowerShell 스크립트입니다.
- include/의존성: -
- 주요 함수: -
- 주요 전역값/설정값: -
- 핀/입출력 설정: -
- 입출력/통신 호출: -
This prevents unintended upward movement after USB reconnect while the gear is not in P. If the gear is then moved to P, the controller moves down from the saved position estimate to 0.
## 동작 흐름
## Serial Console
1. `setup()`에서 시리얼, 센서, 통신 모듈, LCD/릴레이/핀 모드 등 초기 설정을 수행합니다.
2. `loop()`에서 센서 측정, 입력 확인, 상태 계산, 출력 제어, 통신 전송 또는 화면 갱신을 반복합니다.
3. 보조 함수들은 측정값 변환, 값 변화 감지, 릴레이/모터 제어, 시간 표시, 네트워크 응답 같은 세부 동작을 나눠 담당합니다.
Serial is quiet by default.
## 빌드 및 사용 메모
1. Open serial at `115200`.
2. Send an empty Enter once to unlock the console and print help.
3. After unlock, only one-character commands are accepted.
- Arduino IDE 또는 PlatformIO에서 폴더명과 같은 대표 스케치를 열어 빌드합니다.
- 코드에 포함된 네트워크 주소, Wi-Fi 정보, DB 정보, 장치 핀 번호는 실제 하드웨어 구성에 맞춰 확인해야 합니다.
- 공개 저장소에 올릴 때는 비밀번호, 토큰, 차량별 민감 정보가 포함되지 않았는지 확인합니다.
Commands:
```text
s print current status
u manual up +500ms
d manual down -500ms
p save current estimated position to EEPROM
g start debug output every 1s
x stop debug mode only
a return to gear-based automatic target
c run calibration: up 7s, down 6s, set current position to 0
r clear 10-use auto calibration counter
h print help
```
These management commands require parking gear for safety:
```text
a c r
```
If the gear is not P:
```text
ERR: check gear state. Command requires P.
```
While debug mode is active, only `x` is accepted. Other input is ignored.
Manual position commands `u`, `d`, and `p` are allowed while driving, as long as the controller is not busy with an in-progress movement.
Every status output uses one item per line and includes boot uptime:
```text
[STATUS]
uptime=0h 2m 13s 457ms
parkingRawValue=1023
ignition=true
parking=true
rawDriveMode=false
confirmedDriveMode=false
```
If the use counter reached 10, status/debug output also includes the ignition-off calibration timer:
```text
autoCalibrationOffTimer=elapsed 0h 1m 20s 123ms remaining 0h 3m 39s 877ms
```
If IG1 is still ON:
```text
autoCalibrationOffTimer=waiting_for_ig_off
```
Debug mode prints a readable block every second, one item per line. Boolean values are printed as `true` or `false`:
```text
[DEBUG]
uptime=0h 0m 5s 123ms
parkingRawValue=1023
ignition=true
parking=true
rawDriveMode=false
confirmedDriveMode=false
currentPositionMs=0
targetPositionMs=0
savedPositionMs=6000
hardLimitPositionMs=6000
requestedRelayAction=0
actualRelayAction=0
manualMode=false
stabilizing=false
useCount=0/10
autoCalibrationPending=false
forceCalibration=false
autoCalibrationState=0
```
## Automatic Calibration
- Each `IG1 HIGH -> LOW` transition increments the EEPROM use counter.
- After 10 uses, calibration is scheduled.
- If IG1 remains OFF for 5 minutes, automatic calibration runs:
```text
up 7000ms
down 6000ms
currentPositionMs = 0
targetPositionMs = 0
useCount = 0
```
If IG1 turns ON during scheduled calibration, calibration is aborted. Manual `c` calibration is allowed only in P and runs to completion.
## Safety Guards
- Commands are rejected while the seat is moving or a relay action is pending.
- Internal position values are clamped to `0..6000ms`.
- Manual step completion is cancelled if gear state changes before completion.
- Factory reset command was removed to avoid accidental EEPROM reset.
## Build / Upload
Compile:
```powershell
& "C:\Program Files\Arduino CLI\arduino-cli.exe" compile --fqbn arduino:avr:nano:cpu=atmega328 --warnings all .
```
Upload:
```powershell
.\upload.ps1 -Port COM3
```
## Notes
The controller estimates seat position by time. There is no physical position sensor. If power is cut while IG1 is OFF/P state, the next boot starts from estimated position 0. If the board resets while already in drive mode, it starts from the EEPROM saved driving position to avoid USB reconnect induced upward movement.
+15 -5
View File
@@ -187,6 +187,13 @@ 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
@@ -198,7 +205,7 @@ when currentPosMs <= 0:
<p>하강은 1.1배 속도로 위치를 추정하고, 바닥 도달 후 300ms 추가 하강으로 바닥 밀착감을 줍니다.</p>
<h2>4. 한 글자 시리얼 명령</h2>
<p>시리얼 연결 후 처음에는 잠금 상태입니다. 빈 Enter를 한 번 입력하면 도움말과 현재 상태를 출력하고, 그때부터 한 글자 명령을 사용할 수 있습니다. 잠금 해제 후 Enter 단독 입력은 아무 동작도 하지 않습니다. 안전을 위해 <code>p/u/d/a/c/r</code> 명령은 P단에서만 실행됩니다.</p>
<p>시리얼 연결 후 처음에는 잠금 상태입니다. 빈 Enter를 한 번 입력하면 도움말과 현재 상태를 출력하고, 그때부터 한 글자 명령을 사용할 수 있습니다. 잠금 해제 후 Enter 단독 입력은 아무 동작도 하지 않습니다. 운행 중에도 <code>u/d/p</code> 수동 위치 조정 및 저장은 가능하며, <code>a/c/r</code> 관리 명령은 P단에서만 실행됩니다.</p>
<table>
<thead>
<tr><th>명령</th><th>동작</th><th>응답</th></tr>
@@ -217,6 +224,9 @@ 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>
@@ -261,7 +271,7 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
<h2>8. 현재 주의 사항</h2>
<ul>
<li>시트 위치는 실제 센서가 아니라 시간 기반 추정값입니다.</li>
<li>전원 차단 후 재부팅하면 <code>currentPosMs</code>는 0에서 시작합니다.</li>
<li>전원 차단 후 P단/IG1 OFF 상태로 재부팅하면 <code>currentPosMs</code>는 0에서 시작합니다. 단, IG1 ON + P단 외 상태에서 재부팅하면 저장 위치에서 시작한 것으로 간주합니다.</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>
@@ -279,9 +289,9 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
<td>연속 입력으로 500ms 단위가 흐트러지는 문제 방지</td>
</tr>
<tr>
<td>P단이 아닌 상태에서 <code>p/u/d/a/c/r</code> 입력</td>
<td>P단이 아닌 상태에서 <code>a/c/r</code> 입력</td>
<td><code>ERR: check gear state. Command requires P.</code> 출력</td>
<td>수동 이동, 저장, 보정, EEPROM 변경이 P단 외 상태에서 실행되는 문제 방지</td>
<td>자동모드 복귀, 보정, 카운터 초기화가 P단 외 상태에서 실행되는 문제 방지</td>
</tr>
<tr>
<td>수동 이동 완료 전 기어 상태 변경</td>
@@ -290,7 +300,7 @@ if useCount >= 10 and IG1 LOW for 5 minutes:
</tr>
<tr>
<td>내부 위치값 범위 이탈</td>
<td><code>currentMs</code>, <code>targetMs</code>를 0..6000으로 보정</td>
<td><code>currentPositionMs</code>, <code>targetPositionMs</code>를 0..6000으로 보정</td>
<td>시간 계산 오차나 상태 꼬임으로 인한 비정상 목표 방지</td>
</tr>
<tr>