7 분 소요


title: “OCPP 1.6J 붙이기 — SteVe 시험 서버, 원격 시작·정지, 비상정지 보고 (M8-1~M8-4)” date: 2026-11-05 categories: [embedded, ev-charger] tags: [stm32mp157, everest, ocpp, ocpp16j, steve, docker, libocpp, evse] excerpt: “빌드 PC에 docker로 OCPP 서버(SteVe)를 띄우고, 보드의 EVerest를 OCPP 1.6J로 붙였다. 카드 인증부터 거래 시작·종료, 서버에서 원격 시작·정지, 비상정지를 Faulted로 보고하는 것까지 확인했다. 중간에 EVerest가 YAML을 무시하고 DB의 옛 설정으로 부팅하는 함정이 있었다.” —

M7에서 실제 EVerest와 BSP로 충전 한 사이클을 돌렸고, 비상정지 입력까지 넣었다. 이제 충전기가 혼자 도는 단계는 끝났다. 실제 충전기는 충전 관제 서버(CSMS) 에 붙어서 카드를 확인받고, 거래를 기록하고, 원격 명령을 받는다. 그 통신 규격이 OCPP다.

정한 것은 세 가지다.

항목 선택 이유
규격 OCPP 1.6J (JSON over WebSocket) 국내 충전 사업자 서버가 아직 대부분 1.6J
시험 서버 SteVe, 빌드 PC에 docker로 오픈소스 OCPP 1.6 서버, 웹 화면으로 명령을 보낼 수 있음
인증 고정 카드 번호 SC6TEST01 보드에 카드 리더가 아직 없음. 차를 꽂으면 이 번호를 제시

결과부터 정리하면 이렇다.

단계 내용 결과
M8-1 SteVe 서버 설치 ✅ 빌드 PC :8180
M8-2 접속, 부팅 알림 ✅ BootNotification Accepted
M8-3 충전 거래 ✅ 카드 인증 → 시작 → 충전 → 종료
M8-4 서버 → 보드 명령 ✅ 설정 조회, 원격 시작·정지
M8-4 비상정지 보고 ✅ Faulted + 원인 코드. 단, 거래는 끝나지 않음

1. 시험 서버 SteVe

SteVe는 Java 서버와 MariaDB로 돌아간다. 저장소의 docker-compose.yml을 그대로 쓰면 DB 포트(3306)가 랜에 열린다. 사내 랜이라도 쓸 이유가 없으니, 사본(docker-compose.sc6.yml)을 만들어 DB 포트를 빼고 서버 포트 8180만 열었다.

services:
  db:
    image: mariadb:10.11.16
    # ports: 없음 → 컨테이너 내부 네트워크에서만 접근
  app:
    build: .
    ports:
      - "8180:8180"      # 웹 화면 + OCPP 웹소켓
docker compose -p sc6-steve -f docker-compose.sc6.yml up -d --build

첫 기동은 약 6분 걸렸다. 컨테이너 안에서 Maven 빌드를 하기 때문이다.

주소 용도
http://192.168.45.139:8180/steve/manager 웹 화면
ws://192.168.45.139:8180/steve/websocket/CentralSystemService/SC6-0001 보드가 접속할 OCPP 주소 (끝이 충전기 ID)

SteVe는 기본 설정상 모르는 충전기를 받아 주지 않는다(auto.register.unknown.stations=false). 웹 화면에서 충전기 SC6-0001과 카드 SC6TEST01을 먼저 등록했다.


2. 보드 쪽 EVerest 구성

M7-4의 구성(config-sc6-bsp.yaml)은 인증을 생략했다. 이번에는 인증을 켜고 OCPP를 붙였다.

EvseManager ── bsp_1 ══MQTT══ sc6-bsp (CP / K1 / 비상정지)
   ├── Auth ── DummyTokenProvider (차를 꽂으면 SC6TEST01 제시)
   │      └── OCPP (카드 확인 = 서버에 Authorize)
   └── OCPP 1.6J ══ws══ SteVe
          ├── System, EvseSecurity, PersistentStore

OCPP 모듈은 혼자 뜨지 않는다. System(리셋·펌웨어 명령), EvseSecurity(인증서), PersistentStore가 연결돼 있어야 한다. 카드 인증은 Auth 모듈이 맡는다. 토큰을 주는 쪽(DummyTokenProvider, OCPP 원격 시작)과 확인하는 쪽(OCPP Authorize)을 연결하는 구조다.

  auth:
    module: Auth
    config_module:
      selection_algorithm: PlugEventsLIFO
    connections:
      token_provider:
        - module_id: token_provider     # 고정 카드
        - module_id: ocpp               # RemoteStartTransaction
      token_validator:
        - module_id: ocpp               # 서버에 Authorize

  token_provider:
    module: DummyTokenProvider
    config_implementation:
      main:
        token: SC6TEST01
        type: RFID

OCPP 자체 설정은 JSON 파일(ocpp-config-sc6.json)에 있다. 중요한 값만 추리면 이렇다.

키 값 의미
ChargePointId SC6-0001 서버에 등록한 ID
CentralSystemURI 192.168.45.139:8180/steve/websocket/CentralSystemService/ 서버 주소
SecurityProfile 0 평문 ws. 사내 시험용. 제품은 TLS(2 이상)
AuthorizeRemoteTxRequests false 원격 시작은 다시 Authorize 하지 않음
ConnectionTimeOut 60 인증 후 60초 안에 꽂아야 함
WebSocketPingInterval 10 10초마다 연결 확인

3. 함정: EVerest가 YAML을 무시했다

처음 띄웠을 때 OCPP 모듈이 아예 뜨지 않았다. 로그 첫 부분을 보니 설정을 YAML이 아니라 데이터베이스에서 읽어 부팅했다는 줄이 있었다.

EVerest manager는 --db 옵션으로 지정한 파일이 이미 있으면, YAML 대신 그 DB에 저장된 설정으로 부팅한다. M7-4에서 쓰던 DB(everest-bsp.db)가 남아 있었으니, 새 YAML을 줘도 M7-4 구성 그대로 떴던 것이다.

돌아보면 M7-4 두 번째 실행부터 이미 그랬다. 고친 connector_type 경고가 계속 나왔던 이유가 이것이었다.

실행 스크립트를 고쳐 설정 파일마다 DB를 따로 두고, 시작할 때마다 지우게 했다.

CONF=${1:-config-sc6-bsp.yaml}
DB=/home/root/m7/everest-$(basename "$CONF" .yaml).db
rm -f "$DB"
manager --config /home/root/m7/$CONF \
	--settings /etc/everest/settings-mqtt-uds.yaml --db "$DB"

지우는 것은 설정 DB뿐이다. OCPP 거래 DB(ocpp16-db)와 PersistentStore는 다른 파일이라 남는다. 끊겼을 때 보내지 못한 거래 기록이 거기 들어가기 때문에 지우면 안 된다.


4. 메시지를 보는 도구

SteVe는 주고받은 메시지를 로그에 남기지만, 한 줄이 300자가 넘고 시각이 UTC다. 그래서 빌드 PC에 보기 도구를 만들었다(~/ocpp/tools).

명령 용도
ocpp-watch.sh 실시간 보기 (Ctrl+C로 끝)
ocpp-recent.sh 30 최근 30분
steve.sh status 서버 상태 (start / stop / restart / log)

ocpp-fmt.py가 SteVe 로그를 다음과 같이 바꿔 준다.

  • 시각을 한국 시각으로
  • 방향을 보드→서버 / 서버→보드로
  • OCPP의 요청과 응답을 메시지 ID로 짝지어 └ ... 응답으로

아래 기록들이 모두 이 도구의 출력이다.

나중에는 이것도 원격으로 바꿨다. WSL에서 ocpp-watch-remote.sh가 빌드 PC의 ocpp-watch.sh를 실행해 파일에 기록한다. 시험하는 사람은 지그만 조작하고 “했어”라고 말하면 된다. 복사·붙여넣기가 줄어 시험이 훨씬 빨라졌다.


5. 시험

5-1. M8-2 접속

11:46:41.018  [SC6-0001] Connection is established
11:46:41.101  보드→서버  BootNotification           {"chargePointModel":"SC6_0","chargePointVendor":"KNV",...}
11:46:41.106  서버→보드  GetConfiguration           {"key":[]}
11:46:41.629  서버→보드  └ BootNotification 응답    {"status":"Accepted","interval":14400}
11:46:41.642  보드→서버  SecurityEventNotification  {"type":"StartupOfTheDevice"}
11:46:41.881  보드→서버  StatusNotification         {"connectorId":0,"status":"Available"}
11:46:42.100  보드→서버  StatusNotification         {"connectorId":1,"status":"Available"}

몇 가지가 눈에 띄었다.

  • Heartbeat 간격: 서버가 interval: 14400(4시간)으로 덮어썼다. 보드 설정의 300초는 서버 응답으로 바뀐다.
  • GetConfiguration: SteVe가 BootNotification에 응답하기 전에 이 요청을 보냈다. 보드는 답하지 않았다. 부팅 승인 전 요청이라 libocpp가 무시한 것으로 본다. 규격상 서버는 승인 전에 다른 요청을 보내면 안 되니 서버 쪽 순서 문제다. 나중에 웹 화면에서 다시 보냈더니 9 ms 만에 정상 응답했다.
  • 경고: EVerest가 OCPP 1.6 모듈에 DEPRECATED MODULE 경고를 낸다. 1.6과 2.0.1을 함께 다루는 새 모듈로 옮겨 가는 중이라, 나중에 검토할 과제로 남겼다.

5-2. M8-3 충전 거래

지그를 B로 바꾸면(차 연결) 거래가 시작되고, A로 돌리면(분리) 끝난다.

11:51:20.274  보드→서버  StatusNotification   {"info":"EVConnected","status":"Preparing"}
11:51:20.349  보드→서버  Authorize            {"idTag":"SC6TEST01"}
11:51:20.357  서버→보드  └ Authorize 응답     {"idTagInfo":{"status":"Accepted",...}}
11:51:21.026  보드→서버  StartTransaction     {"connectorId":1,"idTag":"SC6TEST01","meterStart":0}
11:51:21.089  서버→보드  └ StartTransaction 응답 {"transactionId":1,...}
11:51:21.548  보드→서버  StatusNotification   {"status":"Charging"}
11:57:24.929  보드→서버  StopTransaction      {"meterStop":0,"reason":"EVDisconnected","transactionId":1}
11:57:25.164  보드→서버  StatusNotification   {"status":"Available"}

차를 꽂고 서버 인증 응답까지 0.1초, 거래 시작까지 0.8초였다. 정지 이유 EVDisconnected도 정확하다. SteVe 웹 화면의 Transactions에도 거래 1, 2가 기록됐다.

meterStart와 meterStop이 0인 것은 정상이다. 보드에 전력량계를 읽는 모듈이 아직 없다. JSY-MK-163T 전력량계(UART5)를 붙이면 실제 Wh가 들어간다.

5-3. M8-4 서버에서 보드로

지금까지는 보드가 먼저 보낸 메시지뿐이었다. 서버가 먼저 명령을 보내는 방향은 따로 확인해야 한다. 먼저 SteVe 웹 화면에서 GetConfiguration을 보내 통로를 확인했고, 다음으로 원격 시작과 정지를 시험했다.

16:27:05.018  서버→보드  RemoteStartTransaction    {"connectorId":1,"idTag":"SC6TEST01"}
16:27:05.141  보드→서버  └ RemoteStartTransaction 응답 {"status":"Accepted"}
16:27:05.165  보드→서버  StatusNotification        {"info":"Authorized","status":"Preparing"}
16:27:08.638  보드→서버  StartTransaction          {"connectorId":1,"idTag":"SC6TEST01","meterStart":0}
16:27:08.689  서버→보드  └ StartTransaction 응답   {"transactionId":3,...}
16:27:09.398  보드→서버  StatusNotification        {"status":"Charging"}
16:27:22.726  서버→보드  RemoteStopTransaction     {"transactionId":3}
16:27:22.740  보드→서버  └ RemoteStopTransaction 응답 {"status":"Accepted"}
16:27:28.967  보드→서버  StopTransaction           {"reason":"Remote","transactionId":3}
16:27:29.054  보드→서버  StatusNotification        {"status":"Finishing"}
16:27:33.393  보드→서버  StatusNotification        {"status":"Available"}
  • 원격 시작을 받자 차 연결 전에 이미 Authorized가 됐다. 꽂자마자 Authorize 없이 거래가 시작됐다(AuthorizeRemoteTxRequests=false).
  • 원격 정지의 이유는 Remote로 정확히 기록됐다. 지그가 아직 꽂혀 있어 Finishing을 거쳤고, 분리하자 Available이 됐다.

정지 명령에서 StopTransaction까지 6.2초가 걸렸다. EVerest 코드를 보면 정지할 때 이렇게 움직인다.

  1. CP를 +12V 고정(X1)으로 바꿔 차에게 멈추라고 알린다.
  2. 차가 C에서 B로 내려가 릴레이가 열리면 거래를 끝낸다.
  3. 차가 끝까지 반응하지 않으면 20초 뒤 강제로 끊는다.

지그는 스스로 반응하지 않으니, 6.2초는 사람이 지그를 B로 돌린 시점으로 본다. 실제 차나 시뮬레이터에서 다시 볼 항목이다.

5-4. M8-4 비상정지 보고

M7-5에서 비상정지를 걸면 K1이 즉시 떨어지는 것은 확인했다. 이번에는 그것이 서버에 어떻게 보이는지 봤다.

17:02:42.205  보드→서버  StartTransaction     {..."transactionId":5}
17:02:42.284  보드→서버  StatusNotification   {"connectorId":1,"status":"Charging"}
17:02:53.134  보드→서버  StatusNotification   {"connectorId":0,"errorCode":"OtherError","info":"EM_INT line 14",
                                              "status":"Available","vendorErrorCode":"CX008"}
17:02:53.250  보드→서버  StatusNotification   {"connectorId":1,"errorCode":"OtherError","status":"Faulted",
                                              "info":"caused_by:evse_board_support/MREC8EmergencyStop",
                                              "vendorErrorCode":"MREC8EmergencyStop"}
17:02:57.970  보드→서버  StatusNotification   {"connectorId":0,"errorCode":"NoError","status":"Available"}
17:02:58.194  보드→서버  StatusNotification   {"connectorId":1,"status":"SuspendedEVSE"}
17:02:58.259  보드→서버  StatusNotification   {"connectorId":1,"status":"Charging"}
17:03:01.117  보드→서버  StopTransaction      {"reason":"EVDisconnected","transactionId":5}

BSP가 보고한 오류가 그대로 서버까지 올라간다.

  • 커넥터 1은 Faulted 가 되고, vendorErrorCode에 MREC8EmergencyStop이 들어간다.
  • 충전기 전체(connector 0)에는 BSP가 보낸 메시지(EM_INT line 14)가 그대로 실린다.
  • CX008은 EVerest가 쓰는 ChargeX의 오류 코드 표(MREC) 번호다.

운영자는 서버 화면만 보고도 어느 충전기가 왜 멈췄는지 알 수 있다.

그런데 StopTransaction이 없다. 거래 5는 비상정지 중에도 계속 열려 있었다. 해제하자 0.3초 만에 같은 거래 번호로 Charging이 됐다. M7-5에서 본 “해제 뒤 자동 재개”가 OCPP에서도 그대로 보인 것이다.

제품에서는 비상정지 때 거래를 reason: "EmergencyStop"으로 끝내는 방식이 더 흔하다. 사용자는 다시 카드를 대야 한다. 이것도 코드가 아니라 요구사항으로 정할 문제라, 설계자 확인 목록의 “비상정지 해제 방식”에 “거래를 끝낼지 유지할지” 를 함께 적었다.


6. 덤: 보드 시계가 흐른다

기록을 맞춰 보다가 보드가 보낸 timestamp가 서버가 받은 시각보다 앞선다는 것을 알았다.

서버 시각 보드가 적은 시각 차이
11:46 +2.0초  
16:27 +2.9초 4시간 반 동안 0.9초 더 벌어짐
$ timedatectl
                Time zone: Asia/Seoul (KST, +0900)
System clock synchronized: no
              NTP service: active

NTP 서비스는 켜져 있는데 한 번도 맞춰지지 않았다. OCPP 거래 시각과 인증서 유효기간 검사에 시각이 쓰이니 제품에서는 꼭 맞춰야 한다. 지금 하는 통신 시험에는 영향이 없어 보류 과제로 넘겼다.


7. 정리

커밋

sc6-work  f895205 M8 OCPP 1.6J: SteVe 시험 서버, 보드 OCPP 구성, 메시지 보기 도구 (M8-2~M8-4 통과)

오늘 배운 것

  1. 설정이 반영되지 않으면 “어디서 읽었는지”부터 보자. EVerest는 DB가 있으면 YAML을 무시한다. 고친 경고가 계속 나왔던 M7-4의 의문도 이걸로 풀렸다.
  2. 시험 서버도 기본값을 그대로 쓰지 말자. SteVe 원본 compose 파일은 DB 포트를 랜에 연다. 필요한 포트만 열자.
  3. 양방향을 따로 확인하자. 보드가 보낸 메시지가 잘 가도 서버가 보내는 명령까지 되는 것은 아니다. 실제로 첫 GetConfiguration에는 응답이 없었다.
  4. “잘 보고된다”와 “거래가 맞게 처리된다”는 다르다. 비상정지는 Faulted로 잘 올라갔지만, 거래를 끝낼지는 요구사항이 정한다.
  5. 로그를 사람이 옮기지 않게 하자. 메시지 보기 도구를 원격으로 돌리자 시험 한 번에 걸리는 시간이 크게 줄었다.

다음 할 일

  • M8-5 네트워크 끊김
    • 대기 중에 LAN을 뽑았을 때 다시 접속하는지
    • 충전 중에 끊기면 그동안의 StopTransaction이 재접속 뒤 서버에 전달되는지 (과금 누락 여부)
  • 보류 과제
    • 보드 NTP 동기화
    • 전력량계 연결(meterStop 0)
    • OCPP 모듈 교체 검토(DEPRECATED 경고)
    • 보안 프로파일(TLS)

업데이트:

댓글남기기