簡電云 | Java + WebSocket 實現(xiàn) OCPP 2.0.1 充電樁模擬程序-我吃了教官的J吧男男(H)_16歲自愿跟爸爸關系怎么判定
- 報價
- 請來電詢價
- 產(chǎn)品咨詢熱線
- 15920323068
- 微信號
- 15920323068
- 軟件開發(fā)
- 小程序+公眾號+APP定制開發(fā)
充電平臺通常需要同時處理設備接入、訂單、計費、告警和遠程控制。如果所有測試都依賴真實充電樁,會遇到幾個現(xiàn)實問題:
設備數(shù)量有限,難以驗證大量設備同時在線;
插槍、刷卡、充電和拔槍需要人工操作,回歸測試效率低;
斷網(wǎng)、重復報文、事件亂序和異常停止不容易穩(wěn)定復現(xiàn);
不同廠商固件行為存在差異,平臺問題與設備問題容易混在一起;
開發(fā)人員無法隨時獲得完整的充電現(xiàn)場環(huán)境。
模擬器并不是為了替代真實設備,而是把協(xié)議行為變成可重復、可編排的測試輸入。開發(fā)階段先用模擬器驗證平臺狀態(tài)機,再使用真實充電樁完成硬件、計量和電氣安全測試,能夠明顯降低聯(lián)調(diào)成本。
本文實現(xiàn)以下Zui小閉環(huán):
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
Java 充電樁模擬器 Java CSMS
| |
|--- WebSocket / ocpp2.0.1 ----->|
|--- BootNotification ---------->|
|<-- Accepted + interval ---------|
|--- StatusNotification -------->|
|--- Authorize ----------------->|
|<-- Accepted --------------------|
|--- TransactionEvent(Started) -->|
|--- MeterValues --------------->|
|--- TransactionEvent(Updated) -->|
|--- TransactionEvent(Ended) ---->|
|--- Heartbeat ----------------->|
OCPP 2.0.1 JSON 通過 WebSocket 傳輸,握手時使用子協(xié)議ocpp2.0.1。協(xié)議常用三種消息類型:
類型
編號
數(shù)組結(jié)構
CALL
2
[2, messageId, action, payload]
CALLRESULT
3
[3, messageId, payload]
CALLERROR
4
[4, messageId, errorCode, description, details]
例如充電樁上線時發(fā)送:
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
[2,"msg-001","BootNotification",{
"reason":"PowerUp",
"chargingStation":{
"model":"SIM-AC-22K",
"vendorName":"DemoVendor"
}
}]
CSMS 返回:
代碼語言:JavaScript
自動換行
AI代碼解釋
[3,"msg-001",{
"currentTime":"2026-09-30T02:00:00Z",
"interval":30,
"status":"Accepted"}]
messageId 用于關聯(lián)請求和響應。一次連接中的CALL 應使用唯一標識,客戶端還需要維護“消息編號—等待結(jié)果”的映射。
本文為了展示 WebSocket 和 OCPP報文的底層處理過程,不引入商業(yè) OCPP SDK,而是使用 Jackson 構造消息。這樣代碼容易理解,但生產(chǎn)項目仍應補充完整的OCPP 2.0.1 JSON Schema 校驗和功能塊實現(xiàn)。
三、創(chuàng)建 Maven 工程示例使用 Java 17。工程結(jié)構如下:
代碼語言:JavaScript
自動換行
AI代碼解釋
ocpp201-java-demo/
├── pom.xml└── src/main/java/com/example/ocpp/
├── CsmsServer.java
├── CsmsEndpoint.java
└── ChargePointSimulator.java
pom.xml 使用 Tyrus 作為 JakartaWebSocket 實現(xiàn),Jackson 負責 JSON 解析:
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ocpp201-java-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly-server</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build></project>
固定依賴版本有利于復現(xiàn)。Tyrus 不僅提供WebSocket API,還提供可直接啟動的服務端容器和客戶端實現(xiàn)。
四、實現(xiàn) CSMS WebSocket服務端1. 創(chuàng)建 WebSocket端點創(chuàng)建CsmsEndpoint.java:
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
package com.example.ocpp;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.node.ArrayNode;importcom.fasterxml.jackson.databind.node.ObjectNode;importjakarta.websocket.CloseReason;importjakarta.websocket.OnClose;import jakarta.websocket.OnError;importjakarta.websocket.OnMessage;import jakarta.websocket.OnOpen;importjakarta.websocket.Session;importjakarta.websocket.server.PathParam;importjakarta.websocket.server.ServerEndpoint;importjava.io.IOException;import java.time.Instant;
@ServerEndpoint(
value = "/ocpp/{chargePointId}",
subprotocols = {"ocpp2.0.1"}
)
public class CsmsEndpoint {
private static final ObjectMapper JSON = newObjectMapper();
@OnOpen
public void onOpen(
Session session,
@PathParam("chargePointId") String chargePointId) throwsIOException {
if(!"ocpp2.0.1".equals(session.getNegotiatedSubprotocol())) {
session.close(newCloseReason(
CloseReason.CloseCodes.PROTOCOL_ERROR,
"OCPP 2.0.1 subprotocol required"));
return;
}
session.getUserProperties().put("chargePointId",chargePointId);
System.out.printf("[Connected] id=%s,session=%s%n",
chargePointId, session.getId());
}
@OnMessage
public void onMessage(String text, Session session)throws IOException {
JsonNode frame =JSON.readTree(text);
if (!frame.isArray() || frame.size()< 3) {
session.getBasicRemote().sendText(callError(
"unknown", "FormationViolation", "Invalid frame"));
return;
}
int messageType =frame.get(0).asInt();
if (messageType != 2 || frame.size() !=4) {
https:// 本示例服務端只處理充電樁發(fā)起的CALL。
return;
}
String messageId =frame.get(1).asText();
String action =frame.get(2).asText();
JsonNode payload = frame.get(3);
try {
ObjectNode response =handleCall(session, action, payload);
session.getBasicRemote().sendText(callResult(messageId,response));
} catch (IllegalArgumentException e){
session.getBasicRemote().sendText(callError(
messageId, "NotSupported", e.getMessage()));
} catch (Exception e) {
session.getBasicRemote().sendText(callError(
messageId, "InternalError", "Failed touest"));
}
}
private ObjectNode handleCall(
Session session, Stringaction, JsonNode payload) {
String cpId = (String)session.getUserProperties().get("chargePointId");
ObjectNode response =JSON.createObjectNode();
switch (action) {
case "BootNotification"-> {
System.out.printf("[Boot] id=%s, payload=%s%n", cpId,payload);
response.put("currentTime", Instant.now().toString());
response.put("interval", 30);
response.put("status", "Accepted");
}
case "Heartbeat" ->{
System.out.printf("[Heartbeat] id=%s%n", cpId);
response.put("currentTime", Instant.now().toString());
}
case "StatusNotification"-> System.out.printf(
"[Status] id=%s, evse=%s, connector=%s, status=%s%n",
cpId,
payload.path("evseId").asText(),
payload.path("connectorId").asText(),
payload.path("connectorStatus").asText());
case "Authorize" ->{
String token= payload.path("idToken").path("idToken").asText();
Stringstatus = "TEST-001".equals(token)
? "Accepted" : "Invalid";
response.putObject("idTokenInfo").put("status", status);
System.out.printf("[Authorize] token=%s, status=%s%n",
token, status);
}
case "TransactionEvent"-> System.out.printf(
"[Transaction] id=%s, event=%s, seq=%s, trigger=%s%n",
payload.path("transactionInfo")
.path("transactionId").asText(),
payload.path("eventType").asText(),
payload.path("seqNo").asText(),
payload.path("triggerReason").asText());
case "MeterValues" ->System.out.printf(
"[Meter] id=%s, evse=%s, values=%s%n",
cpId, payload.path("evseId").asText(),
payload.path("meterValue"));
default -> throw newIllegalArgumentException(
"Unsupported action: " + action);
}
return response;
}
private String callResult(String messageId, ObjectNodepayload) {
ArrayNode frame =JSON.createArrayNode();
frame.add(3).add(messageId).add(payload);
return frame.toString();
}
private String callError(
String messageId, Stringcode, String description) {
ArrayNode frame =JSON.createArrayNode();
frame.add(4)
.add(messageId)
.add(code)
.add(description)
.add(JSON.createObjectNode());
return frame.toString();
}
@OnClose
public void onClose(Session session, CloseReasonreason) {
System.out.printf("[Disconnected]session=%s, reason=%s%n",
session.getId(), reason);
}
@OnError
public void onError(Session session, Throwable error){
System.err.printf("[WebSocket error]session=%s, error=%s%n",
session ==null ? "unknown" : session.getId(),
error.getMessage());
}
}
該端點完成四件事:校驗協(xié)商后的 WebSocket子協(xié)議、從路徑取得充電樁編號、按 CALL 格式解析請求、使用相同 messageId 返回 CALLRESULT。
2. 啟動服務端容器創(chuàng)建CsmsServer.java:
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
package com.example.ocpp;importorg.glassfish.tyrus.server.Server;importjava.io.BufferedReader;import java.io.InputStreamReader;
public class CsmsServer {
public static void main(String[] args) throwsException {
Server server = new Server(
"0.0.0.0",
9000,
"/",
null,
CsmsEndpoint.class);
try {
server.start();
System.out.println(
"CSMS started:ws:https://127.0.0.1:9000/ocpp/{chargePointId}");
System.out.println("PressEnter to stop.");
new BufferedReader(newInputStreamReader(System.in)).readLine();
} finally {
server.stop();
}
}
}
為了突出協(xié)議鏈路,示例只打印收到的數(shù)據(jù)。正式 CSMS還需要設備認證、Schema 校驗、事件持久化、冪等處理、訂單狀態(tài)機和審計日志。
五、實現(xiàn) Java 充電樁模擬器創(chuàng)建ChargePointSimulator.java:
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
package com.example.ocpp;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.node.ArrayNode;importcom.fasterxml.jackson.databind.node.ObjectNode;importjakarta.websocket.ClientEndpoint;importjakarta.websocket.CloseReason;importjakarta.websocket.ContainerProvider;importjakarta.websocket.OnClose;import jakarta.websocket.OnError;importjakarta.websocket.OnMessage;import jakarta.websocket.OnOpen;importjakarta.websocket.Session;importjakarta.websocket.WebSocketContainer;import java.net.URI;importjava.time.Instant;import java.util.Map;import java.util.UUID;importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.TimeUnit;
@ClientEndpoint(subprotocols = {"ocpp2.0.1"})
public class ChargePointSimulator {
private static final ObjectMapper JSON = newObjectMapper();
private static final long RESPONSE_TIMEOUT_SECONDS =10;
private final Map<String,CompletableFuture<JsonNode>> pending =
newConcurrentHashMap<>();
private Session session;
private int heartbeatInterval = 30;
@OnOpen
public void onOpen(Session session) {
this.session = session;
System.out.printf("Connected,subprotocol=%s%n",
session.getNegotiatedSubprotocol());
}
@OnMessage
public void onMessage(String text) {
try {
JsonNode frame =JSON.readTree(text);
int messageType =frame.get(0).asInt();
String messageId =frame.get(1).asText();
if (messageType == 3){
complete(messageId, frame.get(2));
} else if (messageType ==4) {
String error= frame.get(2).asText()
+ ": " + frame.get(3).asText();
fail(messageId, new IllegalStateException(error));
}
} catch (Exception e) {
System.err.println("Invalid response: " +e.getMessage());
}
}
@OnClose
public void onClose(Session session, CloseReasonreason) {
IllegalStateException error = newIllegalStateException(
"WebSocketclosed: " + reason);
pending.values().forEach(future ->future.completeExceptionally(error));
pending.clear();
}
@OnError
public void onError(Session session, Throwable error){
System.err.println("WebSocket error: " +error.getMessage());
}
public JsonNode call(String action, ObjectNodepayload) throws Exception {
String messageId =UUID.randomUUID().toString();
CompletableFuture<JsonNode> future= new CompletableFuture<>();
pending.put(messageId, future);
ArrayNode frame =JSON.createArrayNode();
frame.add(2).add(messageId).add(action).add(payload);
try {
session.getAsyncRemote().sendText(frame.toString(), result-> {
if(!result.isOK()) {
fail(messageId, result.getException());
}
});
returnfuture.get(RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} finally {
pending.remove(messageId);
}
}
private void complete(String messageId, JsonNodepayload) {
CompletableFuture<JsonNode> future= pending.get(messageId);
if (future != null) {
future.complete(payload);
}
}
private void fail(String messageId, Throwable error){
CompletableFuture<JsonNode> future= pending.get(messageId);
if (future != null) {
future.completeExceptionally(error);
}
}
private void boot() throws Exception {
ObjectNode station =JSON.createObjectNode();
station.put("model", "SIM-AC-22K");
station.put("vendorName","DemoVendor");
station.put("serialNumber","SIM-CP-001");
station.put("firmwareVersion","1.0.0");
ObjectNode payload =JSON.createObjectNode();
payload.put("reason", "PowerUp");
payload.set("chargingStation",station);
JsonNode response =call("BootNotification", payload);
if(!"Accepted".equals(response.path("status").asText())) {
throw newIllegalStateException(
"Registration rejected: " + response);
}
heartbeatInterval =response.path("interval").asInt(30);
System.out.printf("Boot accepted,heartbeat=%ds%n", heartbeatInterval);
}
private void heartbeat() throws Exception {
JsonNode response = call("Heartbeat",JSON.createObjectNode());
System.out.println("Heartbeat response:" + response);
}
private void reportStatus(String status) throwsException {
ObjectNode payload =JSON.createObjectNode();
payload.put("timestamp",Instant.now().toString());
payload.put("connectorStatus",status);
payload.put("evseId", 1);
payload.put("connectorId", 1);
call("StatusNotification", payload);
}
private boolean authorize(String token) throwsException {
ObjectNode idToken =JSON.createObjectNode();
idToken.put("idToken", token);
idToken.put("type", "ISO14443");
ObjectNode payload =JSON.createObjectNode();
payload.set("idToken", idToken);
JsonNode response = call("Authorize",payload);
return "Accepted".equals(
response.path("idTokenInfo").path("status").asText());
}
private void transactionEvent(
String eventType,
String triggerReason,
int seqNo,
String transactionId,
String token) throwsException {
ObjectNode payload =JSON.createObjectNode();
payload.put("eventType", eventType);
payload.put("timestamp",Instant.now().toString());
payload.put("triggerReason",triggerReason);
payload.put("seqNo", seqNo);
ObjectNode transactionInfo =payload.putObject("transactionInfo");
transactionInfo.put("transactionId",transactionId);
transactionInfo.put("chargingState",
"Ended".equals(eventType) ? "Idle" : "Charging");
ObjectNode evse =payload.putObject("evse");
evse.put("id", 1);
evse.put("connectorId", 1);
if (token != null) {
ObjectNode idToken =payload.putObject("idToken");
idToken.put("idToken",token);
idToken.put("type","ISO14443");
}
call("TransactionEvent", payload);
}
private void meterValues(double energyKwh) throwsException {
ObjectNode payload =JSON.createObjectNode();
payload.put("evseId", 1);
ObjectNode meterValue =JSON.createObjectNode();
meterValue.put("timestamp",Instant.now().toString());
ObjectNode sampledValue =JSON.createObjectNode();
sampledValue.put("value",energyKwh);
sampledValue.put("context","Sample.Periodic");
sampledValue.put("measurand","Energy.Active.Import.Register");
sampledValue.putObject("unitOfMeasure").put("unit","kWh");
meterValue.putArray("sampledValue").add(sampledValue);
payload.putArray("meterValue").add(meterValue);
call("MeterValues", payload);
}
private void runChargeSession() throws Exception {
String token = "TEST-001";
reportStatus("Available");
Thread.sleep(1_000);
if (!authorize(token)) {
System.out.println("Tokenrejected: " + token);
return;
}
String transactionId =UUID.randomUUID().toString();
reportStatus("Occupied");
transactionEvent(
"Started","Authorized", 0, transactionId, token);
double[] energyValues = {0.2, 0.5,0.9};
for (int i = 0; i <energyValues.length; i++) {
Thread.sleep(2_000);
meterValues(energyValues[i]);
transactionEvent(
"Updated", "MeterValuePeriodic", i + 1,
transactionId, null);
}
transactionEvent(
"Ended","StopAuthorized", 4, transactionId, token);
reportStatus("Available");
System.out.println("Transactionfinished: " + transactionId);
}
public static void main(String[] args) throwsException {
String url ="ws:https://127.0.0.1:9000/ocpp/SIM-CP-001";
ChargePointSimulator simulator = newChargePointSimulator();
WebSocketContainer container =ContainerProvider
.getWebSocketContainer();
Session session =container.connectToServer(
simulator,URI.create(url));
try {
simulator.boot();
simulator.runChargeSession();
https:// 演示一次心跳。常駐模擬器應按BootNotification 返回的
https:// interval 使用ScheduledExecutorService 周期發(fā)送。
Thread.sleep(Math.min(simulator.heartbeatInterval, 3) *1_000L);
simulator.heartbeat();
} finally {
session.close();
}
}
}
模擬器中的 pending是關鍵結(jié)構。每發(fā)送一個 CALL,就以messageId 保存一個CompletableFuture;收到 CALLRESULT或 CALLERROR 后,再完成對應的Future。這樣即使多個請求同時在途,也不會把響應關聯(lián)到錯誤的業(yè)務請求。
六、編譯與運行先編譯工程:
代碼語言:JavaScript
自動換行
AI代碼解釋
mvn clean compile
打開第一個終端啟動 CSMS:
代碼語言:JavaScript
自動換行
AI代碼解釋
mvn exec:java-Dexec.mainClass=com.example.ocpp.CsmsServer
打開第二個終端啟動充電樁模擬器:
代碼語言:JavaScript
自動換行
AI代碼解釋
mvn exec:java-Dexec.mainClass=com.example.ocpp.ChargePointSimulator
CSMS 會依次輸出類似日志:
展開
代碼語言:JavaScript
自動換行
AI代碼解釋
[Connected] id=SIM-CP-001, session=...
[Boot] id=SIM-CP-001, payload=...
[Status] id=SIM-CP-001, evse=1, connector=1,status=Available[Authorize] token=TEST-001, status=Accepted[Status]id=SIM-CP-001, evse=1, connector=1, status=Occupied[Transaction]id=..., event=Started, seq=0, trigger=Authorized[Meter]id=SIM-CP-001, evse=1, values=...
[Transaction] id=..., event=Updated, seq=1,trigger=MeterValuePeriodic[Transaction] id=..., event=Ended, seq=4,trigger=StopAuthorized[Status] id=SIM-CP-001, evse=1, connector=1,status=Available[Heartbeat] id=SIM-CP-001
OCPP 2.0.1 不再使用 OCPP 1.6的 StartTransaction 和 StopTransaction,而是統(tǒng)一使用 TransactionEvent。充電站生成字符串類型的transactionId,eventType表示開始、更新或結(jié)束,seqNo表示事件在本次交易中的順序。
七、幾個容易踩坑的實現(xiàn)細節(jié)1.WebSocket 路徑不等于可信身份示例從 /ocpp/SIM-CP-001中取得充電樁編號,但客戶端聲明的編號不能直接作為可信身份。生產(chǎn)環(huán)境至少應使用wss:https://,并通過 HTTP BasicAuthentication 或客戶端證書校驗設備身份,同時檢查憑據(jù)與充電站編號的綁定關系。
2. 必須協(xié)商 OCPP 子協(xié)議客戶端和服務端都聲明ocpp2.0.1,握手后還要檢查Session.getNegotiatedSubprotocol()。如果沒有協(xié)商出雙方支持的版本,應關閉連接,不能繼續(xù)按普通 WebSocket文本處理。
3. BootNotification決定設備后續(xù)行為CSMS 可以返回:
Accepted:注冊成功,可以發(fā)送業(yè)務消息;
Pending:平臺暫未完成注冊,應按interval 再次請求;
Rejected:注冊被拒絕,不應繼續(xù)發(fā)送業(yè)務消息。
演示代碼只繼續(xù)處理Accepted。完整模擬器還應實現(xiàn) Pending 重試和Rejected 退避策略。
4. WebSocket Ping 不能代替HeartbeatPing/Pong用于判斷網(wǎng)絡連接是否存活,OCPP Heartbeat 用于協(xié)議層時鐘同步和在線狀態(tài)維護。兩者用途不同。常駐模擬器應根據(jù)BootNotification 返回的 interval,使用 ScheduledExecutorService周期發(fā)送 Heartbeat。
5. 交易事件必須保證順序與冪等seqNo 應在同一交易內(nèi)單調(diào)遞增。設備斷網(wǎng)恢復后可能補發(fā)事件,平臺必須結(jié)合充電樁編號、transactionId、seqNo和事件內(nèi)容去重,不能因為 Updated重發(fā)而重復計費。
本文分別發(fā)送 MeterValues與TransactionEvent(Updated)。實際項目也可以在 TransactionEvent的 meterValue中攜帶交易計量值。平臺應保存原始表底,費用計算不應依賴浮點差值反復累加。
6. 狀態(tài)與交易不能互相替代StatusNotification(Occupied) 只表示 EVSE或連接器被占用,不一定已經(jīng)產(chǎn)生有效交易;TransactionEvent(Started) 表示交易建立,也不代表此刻一定有功率輸出。平臺需要分別維護設備、連接器和交易狀態(tài)。
八、擴展為自動化測試工具1. 支持遠程啟動和停止當前 CSMS 只響應充電樁上行請求。下一步可以讓CSMS 主動發(fā)送 Retion 和 Reion,模擬器則處理服務端 CALL 并返回 CALLRESULT。
遠程啟動時要保存remoteStartId,并在后續(xù)TransactionEvent 中回傳,使平臺可以關聯(lián)遠程命令和實際交易。由于雙方都可能發(fā)起 CALL,CSMS也需要實現(xiàn)與模擬器相同的 messageId 和Future 映射。
2. 使用場景文件驅(qū)動可以把設備編號、EVSE數(shù)量、Token、功率曲線和停止原因放入 YAML:
代碼語言:JavaScript
自動換行
AI代碼解釋
chargePointId: SIM-CP-001evseId: 1connectorId: 1token:TEST-001meterValuesKwh: [0.2, 0.5, 0.9]stopReason: Local
測試人員只需修改場景文件,不必改動 Java代碼。
3. 增加異常場景場景
模擬方式
平臺檢查點
Token 無效
使用 TEST-INVALID
不創(chuàng)建交易,不允許啟動
充電中斷網(wǎng)
Started 后關閉WebSocket
訂單保持、超時與補傳是否正確
請求超時
CSMS 延遲返回CALLRESULT
Future超時與重試策略是否正確
消息重復
重發(fā)同一業(yè)務事件
是否冪等,費用是否重復累加
事件亂序
延遲發(fā)送較小的 seqNo
是否識別異常順序并告警
電表回退
后一次表底小于前一次
是否阻止負電量和負費用
設備故障
上報 Faulted
是否停止可用性展示并產(chǎn)生告警
批量測試時,每臺模擬設備應擁有獨立的 WebSocketSession、設備編號、交易編號和狀態(tài)??梢允褂镁€程池或虛擬線程并發(fā)創(chuàng)建客戶端,但不要一開始就追求極高連接數(shù)。建議從10、100、500 臺逐級增加,同時觀察 CSMS 的 CPU、內(nèi)存、數(shù)據(jù)庫連接、消息積壓和響應時間。
5. 增加 Schema 校驗和測試斷言當前代碼只檢查 WebSocket 數(shù)組結(jié)構,沒有加載OCPP 2.0.1 JSON Schema。平臺測試工具至少應增加:
代碼語言:JavaScript
自動換行
AI代碼解釋
原始 WebSocket 幀
-> OCPP JSON Schema 校驗
-> 消息解析與狀態(tài)更新
-> 場景斷言和測試報告
原始幀用于還原事實,Schema用于發(fā)現(xiàn)字段問題,場景斷言用于判斷平臺行為。日志中的Token、證書信息和用戶標識需要脫敏,并設置合理的保留期限。
九、從示例走向生產(chǎn)聯(lián)調(diào)本文代碼適合本地開發(fā)和協(xié)議學習,不應直接作為生產(chǎn)充電樁固件或完整CSMS。正式聯(lián)調(diào)前至少需要補齊:
使用WSS、設備認證和證書生命周期管理;
加載 OCPP 2.0.1Schema,嚴格校驗請求和響應;
建立設備、EVSE、連接器和交易的持久化狀態(tài)模型;
正確處理 CALLERROR、超時、重試和WebSocket 重連;
支持離線消息緩存、事件補傳和重復消息冪等;
處理時間同步、計量單位、精度和異常表底;
增加遠程啟停、復位、解鎖、固件升級和智能充電;
使用 OCA一致性測試工具和真實設備完成Zui終驗證。
代碼能夠收發(fā) OCPP 報文,不代表產(chǎn)品已經(jīng)通過OCPP 認證。正式項目仍應以 Open Charge Alliance 發(fā)布的 OCPP 2.0.1規(guī)范、勘誤、實施指南和認證要求為準。
十、總結(jié)本文使用 Java 17、JakartaWebSocket、Tyrus 和 Jackson 實現(xiàn)了一個 OCPP 2.0.1 Zui小通信閉環(huán)。CSMS 接收 WebSocket連接并處理 CALL,充電樁模擬器完成上線、狀態(tài)上報、鑒權、交易開始、計量上報、交易結(jié)束和心跳。
一個真正有價值的模擬器,核心不是生成幾條JSON,而是復現(xiàn)連接、設備狀態(tài)、授權、交易和計量之間的時序關系。在此基礎上加入遠程命令、異常注入、批量并發(fā)、Schema校驗和自動斷言,就可以形成供研發(fā)、測試和交付團隊共同使用的 OCPP 2.0.1 測試工具。
說明:本文代碼用于技術學習和聯(lián)調(diào)演示,不替代 OCPP2.0.1 正式規(guī)范,也不構成生產(chǎn)部署的完整安全方案。
共享充電樁軟件開發(fā),充電樁共享軟件開發(fā),智能充電樁系統(tǒng)開發(fā),充電樁軟件開發(fā)公司,充電樁app的開發(fā),共享充電樁app開發(fā),充電樁平臺開發(fā),充電樁app開發(fā)
公司介紹廣州優(yōu)匠科技有限公司成立于2017年,于2018年開始深耕充電樁運營平臺,是國內(nèi)海外的共享充電運營平臺整體解決方案服務商,致力于推動共享充電領域的標準化、產(chǎn)品化,為用戶提供簡單便捷的充電體驗、充電安全及幫助合作伙伴提供穩(wěn)定、高效的運營平臺。公司為全球運營商、充電樁廠家提供電動自行車、新能源汽車運營平臺的Saas租賃服務、私有化部署、個性定制化開發(fā)服務。同時我們將為全球所有國家、地區(qū)等運營商提供運營平臺的Saas租賃服務、幫助中小...