1. 引言与概述
本项目旨在基于 OpenHarmony 操作系统,开发一款符合行业标准的智能交流/直流充电桩。核心技术挑战在于实现与后台管理系统的稳定通信(OCPP)、高精度的电能计量与校准,以及在复杂网络环境下的可靠数据同步。
本文档将围绕以下三个核心模块展开,详细阐述其设计思路、技术选型、具体实现及模块间的整合方案:
- OCPP 1.6/2.0.1 通信协议栈:实现充电桩与中央管理系统(CMS)的实时双向通信。
- 电能计量与校准模块:确保充电电量数据的精确性和可追溯性。
- 车-桩-云协同与数据同步:保障充电记录在弱网或断网情况下的完整性与最终一致性。
2. 系统架构
系统采用分层架构设计,从上至下分为:应用层、框架与服务层、以及驱动与硬件层。
架构说明:
- 应用层:使用 ArkTS 编写业务逻辑,包括 OCPP 状态机、计量服务、数据同步服务和用户界面。
- 框架与服务层:利用 OpenHarmony 提供的系统能力,如网络通信(WebSocket)、安全存储(Huks)、分布式数据库等。
- 驱动与硬件层:通过自定义 NAPI 模块桥接 ArkTS 与 HDF 驱动框架,与底层硬件(如 ADC、电表芯片)交互。
3. 核心功能实现
3.1 OCPP 1.6/2.0.1 通信协议实现
3.1.1 设计思路
OCPP(Open Charge Point Protocol)是基于 JSON 的 WebSocket 协议。我们在 ArkTS 层构建一个状态机来管理连接和消息流,并将充电会话与 Ability 生命周期关联。
3.1.2 技术选型
- 语言/框架:ArkTS
- 网络通信 :
@kit.NetworkKit
(WebSocket)
3.1.3 代码实现
1. OCPP 状态机管理器 (model/OcppManager.ets
)
typescript
// model/OcppManager.ets
import { webSocket } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
enum OcppState {
DISCONNECTED = 'Disconnected',
CONNECTING = 'Connecting',
CONNECTED = 'Connected',
REGISTERED = 'Registered',
}
const OCPP_MESSAGE_TYPE = {
CALL: 2,
CALL_RESULT: 3,
CALL_ERROR: 4,
};
export class OcppManager {
private ws: webSocket.WebSocket | null = null;
private state: OcppState = OcppState.DISCONNECTED;
private serverUrl: string = 'ws://your.cms.server/ocpp/';
private chargePointId: string = 'CHARGE_POINT_001';
private messageId: number = 1;
constructor() {
this.connect();
}
private connect(): void {
if (this.state !== OcppState.DISCONNECTED) return;
this.setState(OcppState.CONNECTING);
this.ws = webSocket.createWebSocket();
this.ws.on('open', (err: BusinessError) => {
if (err) {
hilog.error(0x0000, 'OcppManager', `WebSocket open error: ${JSON.stringify(err)}`);
this.setState(OcppState.DISCONNECTED);
return;
}
hilog.info(0x0000, 'OcppManager', 'WebSocket connected.');
this.setState(OcppState.CONNECTED);
this.sendBootNotification();
});
this.ws.on('message', (err: BusinessError, value: string | ArrayBuffer) => {
if (err) return;
this.handleIncomingMessage(value as string);
});
this.ws.on('close', () => {
hilog.info(0x0000, 'OcppManager', 'WebSocket closed.');
this.setState(OcppState.DISCONNECTED);
// Implement reconnection logic here
});
this.ws.on('error', (err: BusinessError) => {
hilog.error(0x0000, 'OcppManager', `WebSocket error: ${JSON.stringify(err)}`);
this.setState(OcppState.DISCONNECTED);
});
this.ws.connect(this.serverUrl);
}
private setState(newState: OcppState): void {
hilog.info(0x0000, 'OcppManager', `State transition: ${this.state} -> ${newState}`);
this.state = newState;
}
private sendBootNotification(): void {
const payload = { chargePointModel: "OH Charger", chargePointVendor: "MyCompany" };
this.send('BootNotification', payload);
}
public send(action: string, payload: Object): void {
if (this.state !== OcppState.CONNECTED && this.state !== OcppState.REGISTERED) return;
const message = [OCPP_MESSAGE_TYPE.CALL, this.messageId++, action, payload];
this.ws?.send(JSON.stringify(message));
}
private handleIncomingMessage(message: string): void {
const [messageType, messageId, , payload] = JSON.parse(message);
if (messageType === OCPP_MESSAGE_TYPE.CALL_RESULT && messageId === 1) {
if (payload.status === 'Accepted') {
this.setState(OcppState.REGISTERED);
hilog.info(0x0000, 'OcppManager', 'Charge point registered.');
}
}
}
public startTransaction(connectorId: number, idTag: string): void {
const payload = { connectorId, idTag, meterStart: 0, timestamp: new Date().toISOString() };
this.send('StartTransaction', payload);
}
public stopTransaction(transactionId: number, idTag: string): void {
const payload = { transactionId, idTag, meterStop: 1000, timestamp: new Date().toISOString() };
this.send('StopTransaction', payload);
}
public getState(): OcppState {
return this.state;
}
}
3.1.4 讲解
- 状态机 :
OcppState
枚举管理连接生命周期,setState
方法封装状态切换。 - WebSocket 封装 :使用
@kit.NetworkKit
的 WebSocket API,监听核心事件,实现异步通信。 - 消息格式 :严格遵循 OCPP-J
[messageType, messageId, action, payload]
格式。 - 生命周期映射 :
startTransaction
和stopTransaction
方法可供 UI Ability 在其生命周期回调中调用,实现充电会话与用户交互的关联。
3.2 电能计量校准
3.2.1 设计思路
ArkTS 应用无法直接访问硬件。我们通过 NAPI 模块作为桥梁,连接 ArkTS 与 C++ 编写的 HDF 驱动代码,实现 ADC 数据读取。校准系数使用 Huks 加密后,通过文件系统存储。
3.2.2 技术选型
- 硬件交互 :HDF ADC Driver + 自定义 NAPI 模块
- 安全存储 :
@kit.UnifiedKeyStoreKit
(Huks) +@ohos.file.fs
3.2.3 代码实现
第一步:编写 C++ NAPI 模块 (src/main/cpp/adc_reader.cpp
)
cpp
// src/main/cpp/adc_reader.cpp
#include "napi/native_api.h"
#include <hdf_log.h>
#include <adc_if.h>
#define ADC_DEVICE_NUM 0
#define ADC_CHANNEL_NUM 1
static napi_value ReadAdc(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int32_t deviceNum, channelNum;
napi_get_value_int32(env, args[0], &deviceNum);
napi_get_value_int32(env, args[1], &channelNum);
DevHandle *handle = AdcOpen(deviceNum);
if (handle == nullptr) {
HDF_LOGE("AdcOpen failed");
return nullptr;
}
uint32_t readData;
int32_t ret = AdcRead(handle, channelNum, &readData);
AdcClose(handle);
if (ret != HDF_SUCCESS) {
HDF_LOGE("AdcRead failed, ret: %d", ret);
return nullptr;
}
HDF_LOGI("ADC Read Success: %u", readData);
napi_value result;
napi_create_uint32(env, readData, &result);
return result;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{ "readAdc", nullptr, ReadAdc, nullptr, nullptr, nullptr, napi_default, nullptr },
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
NAPI_MODULE(NAPI_GITHUB_MODULE_NAME, Init)
第二步:配置编译 (src/main/cpp/BUILD.gn
)
gn
# src/main/cpp/BUILD.gn
import("//build/ohos.gni")
ohos_shared_library("adc_reader") {
sources = [ "adc_reader.cpp" ]
include_dirs = [
"//drivers/hdf_core/adapter/uhdf2/include",
"//drivers/hdf_core/framework/include/core",
"//drivers/hdf_core/framework/include/utils",
"//third_party/bounds_checking_function/include",
]
deps = [
"//drivers/hdf_core/adapter/uhdf2:libhdf",
"//drivers/hdf_core/adapter/uhdf2/metadata:libhdf_meta",
]
external_deps = [ "hiviewdfx_hilog_native:libhilog" ]
output_name = "adc_reader" # ArkTS 中 import 时使用
subsystem_name = "charger"
part_name = "charger_part"
}
第三步:ArkTS 层调用 (model/MeteringService.ets
)
typescript
// model/MeteringService.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { HuksHelper } from './HuksHelper';
import adcReader from 'libadc_reader.so'; // 导入 NAPI 模块
interface CalibrationFactors { offset: number; scale: number; }
export class MeteringService {
private calibrationFactors: CalibrationFactors = { offset: 0, scale: 1.0 };
private readonly CALIBRATION_KEY_ALIAS = 'metering_calibration_factors';
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
this.init();
}
private async init(): Promise<void> {
await this.loadCalibrationFactors();
}
private async loadCalibrationFactors(): Promise<void> {
try {
const encryptedData = await HuksHelper.getData(this.context, 'calibration.bin');
if (encryptedData) {
const decryptedData = await HuksHelper.decrypt(this.CALIBRATION_KEY_ALIAS, encryptedData);
this.calibrationFactors = JSON.parse(decryptedData);
hilog.info(0x0000, 'MeteringService', `Calibration loaded: ${JSON.stringify(this.calibrationFactors)}`);
}
} catch (error) {
hilog.error(0x0000, 'MeteringService', `Failed to load calibration: ${JSON.stringify(error)}`);
}
}
public async calibrateAndSave(standardValue: number): Promise<void> {
const rawValue = await this.getRawValue(0, 1);
const newScale = rawValue > 0 ? standardValue / rawValue : 1.0;
this.calibrationFactors = { offset: 0, scale: newScale };
hilog.info(0x0000, 'MeteringService', `New calibration: ${JSON.stringify(this.calibrationFactors)}`);
try {
const plainData = JSON.stringify(this.calibrationFactors);
const encryptedData = await HuksHelper.encrypt(this.CALIBRATION_KEY_ALIAS, plainData);
await HuksHelper.saveData(this.context, 'calibration.bin', encryptedData);
} catch (error) {
hilog.error(0x0000, 'MeteringService', `Failed to save calibration: ${JSON.stringify(error)}`);
}
}
private async getRawValue(device: number, channel: number): Promise<number> {
try {
return adcReader.readAdc(device, channel);
} catch (error) {
hilog.error(0x0000, 'MeteringService', `NAPI read failed: ${JSON.stringify(error)}`);
return 0;
}
}
public async getCalibratedEnergy(): Promise<number> {
const rawValue = await this.getRawValue(0, 1);
return (rawValue - this.calibrationFactors.offset) * this.calibrationFactors.scale;
}
}
第四步:Huks 辅助类 (model/HuksHelper.ets
)
typescript
// model/HuksHelper.ets
import { huks } from '@kit.UnifiedKeyStoreKit';
import { HksKeyAlg, HksKeyPurpose, HksTag } from '@kit.UnifiedKeyStoreKit';
import { fs } from '@ohos.file.fs';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class HuksHelper {
private static readonly OPTIONS: huks.HuksOptions = {
properties: [
{ tag: HksTag.HKS_TAG_ALGORITHM, value: HksKeyAlg.HKS_ALG_AES },
{ tag: HksTag.HKS_TAG_PURPOSE, value: HksKeyPurpose.HKS_KEY_PURPOSE_ENCRYPT | HksKeyPurpose.HKS_KEY_PURPOSE_DECRYPT },
{ tag: HksTag.HKS_TAG_KEY_SIZE, value: 256 },
{ tag: HksTag.HKS_TAG_BLOCK_MODE, value: huks.HksKeyBlockMode.HKS_MODE_GCM }
],
inData: new Uint8Array()
};
public static async getKey(alias: string): Promise<boolean> {
try { await huks.isKeyExist(alias, this.OPTIONS); return true; }
catch {
try { await huks.generateKey(alias, this.OPTIONS); return true; }
catch (e) { hilog.error(0x0000, 'HuksHelper', `Gen key failed: ${JSON.stringify(e)}`); return false; }
}
}
public static async encrypt(alias: string, plainText: string): Promise<Uint8Array> {
if (!(await this.getKey(alias))) throw new Error("Key not available");
const options = { ...this.OPTIONS, inData: new Uint8Array(buffer.from(plainText, 'utf8')) };
const result = await huks.encrypt(alias, options);
return result.outData;
}
public static async decrypt(alias: string, cipherText: Uint8Array): Promise<string> {
if (!(await this.getKey(alias))) throw new Error("Key not available");
const options = { ...this.OPTIONS, inData: cipherText };
const result = await huks.decrypt(alias, options);
return String.fromCharCode.apply(null, result.outData);
}
public static async saveData(context: common.UIAbilityContext, fileName: string, data: Uint8Array): Promise<void> {
const filePath = context.filesDir + '/' + fileName;
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, data);
fs.closeSync(file);
}
public static async getData(context: common.UIAbilityContext, fileName: string): Promise<Uint8Array | null> {
try {
const filePath = context.filesDir + '/' + fileName;
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const stat = fs.statSync(filePath);
const buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
return new Uint8Array(buffer);
} catch (error) {
if ((error as BusinessError).code === 13900002) return null; // File not found
hilog.error(0x0000, 'HuksHelper', `Read file error: ${JSON.stringify(error)}`);
return null;
}
}
}
3.2.4 讲解
- NAPI 桥梁 :
adc_reader.cpp
使用 HDF 的 C 接口读取 ADC,并通过 NAPI 将其暴露给 ArkTS。BUILD.gn
是编译此 C++ 代码为动态库的关键。 - ArkTS 调用 :通过
import adcReader from 'libadc_reader.so'
加载模块,并像调用普通 JS 函数一样调用adcReader.readAdc()
。 - 安全存储 :
HuksHelper
完整实现了加密数据到文件的存取流程,MeteringService
依赖它来安全地管理校准系数。
3.3 车-桩-云协同与数据同步
3.3.1 设计思路
所有充电记录优先存入本地分布式数据库(KvStore),并加入一个"待同步队列"。通过监听网络状态,在网络恢复时自动将队列中的数据续传至云端,实现弱网补偿。
3.3.2 技术选型
- 本地数据库 :
@kit.ArkData
(distributedKVStore) - 网络状态监听 :
@kit.NetworkKit
3.3.3 代码实现
typescript
// model/DataSyncManager.ets
import { distributedKVStore } from '@kit.ArkData';
import { connection } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { OcppManager } from './OcppManager';
import { OcppState } from './OcppManager'; // Import the enum
interface ChargingRecord {
sessionId: string;
startTime: string;
endTime?: string;
energy: number;
synced: boolean;
}
export class DataSyncManager {
private kvStore: distributedKVStore.SingleKVStore | null = null;
private netConnection: connection.NetConnection | null = null;
private readonly SYNC_QUEUE_KEY = 'sync_queue';
private readonly STORE_ID = 'charging_records_db';
private ocppManager: OcppManager;
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext, ocppManager: OcppManager) {
this.context = context;
this.ocppManager = ocppManager;
this.init();
}
private async init(): Promise<void> {
await this.initKVStore();
this.setupNetworkListener();
}
private async initKVStore(): Promise<void> {
try {
const kvStoreConfig: distributedKVStore.KVStoreConfig = { storeId: this.STORE_ID, context: this.context };
const kvStoreManager = distributedKVStore.createKVStoreManager(this.context);
this.kvStore = await kvStoreManager.getKVStore(kvStoreConfig);
hilog.info(0x0000, 'DataSyncManager', 'KVStore initialized.');
} catch (error) {
hilog.error(0x0000, 'DataSyncManager', `KVStore init failed: ${JSON.stringify(error)}`);
}
}
private setupNetworkListener(): void {
this.netConnection = connection.createNetConnection();
this.netConnection.on('netAvailable', () => {
hilog.info(0x0000, 'DataSyncManager', 'Network available. Starting sync.');
this.syncToCloud();
});
this.netConnection.register();
}
public async saveRecord(record: ChargingRecord): Promise<void> {
if (!this.kvStore) return;
try {
await this.kvStore.put(record.sessionId, JSON.stringify(record));
const queueData = await this.kvStore.get(this.SYNC_QUEUE_KEY, '[]');
const queue: string[] = JSON.parse(queueData as string);
if (!queue.includes(record.sessionId)) {
queue.push(record.sessionId);
await this.kvStore.put(this.SYNC_QUEUE_KEY, JSON.stringify(queue));
}
hilog.info(0x0000, 'DataSyncManager', `Record ${record.sessionId} saved.`);
} catch (error) {
hilog.error(0x0000, 'DataSyncManager', `Save record failed: ${JSON.stringify(error)}`);
}
}
public async syncToCloud(): Promise<void> {
if (!this.kvStore || this.ocppManager.getState() !== OcppState.REGISTERED) return;
try {
const queueData = await this.kvStore.get(this.SYNC_QUEUE_KEY, '[]');
let queue: string[] = JSON.parse(queueData as string);
if (queue.length === 0) return;
hilog.info(0x0000, 'DataSyncManager', `Syncing ${queue.length} records...`);
const failedQueue: string[] = [];
for (const sessionId of queue) {
const recordData = await this.kvStore.get(sessionId);
if (recordData) {
const record: ChargingRecord = JSON.parse(recordData as string);
try {
// Assume OcppManager has a method to send data
// this.ocppManager.sendMeterValues(record);
hilog.info(0x0000, 'DataSyncManager', `Successfully synced record ${sessionId}`);
record.synced = true;
await this.kvStore.put(sessionId, JSON.stringify(record));
} catch (sendError) {
hilog.error(0x0000, 'DataSyncManager', `Sync failed for ${sessionId}: ${JSON.stringify(sendError)}`);
failedQueue.push(sessionId);
}
}
}
await this.kvStore.put(this.SYNC_QUEUE_KEY, JSON.stringify(failedQueue));
} catch (error) {
hilog.error(0x0000, 'DataSyncManager', `Sync process error: ${JSON.stringify(error)}`);
}
}
}
3.3.4 讲解
- 依赖注入 :构造函数接收
context
和ocppManager
,降低了模块间的耦合度。 - 本地优先 :
saveRecord
总是先写入本地数据库,并更新同步队列,确保数据不丢失。 - 自动同步 :
setupNetworkListener
监听网络状态,一旦网络可用,自动触发syncToCloud
。
4. 模块整合与启动流程
entry/src/main/ets/entryability/EntryAbility.ets
typescript
// entry/src/main/ets/entryability/EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { OcppManager } from '../model/OcppManager';
import { MeteringService } from '../model/MeteringService';
import { DataSyncManager } from '../model/DataSyncManager';
// 全局服务实例,便于其他模块访问
export let globalOcppManager: OcppManager | null = null;
export let globalMeteringService: MeteringService | null = null;
export let globalDataSyncManager: DataSyncManager | null = null;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'EntryAbility', 'Ability onCreate');
// 1. 初始化核心服务
// OCPP Manager 不依赖其他服务,最先创建
globalOcppManager = new OcppManager();
// Metering Service 依赖 Context
globalMeteringService = new MeteringService(this.context);
// Data Sync Manager 依赖 Context 和 Ocpp Manager
globalDataSyncManager = new DataSyncManager(this.context, globalOcppManager);
hilog.info(0x0000, 'EntryAbility', 'All services initialized.');
}
onDestroy(): void {
hilog.info(0x0000, 'EntryAbility', 'Ability onDestroy');
// 可以在这里执行资源清理
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(0x0000, 'EntryAbility', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'EntryAbility', 'Failed to load content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
});
}
}
整合讲解:
- 服务容器 :
EntryAbility
作为应用入口,充当了简单的服务容器。它负责按依赖顺序创建所有核心服务实例。 - 依赖管理 :
OcppManager
是独立的,首先创建。MeteringService
需要context
来访问文件系统,因此传入this.context
。DataSyncManager
需要context
和OcppManager
实例,因此将两者都传入。
- 全局访问 :通过导出全局变量,使得应用内的其他页面或组件可以方便地访问这些服务实例(例如,UI 页面可以调用
globalOcppManager.startTransaction()
)。在更复杂的应用中,可以考虑使用更高级的依赖注入框架。
待做:
- UI/UX 完善:开发丰富的用户交互界面,如扫码充电、支付、账单展示等。
- OCPP 2.0.1 深度集成:实现 ISO 15118 插充通信、智能充电等高级特性。
- 分布式能力扩展:探索与手机、智能家居等设备的分布式协同,打造智慧充电生态。