Compare commits
3 Commits
e1fb43d57b
...
dee947ce3e
| Author | SHA1 | Date | |
|---|---|---|---|
| dee947ce3e | |||
| 4d940e2cd4 | |||
| 8371b2a76b |
6
apps/csms/drizzle/0006_spooky_skin.sql
Normal file
6
apps/csms/drizzle/0006_spooky_skin.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE "system_setting" (
|
||||
"key" varchar(64) PRIMARY KEY NOT NULL,
|
||||
"value" jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
1987
apps/csms/drizzle/meta/0006_snapshot.json
Normal file
1987
apps/csms/drizzle/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,13 @@
|
||||
"when": 1773678571220,
|
||||
"tag": "0005_peaceful_anthem",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1773682931777,
|
||||
"tag": "0006_spooky_skin",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './auth-schema.ts'
|
||||
export * from './ocpp-schema.ts'
|
||||
export * from './tariff-schema.ts'
|
||||
export * from './settings-schema.ts'
|
||||
|
||||
15
apps/csms/src/db/settings-schema.ts
Normal file
15
apps/csms/src/db/settings-schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { jsonb, pgTable, timestamp, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
/**
|
||||
* 系统参数配置(按模块 key 存储)
|
||||
* 例如:key=ocpp16j, value={ heartbeatInterval: 60 }
|
||||
*/
|
||||
export const systemSetting = pgTable("system_setting", {
|
||||
key: varchar("key", { length: 64 }).primaryKey(),
|
||||
value: jsonb("value").notNull().$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import idTagRoutes from './routes/id-tags.ts'
|
||||
import userRoutes from './routes/users.ts'
|
||||
import setupRoutes from './routes/setup.ts'
|
||||
import tariffRoutes from './routes/tariff.ts'
|
||||
import settingsRoutes from './routes/settings.ts'
|
||||
|
||||
import type { HonoEnv } from './types/hono.ts'
|
||||
|
||||
@@ -64,6 +65,7 @@ app.route('/api/id-tags', idTagRoutes)
|
||||
app.route('/api/users', userRoutes)
|
||||
app.route('/api/setup', setupRoutes)
|
||||
app.route('/api/tariff', tariffRoutes)
|
||||
app.route('/api/settings', settingsRoutes)
|
||||
|
||||
app.get('/api', (c) => {
|
||||
const user = c.get('user')
|
||||
|
||||
45
apps/csms/src/lib/system-settings.ts
Normal file
45
apps/csms/src/lib/system-settings.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { systemSetting } from "@/db/schema.js";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
|
||||
export const SETTINGS_KEY_OCPP16J = "ocpp16j";
|
||||
const DEFAULT_HEARTBEAT_INTERVAL = 60;
|
||||
const MIN_HEARTBEAT_INTERVAL = 10;
|
||||
const MAX_HEARTBEAT_INTERVAL = 86400;
|
||||
|
||||
export type Ocpp16jSettings = {
|
||||
heartbeatInterval: number;
|
||||
};
|
||||
|
||||
export type SettingsPayload = {
|
||||
ocpp16j: Ocpp16jSettings;
|
||||
};
|
||||
|
||||
export function sanitizeHeartbeatInterval(raw: unknown): number {
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return DEFAULT_HEARTBEAT_INTERVAL;
|
||||
}
|
||||
const n = Math.round(raw);
|
||||
if (n < MIN_HEARTBEAT_INTERVAL) return MIN_HEARTBEAT_INTERVAL;
|
||||
if (n > MAX_HEARTBEAT_INTERVAL) return MAX_HEARTBEAT_INTERVAL;
|
||||
return n;
|
||||
}
|
||||
|
||||
export function getDefaultHeartbeatInterval(): number {
|
||||
return DEFAULT_HEARTBEAT_INTERVAL;
|
||||
}
|
||||
|
||||
export async function getOcpp16jSettings(): Promise<Ocpp16jSettings> {
|
||||
const db = useDrizzle();
|
||||
const [ocpp16jRow] = await db
|
||||
.select()
|
||||
.from(systemSetting)
|
||||
.where(eq(systemSetting.key, SETTINGS_KEY_OCPP16J))
|
||||
.limit(1);
|
||||
|
||||
const ocpp16jRaw = (ocpp16jRow?.value ?? {}) as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
heartbeatInterval: sanitizeHeartbeatInterval(ocpp16jRaw.heartbeatInterval),
|
||||
};
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useDrizzle } from '@/lib/db.js'
|
||||
import dayjs from 'dayjs'
|
||||
import { chargePoint } from '@/db/schema.js'
|
||||
import { getOcpp16jSettings } from '@/lib/system-settings.js'
|
||||
import type {
|
||||
BootNotificationRequest,
|
||||
BootNotificationResponse,
|
||||
OcppConnectionContext,
|
||||
} from '../types.ts'
|
||||
|
||||
const DEFAULT_HEARTBEAT_INTERVAL = 60
|
||||
|
||||
export async function handleBootNotification(
|
||||
payload: BootNotificationRequest,
|
||||
ctx: OcppConnectionContext,
|
||||
): Promise<BootNotificationResponse> {
|
||||
const db = useDrizzle()
|
||||
const { heartbeatInterval } = await getOcpp16jSettings()
|
||||
|
||||
const [cp] = await db
|
||||
.insert(chargePoint)
|
||||
@@ -30,7 +30,7 @@ export async function handleBootNotification(
|
||||
meterSerialNumber: payload.meterSerialNumber ?? null,
|
||||
// New, unknown devices start as Pending — admin must manually accept them
|
||||
registrationStatus: 'Pending',
|
||||
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
||||
heartbeatInterval,
|
||||
lastBootNotificationAt: dayjs().toDate(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
@@ -45,7 +45,7 @@ export async function handleBootNotification(
|
||||
meterType: payload.meterType ?? null,
|
||||
meterSerialNumber: payload.meterSerialNumber ?? null,
|
||||
// Do NOT override registrationStatus — preserve whatever the admin set
|
||||
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
||||
heartbeatInterval,
|
||||
lastBootNotificationAt: dayjs().toDate(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
},
|
||||
@@ -59,7 +59,7 @@ export async function handleBootNotification(
|
||||
|
||||
return {
|
||||
currentTime: dayjs().toISOString(),
|
||||
interval: DEFAULT_HEARTBEAT_INTERVAL,
|
||||
interval: heartbeatInterval,
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
65
apps/csms/src/routes/settings.ts
Normal file
65
apps/csms/src/routes/settings.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Hono } from "hono";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { systemSetting } from "@/db/schema.js";
|
||||
import type { HonoEnv } from "@/types/hono.ts";
|
||||
import {
|
||||
SETTINGS_KEY_OCPP16J,
|
||||
getOcpp16jSettings,
|
||||
sanitizeHeartbeatInterval,
|
||||
type SettingsPayload,
|
||||
} from "@/lib/system-settings.js";
|
||||
|
||||
const app = new Hono<HonoEnv>();
|
||||
|
||||
app.get("/", async (c) => {
|
||||
const payload: SettingsPayload = {
|
||||
ocpp16j: await getOcpp16jSettings(),
|
||||
};
|
||||
|
||||
return c.json(payload);
|
||||
});
|
||||
|
||||
app.put("/", async (c) => {
|
||||
const currentUser = c.get("user");
|
||||
if (currentUser?.role !== "admin") {
|
||||
return c.json({ error: "Forbidden" }, 403);
|
||||
}
|
||||
|
||||
let body: Partial<SettingsPayload>;
|
||||
try {
|
||||
body = await c.req.json<Partial<SettingsPayload>>();
|
||||
} catch {
|
||||
return c.json({ error: "Invalid JSON" }, 400);
|
||||
}
|
||||
|
||||
if (!body.ocpp16j) {
|
||||
return c.json({ error: "Missing ocpp16j settings" }, 400);
|
||||
}
|
||||
|
||||
const heartbeatInterval = sanitizeHeartbeatInterval(body.ocpp16j.heartbeatInterval);
|
||||
|
||||
const db = useDrizzle();
|
||||
await db
|
||||
.insert(systemSetting)
|
||||
.values({
|
||||
key: SETTINGS_KEY_OCPP16J,
|
||||
value: { heartbeatInterval },
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSetting.key,
|
||||
set: {
|
||||
value: { heartbeatInterval },
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const payload: SettingsPayload = {
|
||||
ocpp16j: {
|
||||
heartbeatInterval,
|
||||
},
|
||||
};
|
||||
|
||||
return c.json(payload);
|
||||
});
|
||||
|
||||
export default app;
|
||||
161
apps/web/app/dashboard/settings/parameters/page.tsx
Normal file
161
apps/web/app/dashboard/settings/parameters/page.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Alert, Button, Input, Label, Spinner, TextField, toast } from "@heroui/react";
|
||||
import { Gear, Lock } from "@gravity-ui/icons";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { api, type SystemSettings } from "@/lib/api";
|
||||
|
||||
const MIN_HEARTBEAT = 10;
|
||||
const MAX_HEARTBEAT = 86400;
|
||||
const DEFAULT_HEARTBEAT = 60;
|
||||
|
||||
export default function ParametersSettingsPage() {
|
||||
const { data: session } = useSession();
|
||||
const isAdmin = session?.user?.role === "admin";
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: settings, isLoading } = useQuery({
|
||||
queryKey: ["system-settings"],
|
||||
queryFn: () => api.settings.get(),
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const [heartbeatInterval, setHeartbeatInterval] = useState(String(DEFAULT_HEARTBEAT));
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings) return;
|
||||
setHeartbeatInterval(String(settings.ocpp16j.heartbeatInterval));
|
||||
}, [settings]);
|
||||
|
||||
const parsedHeartbeat = useMemo(() => {
|
||||
const n = Number(heartbeatInterval);
|
||||
if (!Number.isFinite(n)) return null;
|
||||
return Math.round(n);
|
||||
}, [heartbeatInterval]);
|
||||
|
||||
const heartbeatError =
|
||||
parsedHeartbeat === null
|
||||
? "请输入有效数字"
|
||||
: parsedHeartbeat < MIN_HEARTBEAT || parsedHeartbeat > MAX_HEARTBEAT
|
||||
? `范围应为 ${MIN_HEARTBEAT} - ${MAX_HEARTBEAT} 秒`
|
||||
: "";
|
||||
|
||||
const isDirty = settings
|
||||
? Number(heartbeatInterval) !== settings.ocpp16j.heartbeatInterval
|
||||
: false;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (parsedHeartbeat === null) {
|
||||
toast.warning("请输入有效的心跳间隔");
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedHeartbeat < MIN_HEARTBEAT || parsedHeartbeat > MAX_HEARTBEAT) {
|
||||
toast.warning(`心跳间隔范围应为 ${MIN_HEARTBEAT}-${MAX_HEARTBEAT} 秒`);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: SystemSettings = {
|
||||
ocpp16j: { heartbeatInterval: parsedHeartbeat },
|
||||
};
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.settings.put(payload);
|
||||
toast.success("参数配置已保存");
|
||||
queryClient.invalidateQueries({ queryKey: ["system-settings"] });
|
||||
} catch {
|
||||
toast.warning("保存失败,请稍后重试");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-full bg-warning/10">
|
||||
<Lock className="size-6 text-warning" />
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-foreground">需要管理员权限</p>
|
||||
<p className="mt-1 text-sm text-muted">参数配置仅对管理员开放</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Gear className="size-5 text-foreground" />
|
||||
<h1 className="text-xl font-semibold text-foreground">参数配置</h1>
|
||||
</div>
|
||||
<p className="mt-0.5 text-sm text-muted">
|
||||
按功能模块管理系统参数,变更将影响后续设备交互行为
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-surface-secondary">
|
||||
<div className="flex items-center gap-3 border-b border-border px-5 py-4">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-accent/10">
|
||||
<Gear className="size-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">OCPP 1.6J</p>
|
||||
<p className="text-xs text-muted">协议层行为参数</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 px-5 py-4">
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">心跳间隔</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={String(MIN_HEARTBEAT)}
|
||||
max={String(MAX_HEARTBEAT)}
|
||||
step="1"
|
||||
value={heartbeatInterval}
|
||||
onChange={(e) => setHeartbeatInterval(e.target.value)}
|
||||
placeholder="60"
|
||||
/>
|
||||
</TextField>
|
||||
|
||||
<p className="text-xs text-muted">
|
||||
单位:秒。该值会用于 BootNotification.conf 的 interval
|
||||
字段,并下发给充电桩默认心跳参数。
|
||||
</p>
|
||||
|
||||
{!!heartbeatError && (
|
||||
<Alert status="warning">
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Description>{heartbeatError}</Alert.Description>
|
||||
</Alert.Content>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
isDisabled={saving || !isDirty || !!heartbeatError}
|
||||
onPress={handleSave}
|
||||
>
|
||||
{saving ? <Spinner size="sm" color="current" /> : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ const navItems = [
|
||||
|
||||
const settingsItems = [
|
||||
{ href: "/dashboard/settings/user", label: "账号设置", icon: UserCog, adminOnly: false },
|
||||
{ href: "/dashboard/settings/parameters", label: "参数配置", icon: Gear, adminOnly: true },
|
||||
];
|
||||
|
||||
function NavContent({
|
||||
|
||||
@@ -202,6 +202,14 @@ export type TariffConfig = {
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type Ocpp16jSettings = {
|
||||
heartbeatInterval: number;
|
||||
};
|
||||
|
||||
export type SystemSettings = {
|
||||
ocpp16j: Ocpp16jSettings;
|
||||
};
|
||||
|
||||
// ── API functions ──────────────────────────────────────────────────────────
|
||||
|
||||
export type ChartRange = "30d" | "7d" | "24h";
|
||||
@@ -349,4 +357,9 @@ export const api = {
|
||||
put: (data: { slots: TariffSlot[]; prices: Record<PriceTier, TierPricing> }) =>
|
||||
apiFetch<TariffConfig>("/api/tariff", { method: "PUT", body: JSON.stringify(data) }),
|
||||
},
|
||||
settings: {
|
||||
get: () => apiFetch<SystemSettings>("/api/settings"),
|
||||
put: (data: SystemSettings) =>
|
||||
apiFetch<SystemSettings>("/api/settings", { method: "PUT", body: JSON.stringify(data) }),
|
||||
},
|
||||
};
|
||||
|
||||
37
hardware/firmware/lib/MicroOcppMongoose/library.json
Normal file
37
hardware/firmware/lib/MicroOcppMongoose/library.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "MicroOcppMongoose",
|
||||
"version": "1.2.0",
|
||||
"description": "Mongoose Adapter for the MicroOCPP Client",
|
||||
"keywords": "OCPP, 1.6, OCPP 1.6, OCPP 2.0.1, Smart Energy, Smart Charging, client, ESP8266, ESP32, Arduino, EVSE, Charge Point, Mongoose",
|
||||
"repository":
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/matth-x/MicroOcppMongoose/"
|
||||
},
|
||||
"authors":
|
||||
[
|
||||
{
|
||||
"name": "Matthias Akstaller",
|
||||
"url": "https://www.micro-ocpp.com",
|
||||
"maintainer": true
|
||||
}
|
||||
],
|
||||
"license": "GPL-3.0",
|
||||
"homepage": "https://www.micro-ocpp.com",
|
||||
"frameworks": "arduino,espidf",
|
||||
"platforms": "espressif8266, espressif32",
|
||||
"export": {
|
||||
"include":
|
||||
[
|
||||
"src/MicroOcppMongooseClient_c.cpp",
|
||||
"src/MicroOcppMongooseClient_c.h",
|
||||
"src/MicroOcppMongooseClient.cpp",
|
||||
"src/MicroOcppMongooseClient.h",
|
||||
"CHANGELOG.md",
|
||||
"CMakeLists.txt",
|
||||
"library.json",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
// matth-x/MicroOcppMongoose
|
||||
// Copyright Matthias Akstaller 2019 - 2024
|
||||
// GPL-3.0 License (see LICENSE)
|
||||
|
||||
#include "MicroOcppMongooseClient.h"
|
||||
#include <MicroOcpp/Core/Configuration.h>
|
||||
#include <MicroOcpp/Debug.h>
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
#include <MicroOcpp/Model/Variables/VariableContainer.h>
|
||||
#endif
|
||||
|
||||
#define DEBUG_MSG_INTERVAL 5000UL
|
||||
#define WS_UNRESPONSIVE_THRESHOLD_MS 15000UL
|
||||
|
||||
#define MO_MG_V614 614
|
||||
#define MO_MG_V708 708
|
||||
#define MO_MG_V713 713
|
||||
#define MO_MG_V714 714
|
||||
#define MO_MG_V715 715
|
||||
|
||||
#ifndef MO_MG_USE_VERSION
|
||||
#if defined(MO_MG_VERSION_614)
|
||||
#define MO_MG_USE_VERSION MO_MG_V614
|
||||
#else
|
||||
#define MO_MG_USE_VERSION MO_MG_V708
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
#define MO_MG_F_IS_MOcppMongooseClient MG_F_USER_2
|
||||
#endif
|
||||
|
||||
namespace MicroOcpp {
|
||||
bool validateAuthorizationKeyHex(const char *auth_key_hex);
|
||||
}
|
||||
|
||||
using namespace MicroOcpp;
|
||||
|
||||
#if MO_MG_USE_VERSION <= MO_MG_V708
|
||||
void ws_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data);
|
||||
#else
|
||||
void ws_cb(struct mg_connection *c, int ev, void *ev_data);
|
||||
#endif
|
||||
|
||||
MOcppMongooseClient::MOcppMongooseClient(struct mg_mgr *mgr,
|
||||
const char *backend_url_factory,
|
||||
const char *charge_box_id_factory,
|
||||
unsigned char *auth_key_factory, size_t auth_key_factory_len,
|
||||
const char *ca_certificate,
|
||||
std::shared_ptr<FilesystemAdapter> filesystem,
|
||||
ProtocolVersion protocolVersion) : mgr(mgr), protocolVersion(protocolVersion) {
|
||||
|
||||
bool readonly;
|
||||
|
||||
if (filesystem) {
|
||||
configuration_init(filesystem);
|
||||
|
||||
//all credentials are persistent over reboots
|
||||
readonly = false;
|
||||
} else {
|
||||
//make the credentials non-persistent
|
||||
MO_DBG_WARN("Credentials non-persistent. Use MicroOcpp::makeDefaultFilesystemAdapter(...) for persistency");
|
||||
readonly = true;
|
||||
}
|
||||
|
||||
if (auth_key_factory_len > MO_AUTHKEY_LEN_MAX) {
|
||||
MO_DBG_WARN("auth_key_factory too long - will be cropped");
|
||||
auth_key_factory_len = MO_AUTHKEY_LEN_MAX;
|
||||
}
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
if (protocolVersion.major == 2) {
|
||||
websocketSettings = std::unique_ptr<VariableContainerOwning>(new VariableContainerOwning());
|
||||
if (filesystem) {
|
||||
websocketSettings->enablePersistency(filesystem, MO_WSCONN_FN_V201);
|
||||
}
|
||||
|
||||
auto csmsUrl = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual);
|
||||
csmsUrl->setComponentId("SecurityCtrlr");
|
||||
csmsUrl->setName("CsmsUrl");
|
||||
csmsUrl->setString(backend_url_factory ? backend_url_factory : "");
|
||||
csmsUrl->setPersistent();
|
||||
v201csmsUrlString = csmsUrl.get();
|
||||
websocketSettings->add(std::move(csmsUrl));
|
||||
|
||||
auto identity = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual);
|
||||
identity->setComponentId("SecurityCtrlr");
|
||||
identity->setName("Identity");
|
||||
identity->setString(charge_box_id_factory ? charge_box_id_factory : "");
|
||||
identity->setPersistent();
|
||||
v201identityString = identity.get();
|
||||
websocketSettings->add(std::move(identity));
|
||||
|
||||
auto basicAuthPassword = makeVariable(Variable::InternalDataType::String, Variable::AttributeType::Actual);
|
||||
basicAuthPassword->setComponentId("SecurityCtrlr");
|
||||
basicAuthPassword->setName("BasicAuthPassword");
|
||||
char basicAuthPasswordVal [MO_AUTHKEY_LEN_MAX + 1];
|
||||
snprintf(basicAuthPasswordVal, sizeof(basicAuthPasswordVal), "%.*s", (int)auth_key_factory_len, auth_key_factory ? (const char*)auth_key_factory : "");
|
||||
basicAuthPassword->setString(basicAuthPasswordVal);
|
||||
basicAuthPassword->setPersistent();
|
||||
v201basicAuthPasswordString = basicAuthPassword.get();
|
||||
websocketSettings->add(std::move(basicAuthPassword));
|
||||
|
||||
websocketSettings->load(); //if settings on flash already exist, this overwrites factory defaults
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
setting_backend_url_str = declareConfiguration<const char*>(
|
||||
MO_CONFIG_EXT_PREFIX "BackendUrl", backend_url_factory, MO_WSCONN_FN, readonly, true);
|
||||
setting_cb_id_str = declareConfiguration<const char*>(
|
||||
MO_CONFIG_EXT_PREFIX "ChargeBoxId", charge_box_id_factory, MO_WSCONN_FN, readonly, true);
|
||||
|
||||
char auth_key_hex [2 * MO_AUTHKEY_LEN_MAX + 1];
|
||||
auth_key_hex[0] = '\0';
|
||||
if (auth_key_factory) {
|
||||
for (size_t i = 0; i < auth_key_factory_len; i++) {
|
||||
snprintf(auth_key_hex + 2 * i, 3, "%02X", auth_key_factory[i]);
|
||||
}
|
||||
}
|
||||
setting_auth_key_hex_str = declareConfiguration<const char*>(
|
||||
"AuthorizationKey", auth_key_hex, MO_WSCONN_FN, readonly, true);
|
||||
registerConfigurationValidator("AuthorizationKey", validateAuthorizationKeyHex);
|
||||
}
|
||||
|
||||
ws_ping_interval_int = declareConfiguration<int>(
|
||||
"WebSocketPingInterval", 5, MO_WSCONN_FN);
|
||||
reconnect_interval_int = declareConfiguration<int>(
|
||||
MO_CONFIG_EXT_PREFIX "ReconnectInterval", 10, MO_WSCONN_FN);
|
||||
stale_timeout_int = declareConfiguration<int>(
|
||||
MO_CONFIG_EXT_PREFIX "StaleTimeout", 300, MO_WSCONN_FN);
|
||||
|
||||
configuration_load(MO_WSCONN_FN); //load configs with values stored on flash
|
||||
|
||||
ca_cert = ca_certificate;
|
||||
|
||||
reloadConfigs(); //load WS creds with configs values
|
||||
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
MO_DBG_DEBUG("use MG version %s (tested with 6.14)", MG_VERSION);
|
||||
#elif MO_MG_USE_VERSION == MO_MG_V708
|
||||
MO_DBG_DEBUG("use MG version %s (tested with 7.8)", MG_VERSION);
|
||||
#elif MO_MG_USE_VERSION == MO_MG_V713
|
||||
MO_DBG_DEBUG("use MG version %s (tested with 7.13)", MG_VERSION);
|
||||
#endif
|
||||
|
||||
maintainWsConn();
|
||||
}
|
||||
|
||||
MOcppMongooseClient::MOcppMongooseClient(struct mg_mgr *mgr,
|
||||
const char *backend_url_factory,
|
||||
const char *charge_box_id_factory,
|
||||
const char *auth_key_factory,
|
||||
const char *ca_certificate,
|
||||
std::shared_ptr<FilesystemAdapter> filesystem,
|
||||
ProtocolVersion protocolVersion) :
|
||||
|
||||
MOcppMongooseClient(mgr,
|
||||
backend_url_factory,
|
||||
charge_box_id_factory,
|
||||
(unsigned char *)auth_key_factory, auth_key_factory ? strlen(auth_key_factory) : 0,
|
||||
ca_certificate,
|
||||
filesystem,
|
||||
protocolVersion) {
|
||||
|
||||
}
|
||||
|
||||
MOcppMongooseClient::~MOcppMongooseClient() {
|
||||
MO_DBG_DEBUG("destruct MOcppMongooseClient");
|
||||
if (websocket) {
|
||||
reconnect(); //close WS connection, won't be reopened
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
websocket->flags &= ~MO_MG_F_IS_MOcppMongooseClient;
|
||||
websocket->user_data = nullptr;
|
||||
#else
|
||||
websocket->fn_data = nullptr;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::loop() {
|
||||
maintainWsConn();
|
||||
}
|
||||
|
||||
bool MOcppMongooseClient::sendTXT(const char *msg, size_t length) {
|
||||
if (!websocket || !isConnectionOpen()) {
|
||||
return false;
|
||||
}
|
||||
size_t sent;
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
if (websocket->send_mbuf.len > 0) {
|
||||
sent = 0;
|
||||
return false;
|
||||
} else {
|
||||
mg_send_websocket_frame(websocket, WEBSOCKET_OP_TEXT, msg, length);
|
||||
sent = length;
|
||||
}
|
||||
#else
|
||||
sent = mg_ws_send(websocket, msg, length, WEBSOCKET_OP_TEXT);
|
||||
#endif
|
||||
if (sent < length) {
|
||||
MO_DBG_WARN("mg_ws_send did only accept %zu out of %zu bytes", sent, length);
|
||||
//flush broken package and wait for next retry
|
||||
(void)0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::maintainWsConn() {
|
||||
if (mocpp_tick_ms() - last_status_dbg_msg >= DEBUG_MSG_INTERVAL) {
|
||||
last_status_dbg_msg = mocpp_tick_ms();
|
||||
|
||||
//WS successfully connected?
|
||||
if (!isConnectionOpen()) {
|
||||
MO_DBG_DEBUG("WS unconnected");
|
||||
} else if (mocpp_tick_ms() - last_recv >= (ws_ping_interval_int && ws_ping_interval_int->getInt() > 0 ? (ws_ping_interval_int->getInt() * 1000UL) : 0UL) + WS_UNRESPONSIVE_THRESHOLD_MS) {
|
||||
//WS connected but unresponsive
|
||||
MO_DBG_DEBUG("WS unresponsive");
|
||||
}
|
||||
}
|
||||
|
||||
if (websocket && isConnectionOpen() &&
|
||||
stale_timeout_int && stale_timeout_int->getInt() > 0 && mocpp_tick_ms() - last_recv >= (stale_timeout_int->getInt() * 1000UL)) {
|
||||
MO_DBG_INFO("connection %s -- stale, reconnect", url.c_str());
|
||||
reconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (websocket && isConnectionOpen() &&
|
||||
ws_ping_interval_int && ws_ping_interval_int->getInt() > 0 && mocpp_tick_ms() - last_hb >= (ws_ping_interval_int->getInt() * 1000UL)) {
|
||||
last_hb = mocpp_tick_ms();
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
mg_send_websocket_frame(websocket, WEBSOCKET_OP_PING, "", 0);
|
||||
#else
|
||||
mg_ws_send(websocket, "", 0, WEBSOCKET_OP_PING);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (websocket != nullptr) { //connection pointer != nullptr means that the socket is still open
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.empty()) {
|
||||
//cannot open OCPP connection: credentials missing
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnect_interval_int && reconnect_interval_int->getInt() > 0 && mocpp_tick_ms() - last_reconnection_attempt < (reconnect_interval_int->getInt() * 1000UL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MO_DBG_DEBUG("(re-)connect to %s", url.c_str());
|
||||
|
||||
last_reconnection_attempt = mocpp_tick_ms();
|
||||
|
||||
/*
|
||||
* determine auth token
|
||||
*/
|
||||
|
||||
std::string basic_auth64;
|
||||
|
||||
if (auth_key_len > 0) {
|
||||
|
||||
#if MO_DBG_LEVEL >= MO_DL_DEBUG
|
||||
{
|
||||
char auth_key_hex [2 * MO_AUTHKEY_LEN_MAX + 1];
|
||||
auth_key_hex[0] = '\0';
|
||||
for (size_t i = 0; i < auth_key_len; i++) {
|
||||
snprintf(auth_key_hex + 2 * i, 3, "%02X", auth_key[i]);
|
||||
}
|
||||
MO_DBG_DEBUG("auth Token=%s:%s (key will be converted to non-hex)", cb_id.c_str(), auth_key_hex);
|
||||
}
|
||||
#endif //MO_DBG_LEVEL >= MO_DL_DEBUG
|
||||
|
||||
unsigned char *token = new unsigned char[cb_id.length() + 1 + auth_key_len]; //cb_id:auth_key
|
||||
if (!token) {
|
||||
//OOM
|
||||
return;
|
||||
}
|
||||
size_t len = 0;
|
||||
memcpy(token, cb_id.c_str(), cb_id.length());
|
||||
len += cb_id.length();
|
||||
token[len++] = (unsigned char) ':';
|
||||
memcpy(token + len, auth_key, auth_key_len);
|
||||
len += auth_key_len;
|
||||
|
||||
int base64_length = ((len + 2) / 3) * 4; //3 bytes base256 get encoded into 4 bytes base64. --> base64_len = ceil(len/3) * 4
|
||||
char *base64 = new char[base64_length + 1];
|
||||
if (!base64) {
|
||||
//OOM
|
||||
delete[] token;
|
||||
return;
|
||||
}
|
||||
|
||||
// mg_base64_encode() places a null terminator automatically, because the output is a c-string
|
||||
#if MO_MG_USE_VERSION <= MO_MG_V708
|
||||
mg_base64_encode(token, len, base64);
|
||||
#else
|
||||
mg_base64_encode(token, len, base64, base64_length + 1);
|
||||
#endif
|
||||
delete[] token;
|
||||
|
||||
MO_DBG_DEBUG("auth64 len=%u, auth64 Token=%s", base64_length, base64);
|
||||
|
||||
basic_auth64 = &base64[0];
|
||||
|
||||
delete[] base64;
|
||||
} else {
|
||||
MO_DBG_DEBUG("no authentication");
|
||||
(void) 0;
|
||||
}
|
||||
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
|
||||
struct mg_connect_opts opts;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
|
||||
const char *ca_string = ca_cert ? ca_cert : "*"; //"*" enables TLS but disables CA verification
|
||||
|
||||
//Check if SSL is disabled, i.e. if URL starts with "ws:"
|
||||
if (url.length() >= strlen("ws:") &&
|
||||
tolower(url.c_str()[0]) == 'w' &&
|
||||
tolower(url.c_str()[1]) == 's' &&
|
||||
url.c_str()[2] == ':') {
|
||||
//yes, disable SSL
|
||||
ca_string = nullptr;
|
||||
MO_DBG_WARN("Insecure connection (WS)");
|
||||
}
|
||||
|
||||
opts.ssl_ca_cert = ca_string;
|
||||
|
||||
char extra_headers [128] = {'\0'};
|
||||
|
||||
if (!basic_auth64.empty()) {
|
||||
auto ret = snprintf(extra_headers, 128, "Authorization: Basic %s\r\n", basic_auth64.c_str());
|
||||
if (ret < 0 || ret >= 128) {
|
||||
MO_DBG_ERR("Basic Authentication failed: %d", ret);
|
||||
(void)0;
|
||||
}
|
||||
}
|
||||
|
||||
websocket = mg_connect_ws_opt(
|
||||
mgr,
|
||||
ws_cb,
|
||||
this,
|
||||
opts,
|
||||
url.c_str(),
|
||||
protocolVersion.major == 2 ? "ocpp2.0.1" : "ocpp1.6",
|
||||
*extra_headers ? extra_headers : nullptr);
|
||||
|
||||
if (websocket) {
|
||||
websocket->flags |= MO_MG_F_IS_MOcppMongooseClient;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
websocket = mg_ws_connect(
|
||||
mgr,
|
||||
url.c_str(),
|
||||
ws_cb,
|
||||
this,
|
||||
"Sec-WebSocket-Protocol: %s%s%s\r\n",
|
||||
protocolVersion.major == 2 ? "ocpp2.0.1" : "ocpp1.6",
|
||||
basic_auth64.empty() ? "" : "\r\nAuthorization: Basic ",
|
||||
basic_auth64.empty() ? "" : basic_auth64.c_str()); // Create client
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::reconnect() {
|
||||
if (!websocket) {
|
||||
return;
|
||||
}
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
if (!connection_closing) {
|
||||
const char *msg = "socket closed by client";
|
||||
mg_send_websocket_frame(websocket, WEBSOCKET_OP_CLOSE, msg, strlen(msg));
|
||||
}
|
||||
#else
|
||||
websocket->is_closing = 1; //Mongoose will close the socket and the following maintainWsConn() call will open it again
|
||||
#endif
|
||||
setConnectionOpen(false);
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::setBackendUrl(const char *backend_url_cstr) {
|
||||
if (!backend_url_cstr) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
if (protocolVersion.major == 2) {
|
||||
if (v201csmsUrlString) {
|
||||
v201csmsUrlString->setString(backend_url_cstr);
|
||||
websocketSettings->commit();
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (setting_backend_url_str) {
|
||||
setting_backend_url_str->setString(backend_url_cstr);
|
||||
configuration_save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::setChargeBoxId(const char *cb_id_cstr) {
|
||||
if (!cb_id_cstr) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
if (protocolVersion.major == 2) {
|
||||
if (v201identityString) {
|
||||
v201identityString->setString(cb_id_cstr);
|
||||
websocketSettings->commit();
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (setting_cb_id_str) {
|
||||
setting_cb_id_str->setString(cb_id_cstr);
|
||||
configuration_save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::setAuthKey(const char *auth_key_cstr) {
|
||||
if (!auth_key_cstr) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
return setAuthKey((const unsigned char*)auth_key_cstr, strlen(auth_key_cstr));
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::setAuthKey(const unsigned char *auth_key, size_t len) {
|
||||
if (!auth_key || len > MO_AUTHKEY_LEN_MAX) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
if (protocolVersion.major == 2) {
|
||||
char basicAuthPassword [MO_AUTHKEY_LEN_MAX + 1];
|
||||
snprintf(basicAuthPassword, sizeof(basicAuthPassword), "%.*s", (int)len, auth_key ? (const char*)auth_key : "");
|
||||
if (v201basicAuthPasswordString) {
|
||||
v201basicAuthPasswordString->setString(basicAuthPassword);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
char auth_key_hex [2 * MO_AUTHKEY_LEN_MAX + 1];
|
||||
auth_key_hex[0] = '\0';
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
snprintf(auth_key_hex + 2 * i, 3, "%02X", auth_key[i]);
|
||||
}
|
||||
if (setting_auth_key_hex_str) {
|
||||
setting_auth_key_hex_str->setString(auth_key_hex);
|
||||
configuration_save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::setCaCert(const char *ca_cert_cstr) {
|
||||
ca_cert = ca_cert_cstr; //updated ca_cert takes immediate effect
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::reloadConfigs() {
|
||||
|
||||
reconnect(); //closes WS connection; will be reopened in next maintainWsConn execution
|
||||
|
||||
/*
|
||||
* reload WS credentials from configs
|
||||
*/
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
if (protocolVersion.major == 2) {
|
||||
if (v201csmsUrlString) {
|
||||
backend_url = v201csmsUrlString->getString();
|
||||
}
|
||||
|
||||
if (v201identityString) {
|
||||
cb_id = v201identityString->getString();
|
||||
}
|
||||
|
||||
if (v201basicAuthPasswordString) {
|
||||
snprintf((char*)auth_key, sizeof(auth_key), "%s", v201basicAuthPasswordString->getString());
|
||||
auth_key_len = strlen((char*)auth_key);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (setting_backend_url_str) {
|
||||
backend_url = setting_backend_url_str->getString();
|
||||
}
|
||||
|
||||
if (setting_cb_id_str) {
|
||||
cb_id = setting_cb_id_str->getString();
|
||||
}
|
||||
|
||||
if (setting_auth_key_hex_str) {
|
||||
auto auth_key_hex = setting_auth_key_hex_str->getString();
|
||||
auto auth_key_hex_len = strlen(setting_auth_key_hex_str->getString());
|
||||
if (!validateAuthorizationKeyHex(auth_key_hex)) {
|
||||
MO_DBG_ERR("AuthorizationKey stored with format error. Disable Basic Auth");
|
||||
auth_key_hex_len = 0;
|
||||
}
|
||||
|
||||
auth_key_len = auth_key_hex_len / 2;
|
||||
|
||||
#if MO_MG_VERSION_614
|
||||
cs_from_hex((char*)auth_key, auth_key_hex, auth_key_hex_len);
|
||||
#elif MO_MG_USE_VERSION <= MO_MG_V713
|
||||
mg_unhex(auth_key_hex, auth_key_hex_len, auth_key);
|
||||
#else
|
||||
for (size_t i = 0; i < auth_key_len; i++) {
|
||||
mg_str_to_num(mg_str_n(auth_key_hex + 2*i, 2), 16, auth_key + i, sizeof(uint8_t));
|
||||
}
|
||||
#endif
|
||||
|
||||
auth_key[auth_key_len] = '\0'; //need null-termination as long as deprecated `const char *getAuthKey()` exists
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* determine new URL with updated WS credentials
|
||||
*/
|
||||
|
||||
url.clear();
|
||||
|
||||
if (backend_url.empty()) {
|
||||
MO_DBG_DEBUG("empty URL closes connection");
|
||||
return;
|
||||
}
|
||||
|
||||
url = backend_url;
|
||||
|
||||
if (url.back() != '/' && !cb_id.empty()) {
|
||||
url.append("/");
|
||||
}
|
||||
url.append(cb_id);
|
||||
}
|
||||
|
||||
int MOcppMongooseClient::printAuthKey(unsigned char *buf, size_t size) {
|
||||
if (!buf || size < auth_key_len) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(buf, auth_key, auth_key_len);
|
||||
return (int)auth_key_len;
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::setConnectionOpen(bool open) {
|
||||
if (open) {
|
||||
connection_established = true;
|
||||
last_connection_established = mocpp_tick_ms();
|
||||
} else {
|
||||
connection_closing = true;
|
||||
}
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::cleanConnection() {
|
||||
connection_established = false;
|
||||
connection_closing = false;
|
||||
websocket = nullptr;
|
||||
}
|
||||
|
||||
void MOcppMongooseClient::updateRcvTimer() {
|
||||
last_recv = mocpp_tick_ms();
|
||||
}
|
||||
|
||||
unsigned long MOcppMongooseClient::getLastRecv() {
|
||||
return last_recv;
|
||||
}
|
||||
|
||||
unsigned long MOcppMongooseClient::getLastConnected() {
|
||||
return last_connection_established;
|
||||
}
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
VariableContainer *MOcppMongooseClient::getVariableContainer() {
|
||||
return websocketSettings.get();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MO_MG_USE_VERSION == MO_MG_V614
|
||||
|
||||
void ws_cb(struct mg_connection *nc, int ev, void *ev_data, void *user_data) {
|
||||
|
||||
MOcppMongooseClient *osock = nullptr;
|
||||
|
||||
if (user_data && nc->flags & MG_F_IS_WEBSOCKET && nc->flags & MO_MG_F_IS_MOcppMongooseClient) {
|
||||
osock = reinterpret_cast<MOcppMongooseClient*>(user_data);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (ev) {
|
||||
case MG_EV_CONNECT: {
|
||||
int status = *((int *) ev_data);
|
||||
if (status != 0) {
|
||||
MO_DBG_WARN("connection %s -- error %d", osock->getUrl(), status);
|
||||
(void)0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MG_EV_WEBSOCKET_HANDSHAKE_DONE: {
|
||||
struct http_message *hm = (struct http_message *) ev_data;
|
||||
if (hm->resp_code == 101) {
|
||||
MO_DBG_INFO("connection %s -- connected!", osock->getUrl());
|
||||
osock->setConnectionOpen(true);
|
||||
} else {
|
||||
MO_DBG_WARN("connection %s -- HTTP error %d", osock->getUrl(), hm->resp_code);
|
||||
(void)0;
|
||||
/* Connection will be closed after this. */
|
||||
}
|
||||
osock->updateRcvTimer();
|
||||
break;
|
||||
}
|
||||
case MG_EV_POLL: {
|
||||
/* Nothing to do here. OCPP engine has own loop-function */
|
||||
break;
|
||||
}
|
||||
case MG_EV_WEBSOCKET_FRAME: {
|
||||
struct websocket_message *wm = (struct websocket_message *) ev_data;
|
||||
|
||||
if (!osock->getReceiveTXTcallback()((const char *) wm->data, wm->size)) { //forward message to Context
|
||||
MO_DBG_ERR("processing WS input failed");
|
||||
(void)0;
|
||||
}
|
||||
osock->updateRcvTimer();
|
||||
break;
|
||||
}
|
||||
case MG_EV_WEBSOCKET_CONTROL_FRAME: {
|
||||
osock->updateRcvTimer();
|
||||
break;
|
||||
}
|
||||
case MG_EV_CLOSE: {
|
||||
MO_DBG_INFO("connection %s -- closed", osock->getUrl());
|
||||
osock->cleanConnection();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#if MO_MG_USE_VERSION <= MO_MG_V708
|
||||
void ws_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||
#else
|
||||
void ws_cb(struct mg_connection *c, int ev, void *ev_data) {
|
||||
void *fn_data = c->fn_data;
|
||||
#endif
|
||||
if (ev != 2) {
|
||||
MO_DBG_VERBOSE("Cb fn with event: %d\n", ev);
|
||||
(void)0;
|
||||
}
|
||||
|
||||
MOcppMongooseClient *osock = reinterpret_cast<MOcppMongooseClient*>(fn_data);
|
||||
if (!osock) {
|
||||
if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
|
||||
MO_DBG_INFO("connection %s", ev == MG_EV_CLOSE ? "closed" : "error");
|
||||
(void)0;
|
||||
} else {
|
||||
MO_DBG_ERR("invalid state %d", ev);
|
||||
(void)0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev == MG_EV_ERROR) {
|
||||
// On error, log error message
|
||||
MG_ERROR(("%p %s", c->fd, (char *) ev_data));
|
||||
} else if (ev == MG_EV_CONNECT) {
|
||||
// If target URL is SSL/TLS, command client connection to use TLS
|
||||
if (mg_url_is_ssl(osock->getUrl())) {
|
||||
const char *ca_string = osock->getCaCert();
|
||||
if (ca_string && *ca_string == '\0') { //check if certificate verification is disabled (cert string is empty)
|
||||
//yes, disabled
|
||||
ca_string = nullptr;
|
||||
}
|
||||
struct mg_tls_opts opts;
|
||||
memset(&opts, 0, sizeof(struct mg_tls_opts));
|
||||
#if MO_MG_USE_VERSION <= MO_MG_V708
|
||||
opts.ca = ca_string;
|
||||
opts.srvname = mg_url_host(osock->getUrl());
|
||||
#else
|
||||
opts.ca = mg_str(ca_string);
|
||||
opts.name = mg_url_host(osock->getUrl());
|
||||
#endif
|
||||
mg_tls_init(c, &opts);
|
||||
} else {
|
||||
MO_DBG_WARN("Insecure connection (WS)");
|
||||
}
|
||||
} else if (ev == MG_EV_WS_OPEN) {
|
||||
// WS connection established. Perform MQTT login
|
||||
MO_DBG_INFO("connection %s -- connected!", osock->getUrl());
|
||||
osock->setConnectionOpen(true);
|
||||
osock->updateRcvTimer();
|
||||
} else if (ev == MG_EV_WS_MSG) {
|
||||
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||
#if MO_MG_USE_VERSION <= MO_MG_V713
|
||||
if (!osock->getReceiveTXTcallback()((const char*) wm->data.ptr, wm->data.len)) {
|
||||
#else
|
||||
if (!osock->getReceiveTXTcallback()((const char*) wm->data.buf, wm->data.len)) {
|
||||
#endif
|
||||
MO_DBG_WARN("processing input message failed");
|
||||
}
|
||||
osock->updateRcvTimer();
|
||||
} else if (ev == MG_EV_WS_CTL) {
|
||||
osock->updateRcvTimer();
|
||||
}
|
||||
|
||||
if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
|
||||
MO_DBG_INFO("connection %s -- %s", osock->getUrl(), ev == MG_EV_CLOSE ? "closed" : "error");
|
||||
osock->cleanConnection();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool MicroOcpp::validateAuthorizationKeyHex(const char *auth_key_hex) {
|
||||
if (!auth_key_hex) {
|
||||
return true; //nullptr (or "") means disable Auth
|
||||
}
|
||||
bool valid = true;
|
||||
size_t i = 0;
|
||||
while (i <= 2 * MO_AUTHKEY_LEN_MAX && auth_key_hex[i] != '\0') {
|
||||
//check if character is in 0-9, a-f, or A-F
|
||||
if ( (auth_key_hex[i] >= '0' && auth_key_hex[i] <= '9') ||
|
||||
(auth_key_hex[i] >= 'a' && auth_key_hex[i] <= 'f') ||
|
||||
(auth_key_hex[i] >= 'A' && auth_key_hex[i] <= 'F')) {
|
||||
//yes, it is
|
||||
i++;
|
||||
} else {
|
||||
//no, it isn't
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
valid &= auth_key_hex[i] == '\0';
|
||||
valid &= (i % 2) == 0;
|
||||
if (!valid) {
|
||||
MO_DBG_ERR("AuthorizationKey must be hex with at most 20 octets");
|
||||
(void)0;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// matth-x/MicroOcppMongoose
|
||||
// Copyright Matthias Akstaller 2019 - 2024
|
||||
// GPL-3.0 License (see LICENSE)
|
||||
|
||||
#ifndef MO_MONGOOSECLIENT_H
|
||||
#define MO_MONGOOSECLIENT_H
|
||||
|
||||
#if defined(ARDUINO) //fix for conflicting definitions of IPAddress on Arduino
|
||||
#include <Arduino.h>
|
||||
#include <IPAddress.h>
|
||||
#endif
|
||||
|
||||
#include "mongoose.h"
|
||||
#include <MicroOcpp/Core/Connection.h>
|
||||
#include <MicroOcpp/Version.h>
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
#ifndef MO_WSCONN_FN
|
||||
#define MO_WSCONN_FN (MO_FILENAME_PREFIX "ws-conn.jsn")
|
||||
#define MO_WSCONN_FN_V201 (MO_FILENAME_PREFIX "ws-conn-v201.jsn")
|
||||
#endif
|
||||
|
||||
#define MO_AUTHKEY_LEN_MAX 63 // Basic Auth password length for both OCPP 1.6 and 2.0.1
|
||||
|
||||
namespace MicroOcpp {
|
||||
|
||||
class FilesystemAdapter;
|
||||
class Configuration;
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
class Variable;
|
||||
class VariableContainer;
|
||||
class VariableContainerOwning;
|
||||
#endif
|
||||
|
||||
class MOcppMongooseClient : public MicroOcpp::Connection {
|
||||
private:
|
||||
struct mg_mgr *mgr {nullptr};
|
||||
struct mg_connection *websocket {nullptr};
|
||||
std::string backend_url;
|
||||
std::string cb_id;
|
||||
std::string url; //url = backend_url + '/' + cb_id
|
||||
unsigned char auth_key [MO_AUTHKEY_LEN_MAX + 1]; // Stores the raw Basic Auth password bytes. Appends a terminating '\0' for legacy accessors.
|
||||
size_t auth_key_len;
|
||||
const char *ca_cert; //zero-copy. The host system must ensure that this pointer remains valid during the lifetime of this class
|
||||
std::shared_ptr<Configuration> setting_backend_url_str;
|
||||
std::shared_ptr<Configuration> setting_cb_id_str;
|
||||
std::shared_ptr<Configuration> setting_auth_key_hex_str;
|
||||
unsigned long last_status_dbg_msg {0}, last_recv {0};
|
||||
std::shared_ptr<Configuration> reconnect_interval_int; //minimum time between two connect trials in s
|
||||
unsigned long last_reconnection_attempt {-1UL / 2UL};
|
||||
std::shared_ptr<Configuration> stale_timeout_int; //inactivity period after which the connection will be closed
|
||||
std::shared_ptr<Configuration> ws_ping_interval_int; //heartbeat intervall in s. 0 sets hb off
|
||||
unsigned long last_hb {0};
|
||||
#if MO_ENABLE_V201
|
||||
std::unique_ptr<VariableContainerOwning> websocketSettings;
|
||||
Variable *v201csmsUrlString = nullptr;
|
||||
Variable *v201identityString = nullptr;
|
||||
Variable *v201basicAuthPasswordString = nullptr;
|
||||
#endif
|
||||
bool connection_established {false};
|
||||
unsigned long last_connection_established {-1UL / 2UL};
|
||||
bool connection_closing {false};
|
||||
ReceiveTXTcallback receiveTXTcallback = [] (const char *, size_t) {return false;};
|
||||
|
||||
ProtocolVersion protocolVersion;
|
||||
|
||||
void reconnect();
|
||||
|
||||
void maintainWsConn();
|
||||
|
||||
public:
|
||||
MOcppMongooseClient(struct mg_mgr *mgr,
|
||||
const char *backend_url_factory,
|
||||
const char *charge_box_id_factory,
|
||||
unsigned char *auth_key_factory, size_t auth_key_factory_len,
|
||||
const char *ca_cert = nullptr, //zero-copy, the string must outlive this class and mg_mgr. Forwards this string to Mongoose as ssl_ca_cert (see https://github.com/cesanta/mongoose/blob/ab650ec5c99ceb52bb9dc59e8e8ec92a2724932b/mongoose.h#L4192)
|
||||
std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem = nullptr,
|
||||
ProtocolVersion protocolVersion = ProtocolVersion(1,6));
|
||||
|
||||
//DEPRECATED: will be removed in a future release
|
||||
MOcppMongooseClient(struct mg_mgr *mgr,
|
||||
const char *backend_url_factory = nullptr,
|
||||
const char *charge_box_id_factory = nullptr,
|
||||
const char *auth_key_factory = nullptr,
|
||||
const char *ca_cert = nullptr, //zero-copy, the string must outlive this class and mg_mgr. Forwards this string to Mongoose as ssl_ca_cert (see https://github.com/cesanta/mongoose/blob/ab650ec5c99ceb52bb9dc59e8e8ec92a2724932b/mongoose.h#L4192)
|
||||
std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem = nullptr,
|
||||
ProtocolVersion protocolVersion = ProtocolVersion(1,6));
|
||||
|
||||
~MOcppMongooseClient();
|
||||
|
||||
void loop() override;
|
||||
|
||||
bool sendTXT(const char *msg, size_t length) override;
|
||||
|
||||
void setReceiveTXTcallback(MicroOcpp::ReceiveTXTcallback &receiveTXT) override {
|
||||
this->receiveTXTcallback = receiveTXT;
|
||||
}
|
||||
|
||||
MicroOcpp::ReceiveTXTcallback &getReceiveTXTcallback() {
|
||||
return receiveTXTcallback;
|
||||
}
|
||||
|
||||
//update WS configs. To apply the updates, call `reloadConfigs()` afterwards
|
||||
void setBackendUrl(const char *backend_url);
|
||||
void setChargeBoxId(const char *cb_id);
|
||||
void setAuthKey(const char *auth_key); //DEPRECATED: will be removed in a future release
|
||||
void setAuthKey(const unsigned char *auth_key, size_t len); //set the auth key in bytes-encoded format
|
||||
void setCaCert(const char *ca_cert); //forwards this string to Mongoose as ssl_ca_cert (see https://github.com/cesanta/mongoose/blob/ab650ec5c99ceb52bb9dc59e8e8ec92a2724932b/mongoose.h#L4192)
|
||||
|
||||
void reloadConfigs();
|
||||
|
||||
const char *getBackendUrl() {return backend_url.c_str();}
|
||||
const char *getChargeBoxId() {return cb_id.c_str();}
|
||||
const char *getAuthKey() {return (const char*)auth_key;} //DEPRECATED: will be removed in a future release
|
||||
int printAuthKey(unsigned char *buf, size_t size);
|
||||
const char *getCaCert() {return ca_cert ? ca_cert : "";}
|
||||
|
||||
const char *getUrl() {return url.c_str();}
|
||||
|
||||
void setConnectionOpen(bool open);
|
||||
bool isConnectionOpen() {return connection_established && !connection_closing;}
|
||||
bool isConnected() {return isConnectionOpen();}
|
||||
void cleanConnection();
|
||||
|
||||
void updateRcvTimer();
|
||||
unsigned long getLastRecv(); //get time of last successful receive in millis
|
||||
unsigned long getLastConnected(); //get time of last connection establish
|
||||
|
||||
#if MO_ENABLE_V201
|
||||
//WS client creates and manages its own Variables. This getter function is a temporary solution, in future
|
||||
//the WS client will be initialized with a Context reference for registering the Variables directly
|
||||
VariableContainer *getVariableContainer();
|
||||
#endif
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,129 @@
|
||||
// matth-x/MicroOcppMongoose
|
||||
// Copyright Matthias Akstaller 2019 - 2024
|
||||
// GPL-3.0 License (see LICENSE)
|
||||
|
||||
#include "MicroOcppMongooseClient_c.h"
|
||||
#include "MicroOcppMongooseClient.h"
|
||||
|
||||
#include <MicroOcpp/Core/FilesystemAdapter.h>
|
||||
#include <MicroOcpp/Debug.h>
|
||||
|
||||
using namespace MicroOcpp;
|
||||
|
||||
OCPP_Connection *ocpp_makeConnection(struct mg_mgr *mgr,
|
||||
const char *backend_url_default,
|
||||
const char *charge_box_id_default,
|
||||
const char *auth_key_default,
|
||||
const char *CA_cert_default,
|
||||
OCPP_FilesystemOpt fsopt) {
|
||||
|
||||
std::shared_ptr<MicroOcpp::FilesystemAdapter> filesystem;
|
||||
|
||||
#ifndef MO_DEACTIVATE_FLASH
|
||||
filesystem = makeDefaultFilesystemAdapter(fsopt);
|
||||
#endif
|
||||
|
||||
auto sock = new MOcppMongooseClient(mgr,
|
||||
backend_url_default,
|
||||
charge_box_id_default,
|
||||
auth_key_default,
|
||||
CA_cert_default,
|
||||
filesystem);
|
||||
|
||||
return reinterpret_cast<OCPP_Connection*>(sock);;
|
||||
}
|
||||
|
||||
void ocpp_deinitConnection(OCPP_Connection *sock) {
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
delete mgsock;
|
||||
}
|
||||
|
||||
void ocpp_setBackendUrl(OCPP_Connection *sock, const char *backend_url) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
mgsock->setBackendUrl(backend_url);
|
||||
}
|
||||
|
||||
void ocpp_setChargeBoxId(OCPP_Connection *sock, const char *cb_id) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
mgsock->setChargeBoxId(cb_id);
|
||||
}
|
||||
|
||||
void ocpp_setAuthKey(OCPP_Connection *sock, const char *auth_key) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
mgsock->setAuthKey(auth_key);
|
||||
}
|
||||
|
||||
void ocpp_setCaCert(OCPP_Connection *sock, const char *ca_cert) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
mgsock->setCaCert(ca_cert);
|
||||
}
|
||||
|
||||
void ocpp_reloadConfigs(OCPP_Connection *sock) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
mgsock->reloadConfigs();
|
||||
}
|
||||
|
||||
const char *ocpp_getBackendUrl(OCPP_Connection *sock) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return nullptr;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
return mgsock->getBackendUrl();
|
||||
}
|
||||
|
||||
const char *ocpp_getChargeBoxId(OCPP_Connection *sock) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return nullptr;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
return mgsock->getChargeBoxId();
|
||||
}
|
||||
|
||||
const char *ocpp_getAuthKey(OCPP_Connection *sock) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return nullptr;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
return mgsock->getAuthKey();
|
||||
}
|
||||
|
||||
const char *ocpp_getCaCert(OCPP_Connection *sock) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return nullptr;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
return mgsock->getCaCert();
|
||||
}
|
||||
|
||||
bool ocpp_isConnectionOpen(OCPP_Connection *sock) {
|
||||
if (!sock) {
|
||||
MO_DBG_ERR("invalid argument");
|
||||
return false;
|
||||
}
|
||||
auto mgsock = reinterpret_cast<MOcppMongooseClient*>(sock);
|
||||
return mgsock->isConnectionOpen();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// matth-x/MicroOcppMongoose
|
||||
// Copyright Matthias Akstaller 2019 - 2024
|
||||
// GPL-3.0 License (see LICENSE)
|
||||
|
||||
#ifndef MO_MONGOOSECLIENT_C_H
|
||||
#define MO_MONGOOSECLIENT_C_H
|
||||
|
||||
#if defined(__cplusplus) && defined(ARDUINO) //fix for conflicting defitions of IPAddress on Arduino
|
||||
#include <Arduino.h>
|
||||
#include <IPAddress.h>
|
||||
#endif
|
||||
|
||||
#include "mongoose.h"
|
||||
#include <MicroOcpp/Core/ConfigurationOptions.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct OCPP_Connection;
|
||||
typedef struct OCPP_Connection OCPP_Connection;
|
||||
|
||||
OCPP_Connection *ocpp_makeConnection(struct mg_mgr *mgr,
|
||||
const char *backend_url_default, //all cstrings can be NULL
|
||||
const char *charge_box_id_default,
|
||||
const char *auth_key_default,
|
||||
const char *CA_cert_default,
|
||||
struct OCPP_FilesystemOpt fsopt);
|
||||
|
||||
void ocpp_deinitConnection(OCPP_Connection *sock);
|
||||
|
||||
//update WS configs. To apply the updates, call `ocpp_reloadConfigs()` afterwards
|
||||
void ocpp_setBackendUrl(OCPP_Connection *sock, const char *backend_url);
|
||||
void ocpp_setChargeBoxId(OCPP_Connection *sock, const char *cb_id);
|
||||
void ocpp_setAuthKey(OCPP_Connection *sock, const char *auth_key);
|
||||
void ocpp_setCaCert(OCPP_Connection *sock, const char *ca_cert);
|
||||
|
||||
void ocpp_reloadConfigs(OCPP_Connection *sock);
|
||||
|
||||
const char *ocpp_getBackendUrl(OCPP_Connection *sock);
|
||||
const char *ocpp_getChargeBoxId(OCPP_Connection *sock);
|
||||
const char *ocpp_getAuthKey(OCPP_Connection *sock);
|
||||
const char *ocpp_getCaCert(OCPP_Connection *sock);
|
||||
|
||||
bool ocpp_isConnectionOpen(OCPP_Connection *sock);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -14,7 +14,6 @@ board = rymcu-esp32-devkitc
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
matth-x/MicroOcpp@^1.2.0
|
||||
matth-x/MicroOcppMongoose@^1.2.0
|
||||
roboticsbrno/SmartLeds@^3.1.5
|
||||
miguelbalboa/MFRC522@^1.4.12
|
||||
tzapu/WiFiManager@^2.0.17
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
// For production: ws://csms.helios.bh8.ga:8180/steve/websocket/CentralSystemService
|
||||
#define CFG_OCPP_BACKEND "wss://csms-server.uniiem.com/ocpp"
|
||||
// #define CFG_CP_IDENTIFIER "CQWU_HHB_0001"
|
||||
#define CFG_CP_IDENTIFIER "ICP_906A28"
|
||||
#define CFG_CP_IDENTIFIER ""
|
||||
#define CFG_CB_SERIAL "REDAone_prototype00"
|
||||
#define CFG_CP_FW_VERSION "1.0.0"
|
||||
#define CFG_CP_MODAL "Helios DA One"
|
||||
#define CFG_CP_VENDOR "RayineElec"
|
||||
// OCPP Security Profile 1: Basic Auth password (username = chargePointIdentifier)
|
||||
// OCPP Security Profile 1: sent as Authorization: Basic base64(<chargePointIdentifier>:<password>)
|
||||
// Set to nullptr to disable authentication
|
||||
// #define CFG_OCPP_PASSWORD "my_password"
|
||||
#define CFG_OCPP_PASSWORD nullptr
|
||||
|
||||
@@ -145,8 +145,16 @@ void setup()
|
||||
"%02X%02X%02X%02X%02X%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
|
||||
if (strlen(CFG_CP_IDENTIFIER) > 0)
|
||||
{
|
||||
strncpy(cp_identifier, CFG_CP_IDENTIFIER, sizeof(cp_identifier) - 1);
|
||||
cp_identifier[sizeof(cp_identifier) - 1] = '\0';
|
||||
}
|
||||
else
|
||||
{
|
||||
// Auto-generate Charge Point Identifier based on MAC (e.g. HLCP_A1B2C3)
|
||||
snprintf(cp_identifier, sizeof(cp_identifier), "HLCP_%s", cpSerial + 6);
|
||||
}
|
||||
|
||||
// reset LED
|
||||
leds[0] = Rgb{0, 0, 0};
|
||||
@@ -155,6 +163,7 @@ void setup()
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.printf("\n\n%s(%s) made by %s\n", CFG_CP_MODAL, cpSerial, CFG_CP_VENDOR);
|
||||
Serial.printf("Charge Point Identifier: %s\n", cp_identifier);
|
||||
Serial.println("Initializing firmware...\n");
|
||||
|
||||
// Initialize LED
|
||||
@@ -174,8 +183,10 @@ void setup()
|
||||
Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str());
|
||||
Serial.printf("[OCPP] Loaded Password length: %d\n", p.length());
|
||||
|
||||
strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend));
|
||||
strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password));
|
||||
strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend) - 1);
|
||||
ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
|
||||
strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password) - 1);
|
||||
ocpp_password[sizeof(ocpp_password) - 1] = '\0';
|
||||
|
||||
WiFiManager wm;
|
||||
wm.setSaveConfigCallback(saveConfigCallback);
|
||||
@@ -184,7 +195,7 @@ void setup()
|
||||
|
||||
// Use autocomplete=off to prevent browsers from autofilling old URLs after a reset
|
||||
WiFiManagerParameter custom_ocpp_backend("backend", "OCPP Backend URL", ocpp_backend, 128, "autocomplete=\"off\"");
|
||||
WiFiManagerParameter custom_ocpp_password("ocpp_password", "OCPP 连接密码 (Basic Auth, 没有请留空)", ocpp_password, 64, "autocomplete=\"off\" type=\"password\"");
|
||||
WiFiManagerParameter custom_ocpp_password("ocpp_password", "OCPP Basic AuthKey", ocpp_password, 64, "autocomplete=\"off\" type=\"password\"");
|
||||
|
||||
wm.addParameter(&custom_ocpp_backend);
|
||||
wm.addParameter(&custom_ocpp_password);
|
||||
@@ -334,8 +345,10 @@ void setup()
|
||||
|
||||
if (shouldSaveConfig)
|
||||
{
|
||||
strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend));
|
||||
strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password));
|
||||
strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend) - 1);
|
||||
ocpp_backend[sizeof(ocpp_backend) - 1] = '\0';
|
||||
strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password) - 1);
|
||||
ocpp_password[sizeof(ocpp_password) - 1] = '\0';
|
||||
|
||||
preferences.putString("backend", ocpp_backend);
|
||||
preferences.putString("ocpp_password", ocpp_password);
|
||||
@@ -354,8 +367,40 @@ void setup()
|
||||
s_led_state = LED_WIFI_CONNECTED;
|
||||
|
||||
mg_mgr_init(&mgr);
|
||||
const char *final_ocpp_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr;
|
||||
MicroOcpp::MOcppMongooseClient *client = new MicroOcpp::MOcppMongooseClient(&mgr, ocpp_backend, cp_identifier, final_ocpp_password, "", MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail), MicroOcpp::ProtocolVersion(1, 6));
|
||||
const char *basic_auth_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr;
|
||||
unsigned char *basic_auth_password_bytes = nullptr;
|
||||
size_t basic_auth_password_len = 0;
|
||||
|
||||
if (basic_auth_password)
|
||||
{
|
||||
basic_auth_password_bytes = reinterpret_cast<unsigned char *>(const_cast<char *>(basic_auth_password));
|
||||
basic_auth_password_len = strlen(basic_auth_password);
|
||||
}
|
||||
|
||||
MicroOcpp::MOcppMongooseClient *client = new MicroOcpp::MOcppMongooseClient(
|
||||
&mgr,
|
||||
ocpp_backend,
|
||||
cp_identifier,
|
||||
nullptr,
|
||||
0,
|
||||
"",
|
||||
MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail),
|
||||
MicroOcpp::ProtocolVersion(1, 6));
|
||||
|
||||
// Preferences and firmware config are the source of truth. Override any stale
|
||||
// values cached in MicroOcpp's ws-conn storage before the first reconnect cycle.
|
||||
client->setBackendUrl(ocpp_backend);
|
||||
client->setChargeBoxId(cp_identifier);
|
||||
if (basic_auth_password_bytes)
|
||||
{
|
||||
client->setAuthKey(basic_auth_password_bytes, basic_auth_password_len);
|
||||
}
|
||||
else
|
||||
{
|
||||
client->setAuthKey(reinterpret_cast<const unsigned char *>(""), 0);
|
||||
}
|
||||
client->reloadConfigs();
|
||||
|
||||
mocpp_initialize(*client, ChargerCredentials(CFG_CP_MODAL, CFG_CP_VENDOR, CFG_CP_FW_VERSION, cpSerial, nullptr, nullptr, CFG_CB_SERIAL, nullptr, nullptr), MicroOcpp::makeDefaultFilesystemAdapter(MicroOcpp::FilesystemOpt::Use_Mount_FormatOnFail));
|
||||
|
||||
// For development/recovery: Set up BOOT button (GPIO 0)
|
||||
|
||||
Reference in New Issue
Block a user