6 분 소요

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]
  1. tcflush로 지난번에 늦게 온 바이트를 버린다. 안 버리면 응답이 한 칸씩 밀린다.
  2. 요청을 쓰고, poll + read를 반복해 25바이트가 다 오거나 500 ms가 지날 때까지 모은다.
  3. 주소, 기능 코드, 바이트 수, 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

댓글남기기