전원만 넣으면 충전기가 뜬다 — 자동 시작, 그리고 하나씩 죽여 보며 찾은 다섯 가지 (M7-6)
지금까지는 SSH로 들어가 run-m7-bsp.sh를 실행해야 충전기가 동작했다. 제품은 그럴 수 없다. 전원만 넣으면 스스로 떠서 서버에 붙어야 한다. 그리고 사람이 없으니, 프로그램 하나가 죽거나 다시 시작해도 스스로 안전하게 수습해야 한다. 오늘(M7-6)은 이 두 가지를 했다.
| 일 | 결과 |
|---|---|
| systemd 서비스로 자동 시작 | ✅ 전원을 넣고 약 36초 뒤 서버에 Available |
| 하나씩 죽여 보기 7가지 | ✅ 모두 통과 |
| 그 과정에서 찾은 문제 | 안전 2개, 상태 불일치 3개 → sc6-bsp r0.6 → r0.11 |
1. 구조
1-1. 서비스 셋, 서로 묶지 않는다
mosquitto.service
├── sc6-bsp.service (새로) CP·K1·비상정지. 죽으면 1초 뒤 다시, 끝날 때마다 K1 OFF + CP F
├── sc6-meter.service (새로) 전력량계
└── everest.service (EVerest 패키지 것 + 덧붙이는 설정) sc6-bsp 다음에 시작
systemd에는 “이 서비스가 죽으면 저것도 같이 죽여라”(BindsTo=) 같은 묶음이 있다. 일부러 쓰지 않았다. EVerest가 다시 떠도 sc6-bsp는 그대로 둔다. 어제 넣은 heartbeat 감시 덕분에 그래도 안전하다. EVerest가 멈추면 sc6-bsp가 3초 안에 K1을 끈다. sc6-bsp는 EVerest 없이 혼자 떠 있어도 안전 상태다. 그래서 EVerest보다 먼저 띄운다.
1-2. 패키지 서비스에 덧붙이기
EVerest 패키지에는 everest.service가 이미 들어 있다. /etc/everest/config.yaml이 있어야만 시작하는 조건이 붙어 있다. 그 파일과, 서비스에 덧붙이는 설정 한 장을 새 레시피(sc6-everest-config)로 넣었다.
# /usr/lib/systemd/system/everest.service.d/10-sc6.conf
[Unit]
Wants=sc6-bsp.service sc6-meter.service network-online.target
After=sc6-bsp.service sc6-meter.service network-online.target
[Service]
ExecStartPre=/bin/rm -f /var/lib/everest/everest.db # manager 는 이 DB 가 있으면 YAML 을 무시한다
ExecStartPre=/bin/mkdir -p /tmp/everest-logs
UMask=0077
첫 줄의 DB 삭제는 M8-2에서 겪은 일 때문이다. manager는 설정 DB가 이미 있으면 YAML 대신 DB 안의 옛 설정으로 뜬다. 패키지 서비스는 이 DB를 지우지 않는다. 그대로 두면 YAML을 고쳐도 반영되지 않는다. 거래 DB는 다른 파일이라 지워지지 않는다.
1-3. UMask=0077 — 600으로 맞춰도 644로 돌아온다
아침 환경 확인에서 서버 비밀번호가 든 ocpp-user-config.json이 644로 바뀌어 있었다. 어제 복원할 때는 분명히 600으로 맞췄다. libocpp 소스에 이유가 있었다.
// charge_point_configuration.cpp, setInUserConfig()
std::ofstream ofs(tmp_file); // 새 파일 -> 기본 권한 644
ofs << user_config << std::endl;
fs::rename(tmp_file, user_config_path); // 원래 파일(600)을 바꿔치기
파일이 깨지지 않게 임시 파일에 쓰고 바꿔치는 좋은 방식이다. 그런데 그 결과 권한이 umask를 따라간다. 파일 권한을 손으로 맞추는 대신 프로세스가 만드는 파일이 처음부터 600이 되게 UMask=0077을 넣었다. 비밀번호 파일은 /var/lib/everest/(700)로 옮겼고, 이미지에는 넣지 않는다.
1-4. 네트워크는 최대 30초만 기다린다
EVerest를 네트워크보다 먼저 띄우면 첫 BootNotification이 시간 동기화(NTP) 전에 나갈 수 있다. 그렇다고 네트워크를 끝까지 기다리면, WiFi가 없을 때 충전기가 아예 뜨지 않는다. 그래서 최대 30초로 정했다.
보드에서 확인해 보니 함정이 하나 있었다. 랜선이 없는 end0도 “온라인 판단 대상”(Required For Online: yes, ST 기본 설정)이었다. 기본 옵션 그대로면 WiFi가 붙어도 end0까지 기다리다 매번 시간을 다 쓴다.
# systemd-networkd-wait-online.service.d/10-sc6.conf
[Service]
ExecStart=
ExecStart=/usr/lib/systemd/systemd-networkd-wait-online --any --timeout=30
--any는 둘 중 하나만 붙으면 통과라는 뜻이다. 시간이 다 되면 이 서비스는 failed로 남는다. 하지만 EVerest는 약한 의존(Wants=)이라 그대로 시작한다.
2. 먼저 확인: 프로그램이 죽으면 릴레이는?
서비스를 만들기 전에 하나를 보드에서 먼저 확인했다. 손으로 띄운 sc6-bsp를 충전 중에 kill -9로 죽였다.
K1 상태는 gpioget으로 읽으면 안 된다. 핀을 입력으로 바꿔 버려서 상태 자체가 바뀐다. 커널 디버그 정보에서 읽기만 했다.
grep '^pin 24 ' /sys/kernel/debug/pinctrl/*50002000*/pinconf-pins # PB8 = K1
결과는 걱정한 대로였다. 릴레이 LED가 켜진 채 남았다.
| 출력 | 제어 | 프로그램이 죽으면 |
|---|---|---|
| K1 | GPIO (libgpiod 요청) | 커널이 사용권을 풀어도 핀은 마지막 값(High) |
| CP | 하드웨어 PWM | 마지막 듀티 그대로 계속 나감 |
EVerest는 5초 뒤 통신 끊김을 알아채고 서버에 Faulted를 보냈다. 하지만 K1을 끌 수단이 없다. 하드웨어를 만지는 프로그램은 sc6-bsp 하나뿐이다. 서버에는 “고장”으로 보이는데 릴레이는 붙어 있는 상태다. AC를 건 상태였다면 차로 전기가 계속 갔을 것이다.
systemd의 ExecStopPost=는 서비스가 어떻게 끝났든(정상 종료든 kill -9든 오류든) 실행된다. 여기서 sc6-bsp를 한 번 더 부르게 했다. 처음에는 셸 스크립트로 핀을 끄려고 했다. 그러면 핀 번호가 설정 파일과 스크립트 두 군데로 갈라진다. 그래서 sc6-bsp에 -s 옵션을 넣었다. 같은 설정 파일을 읽고, 출력만 안전 상태로 만든 뒤 끝낸다.
ExecStart=/usr/bin/sc6-bsp
ExecStopPost=/usr/bin/sc6-bsp -s
Restart=always
3. 하나씩 죽여 보기
모든 시험은 AC 없이 CP 저항 지그로 했다. 항목마다 한 번 실행하면 끝나는 스크립트(test-autostart.sh)로 1초마다 K1과 CP를 기록했다.
| # | 시험 | 결과 |
|---|---|---|
| 1 | 재부팅만 | ✅ sc6-bsp 26.0초 → NTP 26.4초 → 네트워크 대기 27.4초 → EVerest 27.6초 → 서버 Available 35.9초 |
| 2 | 충전 중 EVerest 재시작 | ✅ 3.0초 뒤 K1 OFF. 거래는 PowerLoss로 종료 |
| 3 | 충전 중 kill -9 sc6-bsp |
✅ 31~56 ms 뒤 K1 OFF, 1.4초 뒤 sc6-bsp 다시 뜸 |
| 4 | 충전 중 계량기 15초 정지 | ✅ 4.6초 뒤 일시정지, 다시 붙으면 같은 거래로 충전 |
| 5 | 충전 중 mosquitto 재시작 | ✅ 셋 다 순서대로 함께 다시 시작, 그동안 K1 OFF |
| 6 | 네트워크 없이 부팅 | ✅ 30초 대기 뒤 EVerest 시작, WiFi가 돌아오자 약 40초 뒤 접속 |
| 7 | 네트워크 있는 부팅 | ✅ 대기 1.4초 |
표만 보면 깔끔하다. 하지만 2번과 3번을 통과하기까지 sc6-bsp를 다섯 번 고쳤다.
4. 고친 다섯 가지
4-1. EVerest 재시작 직후 승인 없이 K1이 1초 붙었다 (r0.7)
시험 2의 첫 결과다.
29.680 heartbeat 3003 ms 없음 -> 안전 상태: K1 OFF, CP F ← 설계대로
32.435 heartbeat 회복 -> 끊기기 전 출력 복원 (PWM 53%, allow=true)
32.495 K1 ON (CP C/D 안정) ← 새 EVerest 는 아무것도 허락하지 않았다
33.506 K1 OFF (새 EVerest 가 allow=false)
어제 만든 복원 기능은 EVerest가 잠깐 멈췄다 풀리는 경우에 맞춘 것이었다. EVerest는 PWM과 전원 허락을 바뀔 때만 보내기 때문에, 회복할 때 옛 명령을 되살려야 했다. 그런데 서비스 재시작은 새 EVerest다. 새 EVerest는 옛 명령을 모른다. 손으로 시험할 때는 EVerest를 늘 처음부터 띄웠으니 드러나지 않았다.
둘을 구분할 방법은 EVerest가 보내는 heartbeat 안에 있었다.
// ApiHelper.hpp
mqtt.publish(topic, serialize(hb_id++));
1씩 늘어나는 순번이다. 멈췄다 풀리면 이어지고(120 → 124), 새로 시작하면 0부터 다시 센다(120 → 0). 순번이 줄면 옛 명령을 버린다.
10:43:26.604 sc6-bsp: EVerest 새로 시작 (heartbeat 순번 13 -> 0) - 끊기기 전 출력은 복원하지 않음
4-2. sc6-bsp가 다시 뜨면 서버는 Charging, 실제는 CP F (r0.8 → r0.9)
kill -9 뒤 sc6-bsp는 1.4초 만에 다시 떴다. 그래서 EVerest의 5초 통신 감시가 걸리지 않았다. EVerest는 BSP가 재시작한 줄 모른다. 이미 보낸 명령을 다시 보내지 않는다. 새 sc6-bsp는 안전 상태로 시작해서 그대로 머문다. 안전하지만, 서버에는 거짓 Charging이 보인다.
첫 시도(r0.8)는 sc6-bsp가 스스로 통신 오류를 2초 올렸다 내리는 것이었다. EVerest가 고장 처리 중에 명령을 새로 보내 주기를 기대했다. 결과는 더 나빴다.
EVSE IEC Stop in Charging: [fatal error]
EVSE IEC Charger state: Charging->StoppingCharging
... (오류를 내린 뒤에도)
StoppingCharging: EV did not stop within timeout ← 0.1초마다, 몇 분 동안
EvseManager가 “충전 멈추는 중”에 들어가서 나오지 않았다. 0.1초마다 allow_power_on=false를 다시 보냈다. 지그를 B로, A로 움직여도, 즉 차를 뽑아도 거래가 끝나지 않았다.
두 번째 시도(r0.9)는 오류 대신 “차가 분리됐다(CP A)”를 한 번 보고하는 것이었다. sc6-bsp는 정보를 잃었으니 세션을 처음부터 다시 하자는 뜻이다. 그런데 이것도 같은 상태로 들어가 맴돌았다.
4-3. 진짜 원인은 “릴레이가 떨어졌다”는 확인 (r0.10)
실마리는 다음 시험에서 나왔다. 계량기를 15초 멈추자 EVerest는 똑같이 “충전 멈추는 중”에 들어갔다. 그런데 17 ms 만에 빠져나왔다.
11:17:00.250 Stop in Charging: [fatal error]
Charging -> StoppingCharging
11:17:00.267 StoppingCharging -> EVSE Paused ← 17 ms
차이는 그 순간 K1을 끈 sc6-bsp가 살아 있었다는 것이다. K1에는 보조 접점(피드백)이 없다. 그래서 sc6-bsp는 K1을 끄라는 명령을 받으면 바로 “PowerOff” 확인 이벤트를 보낸다. 그런데 이 확인을 K1이 켜져 있다가 꺼질 때만 보내고 있었다.
/* relay_apply(): 상태가 바뀔 때만 */
if (want == relay_on)
return;
...
pub_event(want ? "PowerOn" : "PowerOff");
다시 시작한 sc6-bsp는 K1이 꺼진 채로 시작한다. EVerest가 “꺼라”고 해도 이미 꺼져 있으니 아무것도 보내지 않는다. EvseManager는 “릴레이가 떨어졌다”는 확인을 기다리며 0.1초마다 다시 묻고 있었던 것이다. 화면을 가득 채운 allow_power_on = false가 바로 그 질문이었다.
if (!allow_power_on && !relay_on)
pub_event("PowerOff"); /* 이미 꺼져 있어도 확인은 보낸다 */
r0.8에서 오류를 올렸을 때 맴돈 것도 같은 원인이었다. 결과는 이렇다.
11:19:42.476 Charging -> StoppingCharging
11:19:42.489 StoppingCharging -> ... ← 13 ms
서버: StopTransaction (EVDisconnected) -> 새 세션 -> Authorize -> StartTransaction
4-4. 새 거래가 승인돼도 K1이 안 붙었다 (r0.11)
거래는 새로 시작됐는데 K1이 붙지 않았다. PWM은 53%로 나가고 있었다. sc6-bsp가 K1을 켜는 조건에는 enabled(커넥터 사용 가능)가 있다. 이 값은 false로 시작하고 EVerest의 enable 명령으로 켜진다. 그런데 EVerest는 enable을 자기가 시작할 때만 보낸다. sc6-bsp만 다시 시작하면 영영 오지 않는다.
EVerest가 이미 돌고 있었다면(첫 heartbeat 순번 3 이상) 사용 가능으로 보게 했다. 그래도 K1은 여전히 EVerest의 전원 허락, CP C, 비상정지·누전 없음이 모두 맞아야 켜진다.
11:26:40 거래 33 종료 (EVDisconnected)
11:26:49 새 세션 -> Authorize -> 거래 34
11:26:51 K1 ON (allow_power_on)
4-5. 지그는 규격을 지키지 않는 차다
시험 3의 새 세션은 지그를 B → C로 한 번 움직여야 시작됐다. 고정 저항 지그는 PWM이 꺼져도 C에 머문다. 규격을 지키는 차라면 PWM이 꺼지면 충전을 멈추고 B로 간다. EvseManager는 그 B → C 전환을 보고 전원을 허락한다. 지그로 시험할 때는 “차라면 이렇게 반응한다”를 손으로 해 줘야 하는 경우가 있다는 것을 기억해 두자.
5. 새 SD 복원도 바꿨다
어제는 새 카드로 부팅한 뒤 시리얼 콘솔에서 WiFi 비밀번호를 손으로 넣었다. 그러면 자동 시작을 첫 부팅부터 시험할 수 없다. 그래서 빌드 PC에서 카드에 바로 넣는 스크립트(sc6-restore-sd.sh)를 만들었다.
bash ~/sc6-tfa-sd.sh raw sdc # 이미지 쓰기
sudo bash ~/sc6-restore-sd.sh sdc # rootfs 에 백업 풀기 (WiFi, CA, 서버 비밀번호 -> /var/lib/everest)
USB 장치이고, rootfs 라벨 파티션이 있고, M7-6 이미지일 때만 쓴다. 비밀 파일 내용은 화면에 출력하지 않는다. 옛 거래 DB는 옮기지 않는다. 어제처럼 이미 닫힌 거래를 서버에 다시 보내지 않게 하려는 것이다.
이 덕에 오늘 아침 새 카드는 꽂자마자 WiFi와 서버에 붙었다. 다만 반대 방향의 부작용이 하나 있었다. kill -9 시험 때 열린 거래가 서버에만 남았다. 같은 시험 카드로 새 거래를 시작하자 서버가 “이미 충전 중”(ConcurrentTx)이라며 거절했다. 보드는 그 거래를 모르니 닫을 수도 없다. SteVe 화면에서 손으로 닫았다.
6. 정리
커밋
meta-sc6 84aaec8 M7-6: 부팅 자동 시작 (sc6-bsp/sc6-meter/everest 서비스), sc6-bsp r0.11
sc6-work cb4895f M7-6: 자동 시작 시험 스크립트, SD 복원 스크립트, 설계 문서 결과
오늘 배운 것
- 프로그램이 죽어도 하드웨어는 그대로다. GPIO도 하드웨어 PWM도 마지막 값을 유지한다. 출력을 끄는 일은 “정상 종료 처리”가 아니라 프로세스 바깥(
ExecStopPost)에 둬야 한다. - “멈췄다 풀림”과 “새로 시작”은 다르다. 복원은 앞의 경우에만 맞다. 상대가 보내는 순번 하나로 구분할 수 있었다.
- 확인 이벤트는 상태가 바뀌지 않아도 보내자. 상대는 “바뀌었다”가 아니라 “지금 꺼져 있다”는 답을 기다릴 수 있다. 두 번의 엉뚱한 시도(r0.8, r0.9) 끝에, 다른 시험(계량기)에서 정상으로 빠져나오는 경우와 나란히 놓고 보니 보였다.
- 고치다 막히면 멈추고 다시 보자. 오늘 중간에 “지금 근본적으로 하는 이유가 뭐지?”라는 질문을 받았다. 안전 문제(1, 2)는 이미 해결됐고 남은 것은 상태 맞추기였다. 그 질문 덕에 한 번만 더 해 보기로 범위를 정했고, 바로 다음 시험에서 실마리가 나왔다.
- “하나씩 죽여 보기”는 꼭 하자. 손으로 실행할 때는 한 번도 드러나지 않은 문제 다섯 개가 나왔다.
남은 것
| 항목 | 내용 |
|---|---|
| 제품값 | EM_ACTIVE 등은 설계자 확인(비상정지 해제 방식, 버튼 극성) 뒤 |
| sc6-bsp 재시작 뒤 | 진행 중 거래는 끝나고 다시 인증해야 함. 이어서 충전하지 않는 것은 의도 (죽어 있던 동안의 명령을 놓쳤을 수 있음) |
| 재시작 직후 카드 승인 | EVerest가 서버보다 먼저 카드를 확인해 오프라인으로 거절됨. 오프라인 충전 정책이 정해지면 다시 볼 것 |
| 모니터 | DWIN 화면·음성을 충전 상태에 맞춰 바꾸는 sc6-hmi 설계 |
댓글남기기