SC6 코드 읽기 2 — sc6-meter, EvseManager 상태 머신, OCPP까지
1편에서 프로그램이 뜨는 순서, EVerest 구성 YAML, sc6-bsp.c를 봤다. 2편은 그 뒤를 잇는다.
| 단계 | 내용 | 볼 파일 |
|---|---|---|
| 5 | sc6-meter: Modbus로 읽고 EVerest로 보내기 | sc6-meter.c (621행) |
| 6 | EVerest 안쪽: 충전 상태 머신 | IECStateMachine.cpp, Charger.cpp |
| 7 | OCPP: 사건이 서버 메시지가 되기까지 | OCPP.cpp, libocpp charge_point_impl.cpp |
EVerest 쪽 소스는 빌드 PC ~/stm32mp/EVerest/에 있다. 읽기 편하게 필요한 파일만 WSL ~/work/ref-everest/로 복사해 두었다.
5. sc6-meter
meta-sc6/recipes-evse/sc6-meter/files/sc6-meter.c. 뼈대는 sc6-bsp와 같다. 설정 읽기, MQTT 콜백 세 개, lock이 모두 같은 모양이다. 다른 점은 하는 일이 단순하다는 것이다. 1초마다 계량기를 읽어 EVerest로 보내기만 한다. 켜고 끄는 하드웨어가 없으니 안전 상태도 없다.
시리얼 열기
UART5는 리눅스에서 /dev/ttySTM3이다. termios로 4800 8N1을 맞추는데, 핵심은 cfmakeraw()다. 리눅스 시리얼은 원래 사람이 치는 터미널용이라 기본값으로는 특정 바이트를 바꾸거나 삼킨다. Modbus 같은 이진 데이터에는 원시 모드가 필수다.
포트는 한 번 열고 계속 쓴다. 11/06 시험에서 명령마다 열고 닫았더니 두 번째에 셸이 멈췄고, 열어 둔 채로는 5번 모두 정상이었다.
Modbus-RTU 한 번 (mb_read, 261행)
보냄 (8바이트) 01 03 00 48 00 0A [CRC]
주소 읽기 시작주소 개수10
받음 (25바이트) 01 03 14 [레지스터 10개 = 20바이트] [CRC]
tcflush로 지난번에 늦게 온 바이트를 버린다. 안 버리면 응답이 한 칸씩 밀린다.- 요청을 쓰고,
poll+read를 반복해 25바이트가 다 오거나 500 ms가 지날 때까지 모은다. - 주소, 기능 코드, 바이트 수, CRC를 차례로 검사한다.
실패는 종류별 음수로 돌려준다. -2(응답 없음)면 배선·전원·속도·주소를, -3(CRC)이면 노이즈를 의심한다.
4800 bps면 1바이트에 약 2 ms다. 요청 17 ms + 응답 52 ms라서 한 번 읽는 데 70 ms 남짓 걸린다. 1초 주기에 여유가 충분하다.
해석 (decode, 341행)
| 레지스터 | 계산 | 예 |
|---|---|---|
| 0x0048 전압 | ÷ 100 | 22050 → 220.50 V |
| 0x0049 전류 | ÷ 1000 | 31500 → 31.500 A |
| 0x004A 유효전력 | ÷ 10 | 최대 6553.5 W (16비트) |
| 0x004B-4C 전력량 | (상위«16 | 하위) ÷ 10 | Wh |
| 0x004D 역률 | ÷ 1000 | |
| 0x0051 주파수 | ÷ 100 |
유효전력 레지스터는 7 kW 충전에서 넘친다. 그래서 전압 × 전류 × 역률이 6,000 W를 넘으면 계산값을 쓴다. 과금에 쓰는 누적 전력량은 32비트라 문제없다.
EVerest로
{"timestamp":"…Z","meter_id":"JSY-MK-163T",
"energy_Wh_import":{"total":1234.5,"L1":1234.5},
"power_W":{…},"voltage_V":{"L1":…},"current_A":{"L1":…},"frequency_Hz":{"L1":…}}
timestamp와 energy_Wh_import가 필수다. 시각은 UTC다. NTP가 틀리면 서버에 가는 MeterValues 시각이 틀린다.
EVerest가 보내는 명령은 거의 없다. start/stop_transaction(독일 계량법용 서명 거래, OCMF)에는 {"status":"NOT_SUPPORTED"}로 바로 답한다. 답하지 않으면 EVerest가 최대 550초를 기다린 뒤 충전을 막는다.
고장은 “신호를 멈춰서” 알린다
한 주기에 실패하면 2번 더 시도
3주기 연속 실패 → communication_check 중단
powermeter_API: 5초간 없음 → CommunicationFault
EvseManager: 계량기 오류 = 과금 불가 → 충전 중지
다시 읽히면 → communication_check 재개 → 오류 해제
sc6-meter가 “고장”이라고 말하지 않고 살아 있다는 신호를 멈춘다. 그래서 프로그램 자체가 죽어도 같은 길로 감지된다. 대략 10초 안팎에 충전이 멈춘다.
주기는 “지금 + 1초”가 아니라 “예정 시각 + 1초”로 잡는다. 그래야 읽는 시간만큼 매번 밀리지 않는다.
| sc6-bsp | sc6-meter | |
|---|---|---|
| 하드웨어 | GPIO, PWM, ADC | 시리얼 Modbus |
| 주기 | 약 10 ms | 1초 |
| 끊기거나 멈추면 | 스스로 K1 OFF, CP F | 보고만 멈춤 → EVerest가 판단 |
6. EvseManager: 상태 머신 두 층
EvseManager는 우리 코드보다 훨씬 크다(Charger.cpp 3002행, EvseManager.cpp 3084행). 그래도 핵심은 두 층이다.
sc6-bsp ─ "C" ─► [IECStateMachine] CP A~F 변화 → 의미 있는 사건
│ (B→C = CarRequestedPower)
▼
[Charger] 세션: 대기 → 인증 → 준비 → 충전 → 정지 → 끝
│
PWM 듀티 / allow_power_on / 거래 시작·끝 → OCPP
아래층: CP 변화의 의미 (IECStateMachine::state_machine, 159행)
| 이전 → 지금 | 사건 | 함께 하는 일 |
|---|---|---|
| A → B | CarPluggedIn |
|
| B → C | CarRequestedPower |
|
| C → B | CarRequestedStopPower |
즉시 K1 OFF (규격: 100 ms 안에) |
| → A | CarUnplugged |
X1, K1 OFF |
| D | 환기 설비 없으면 K1 OFF | |
| E, F | BCDtoEF |
K1 OFF |
우리 구성은 has_ventilation: false라 D에서는 EVerest가 막는다. sc6-bsp의 relay_apply()는 C/D 둘 다 허용하지만 결과적으로 D에서는 충전하지 않는다.
K1까지 관문 세 개
① Charger "세션상 충전해도 된다" Charging 진입 → allow_power_on(true) Charger.cpp 1005
② IEC "CP도 맞다" C 상태 + PWM 켜짐일 때만 BSP로 전달 IECStateMachine.cpp 306
③ sc6-bsp "하드웨어도 맞다" relay_apply() 다섯 조건 sc6-bsp.c 452
// IECStateMachine::allow_power_on(), 466행
power_on_allowed = value; // 켜기 허락은 기억만
if (not power_on_allowed)
call_allow_power_on_bsp(false); // 끄기는 즉시 BSP로
켜기는 세 번 확인하고, 끄기는 어느 층에서든 즉시. sc6-bsp의 원칙이 EVerest 전체에 걸쳐 있다.
위층: 세션 상태 (Charger::run_state_machine, 163행)
Disabled ─enable─► Idle X1, 차 기다림
│ CarPluggedIn
▼
WaitingForAuthentication K1 금지, "AuthRequired"
│ 카드 승인 → start_transaction()
▼
PrepareCharging PWM (32A → 53.3%)
│ CarRequestedPower
▼
Charging allow_power_on(true)
│ 차 멈춤, 원격 정지, 비상정지, 오류 …
▼
StoppingCharging X1, K1 OFF 기다림
│ │
ChargingPausedEV/EVSE Finished stop_transaction()
(다시 충전 가능) │ CarUnplugged
▼
Idle
11/05 비상정지 시험에서 “해제 후 같은 거래로 이어진” 것이 ChargingPausedEVSE 경로다. 설계자에게 물어 둔 비상정지 해제 방식이 이 동작을 바꿀지의 문제다.
전류 → 듀티 (ampere_to_duty_cycle, 1604행)
if (ampere < 5.9) duty = 1.0; // 충전 불가 → X1
else if (ampere <= 6.1) duty = 0.1; // 6A = 10%
else if (ampere < 52.5) duty = ampere / 0.6 / 100; // 32A → 53.3%
else if (ampere <= 80) duty = (ampere / 2.5 + 64) / 100;
IEC 61851 표 그대로다. 충전 중 전류 변경은 5초에 한 번까지만 반영한다.
거래와 전력량계 (start_transaction, 1754행)
response = meter->call_start_transaction(req); // → sc6-meter
if (UNEXPECTED_ERROR) → 충전 거부
else if (OK) → 서명값 저장
// NOT_SUPPORTED는 둘 다 아님 → 그냥 진행
signal_transaction_started_event(); // → OCPP
sc6-meter의 NOT_SUPPORTED가 여기서 “문제없음”으로 처리된다.
안전망
- C1 6초 타이머: PWM을 껐는데 차가 6초 넘게 C에 머물면 부하 중이라도 강제로 끈다. 주석에 실제 차종 이름이 적혀 있다.
- 깨우기: PWM을 켰는데 차가 C로 안 오면 CP를 잠깐 F로 내렸다 올려 깨운다. 그래도 안 오면
ChargingPausedEV.
구성의 session_logging: true 덕분에 보드 /tmp/everest-logs/에 Charger state: Idle->WaitingForAuthentication, Event CarRequestedPower 같은 줄이 세션마다 남는다.
7. OCPP: 사건이 서버 메시지가 되기까지
EvseManager ─► OCPP 모듈 (OCPP.cpp) "통역"
│ on_xxx() ▲ register_xxx_callback()
▼ │
libocpp ChargePointImpl "OCPP 1.6 규칙"
│ JSON ▲
▼ │
wss (TLS + 비밀번호) ═══ SteVe
충전기 → 서버
OCPP::process_session_event()(187행)가 번역한다.
| EvseManager 사건 | libocpp | 서버로 |
|---|---|---|
| SessionStarted | on_session_started |
StatusNotification Preparing |
| TransactionStarted | on_transaction_started |
StartTransaction (meterStart) |
| ChargingStarted | on_resume_charging |
StatusNotification Charging |
| ChargingPausedEV / EVSE | on_suspend_charging_* |
SuspendedEV / SuspendedEVSE |
| TransactionFinished | on_transaction_stopped |
StopTransaction (meterStop, 사유) |
| SessionFinished | on_session_stopped |
StatusNotification Available |
| 오류 | on_error |
StatusNotification Faulted |
Charger 상태와 OCPP 커넥터 상태가 거의 1:1이다. 허용된 상태 변화는 charge_point_state_machine.cpp의 표(FSM_DEF)로 정해져 있다.
거래
// ChargePointImpl::start_transaction(), 4392행
req.meterStart = lround(energy_Wh); // sc6-meter 값
database_handler->insert_transaction(...); // 보드 DB에 먼저 기록
message_dispatcher->dispatch_call(call); // 그다음 전송
- DB(
/home/root/m7/ocpp16-db)에 먼저 적고 보낸다. 전원이 나가도 거래가 남는다. - 거래 번호는 서버가 정한다. 응답 전까지는 음수 난수를 임시로 쓴다. 서버가 끊겨 있어도 거래를 시작할 수 있게 하려는 장치다.
MeterValues는 1초마다 가지 않는다
on_meter_values()(4365행)는 최신값을 저장만 한다. 전송은 타이머가 정한다.
| 종류 | 언제 | 우리 설정 |
|---|---|---|
| 주기 샘플 | 거래 중, 일정 간격 | MeterValueSampleInterval 60초 |
| 정시 샘플 | 거래와 무관, 자정 기준 정각 | ClockAlignedDataInterval 900초 (매 15분) |
| 거래 기록 | StopTransaction에 묶어서 | StopTxnSampledData |
보내는 항목은 누적 전력량 하나로 해 두었다. MeterValuesSampledData에 전압·전류·전력을 더하면 sc6-meter가 이미 보내는 값이 같이 간다.
카드 승인
차 꽂음 → DummyTokenProvider "SC6TEST01" → Auth → OCPP → Authorize.req → SteVe → Accepted
LocalAuthorizeOffline: false라 서버가 끊기면 충전하지 않는다. 오프라인 충전 허용 여부는 운영 정책으로 정할 일이다.
서버 → 충전기
| 서버 명령 | 콜백 | 결과 |
|---|---|---|
| RemoteStartTransaction | register_provide_token_callback |
서버가 준 카드로 승인 요청 |
| RemoteStopTransaction | register_stop_transaction_callback |
EvseManager 정지 |
| ChangeAvailability | register_disable/enable_evse_callback |
사용 중지·재개 |
| SetChargingProfile | register_signal_set_charging_profiles_callback |
전류 제한 → PWM |
연결과 오프라인
wss 연결 → BootNotification → Accepted → StatusNotification → Heartbeat (600초)
- 11/06 패치는 이 순서의 맨 앞을 고쳤다. SteVe가 연결 2 ms 뒤
GetConfiguration을 보내는데, libocpp는 50 ms 뒤에야 “연결됨”이 된다. 그 틈에 온 요청이 메시지 큐를 막아 BootNotification이 나가지 않았다. - 거래 관련 메시지는 끊겨 있는 동안 DB 큐에 쌓였다가 다시 연결되면 순서대로 간다. 오늘 아침 재부팅 뒤 밀린 정시 MeterValues 4개가 한꺼번에 간 것이 이 동작이다.
- 재연결 간격은 9 → 21 → 48 → 97초로 늘어난다.
설정은 두 파일이다. ocpp-config-sc6.json(기본, git 관리) 위에 ocpp-user-config.json(보드에만, 비밀번호 포함)이 덮어쓴다. 서버가 바꾼 설정도 user config에 저장된다.
전체를 한 줄로
지그(CP 저항) → sc6-bsp: ADC → A~E → m2e/event
→ IECStateMachine: B→C = CarRequestedPower
→ Charger: 승인 → 거래 시작 → PWM 53.3% → Charging → allow_power_on
→ sc6-bsp: relay_apply 5조건 → K1 ON → PowerOn
계량기 → sc6-meter: Modbus 1초 → powermeter_values
→ OCPP: StartTransaction, MeterValues 60초/15분, StopTransaction
→ wss → SteVe
정리
- sc6-meter는 고장을 말하지 않고 신호를 멈춰서 알린다. 프로그램이 죽어도 같은 길로 감지된다.
- EvseManager는 두 층이다. 아래층은 CP 변화의 의미, 위층은 세션.
- K1은 Charger → IEC → sc6-bsp 세 관문을 다 지나야 켜지고, 끄기는 어느 층에서든 즉시다.
- OCPP는 사건을 번역할 뿐이고, 거래는 DB에 먼저 적고 보낸다. MeterValues는 최신값을 타이머로 뽑아 보낸다.
오늘 코드 읽기로 충전 한 사이클이 지그에서 서버 화면까지 어떤 코드를 지나가는지 한 번 이어 봤다. 다음은 이 이해를 바탕으로 중간 이미지(sc6-bsp, sc6-meter를 이미지에 넣기)와 운영 정책 정리로 돌아간다.
참고
meta-sc6/recipes-evse/sc6-meter/files/sc6-meter.c- EVerest
modules/EVSE/EvseManager/IECStateMachine.cpp,Charger.cpp - EVerest
modules/EVSE/OCPP/OCPP.cpp - EVerest
lib/everest/ocpp/lib/ocpp/v16/charge_point_impl.cpp,charge_point_state_machine.cpp sc6-work/board/m7/ocpp-config-sc6.json- 이전 글: M9-1 전력량계, M8-5 libocpp 패치, OCPP 보안 프로파일 2
댓글남기기