Stm32mp157_ocpp테스트
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 코드를 보면 정지할 때 이렇게 움직인다.
- CP를 +12V 고정(X1)으로 바꿔 차에게 멈추라고 알린다.
- 차가 C에서 B로 내려가 릴레이가 열리면 거래를 끝낸다.
- 차가 끝까지 반응하지 않으면 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 통과)
오늘 배운 것
- 설정이 반영되지 않으면 “어디서 읽었는지”부터 보자. EVerest는 DB가 있으면 YAML을 무시한다. 고친 경고가 계속 나왔던 M7-4의 의문도 이걸로 풀렸다.
- 시험 서버도 기본값을 그대로 쓰지 말자. SteVe 원본 compose 파일은 DB 포트를 랜에 연다. 필요한 포트만 열자.
- 양방향을 따로 확인하자. 보드가 보낸 메시지가 잘 가도 서버가 보내는 명령까지 되는 것은 아니다. 실제로 첫 GetConfiguration에는 응답이 없었다.
- “잘 보고된다”와 “거래가 맞게 처리된다”는 다르다. 비상정지는 Faulted로 잘 올라갔지만, 거래를 끝낼지는 요구사항이 정한다.
- 로그를 사람이 옮기지 않게 하자. 메시지 보기 도구를 원격으로 돌리자 시험 한 번에 걸리는 시간이 크게 줄었다.
다음 할 일
- M8-5 네트워크 끊김
- 대기 중에 LAN을 뽑았을 때 다시 접속하는지
- 충전 중에 끊기면 그동안의 StopTransaction이 재접속 뒤 서버에 전달되는지 (과금 누락 여부)
- 보류 과제
- 보드 NTP 동기화
- 전력량계 연결(meterStop 0)
- OCPP 모듈 교체 검토(DEPRECATED 경고)
- 보안 프로파일(TLS)
댓글남기기