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,
|
"when": 1773678571220,
|
||||||
"tag": "0005_peaceful_anthem",
|
"tag": "0005_peaceful_anthem",
|
||||||
"breakpoints": true
|
"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 './auth-schema.ts'
|
||||||
export * from './ocpp-schema.ts'
|
export * from './ocpp-schema.ts'
|
||||||
export * from './tariff-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 userRoutes from './routes/users.ts'
|
||||||
import setupRoutes from './routes/setup.ts'
|
import setupRoutes from './routes/setup.ts'
|
||||||
import tariffRoutes from './routes/tariff.ts'
|
import tariffRoutes from './routes/tariff.ts'
|
||||||
|
import settingsRoutes from './routes/settings.ts'
|
||||||
|
|
||||||
import type { HonoEnv } from './types/hono.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/users', userRoutes)
|
||||||
app.route('/api/setup', setupRoutes)
|
app.route('/api/setup', setupRoutes)
|
||||||
app.route('/api/tariff', tariffRoutes)
|
app.route('/api/tariff', tariffRoutes)
|
||||||
|
app.route('/api/settings', settingsRoutes)
|
||||||
|
|
||||||
app.get('/api', (c) => {
|
app.get('/api', (c) => {
|
||||||
const user = c.get('user')
|
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 { useDrizzle } from '@/lib/db.js'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { chargePoint } from '@/db/schema.js'
|
import { chargePoint } from '@/db/schema.js'
|
||||||
|
import { getOcpp16jSettings } from '@/lib/system-settings.js'
|
||||||
import type {
|
import type {
|
||||||
BootNotificationRequest,
|
BootNotificationRequest,
|
||||||
BootNotificationResponse,
|
BootNotificationResponse,
|
||||||
OcppConnectionContext,
|
OcppConnectionContext,
|
||||||
} from '../types.ts'
|
} from '../types.ts'
|
||||||
|
|
||||||
const DEFAULT_HEARTBEAT_INTERVAL = 60
|
|
||||||
|
|
||||||
export async function handleBootNotification(
|
export async function handleBootNotification(
|
||||||
payload: BootNotificationRequest,
|
payload: BootNotificationRequest,
|
||||||
ctx: OcppConnectionContext,
|
ctx: OcppConnectionContext,
|
||||||
): Promise<BootNotificationResponse> {
|
): Promise<BootNotificationResponse> {
|
||||||
const db = useDrizzle()
|
const db = useDrizzle()
|
||||||
|
const { heartbeatInterval } = await getOcpp16jSettings()
|
||||||
|
|
||||||
const [cp] = await db
|
const [cp] = await db
|
||||||
.insert(chargePoint)
|
.insert(chargePoint)
|
||||||
@@ -30,7 +30,7 @@ export async function handleBootNotification(
|
|||||||
meterSerialNumber: payload.meterSerialNumber ?? null,
|
meterSerialNumber: payload.meterSerialNumber ?? null,
|
||||||
// New, unknown devices start as Pending — admin must manually accept them
|
// New, unknown devices start as Pending — admin must manually accept them
|
||||||
registrationStatus: 'Pending',
|
registrationStatus: 'Pending',
|
||||||
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
heartbeatInterval,
|
||||||
lastBootNotificationAt: dayjs().toDate(),
|
lastBootNotificationAt: dayjs().toDate(),
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
@@ -45,7 +45,7 @@ export async function handleBootNotification(
|
|||||||
meterType: payload.meterType ?? null,
|
meterType: payload.meterType ?? null,
|
||||||
meterSerialNumber: payload.meterSerialNumber ?? null,
|
meterSerialNumber: payload.meterSerialNumber ?? null,
|
||||||
// Do NOT override registrationStatus — preserve whatever the admin set
|
// Do NOT override registrationStatus — preserve whatever the admin set
|
||||||
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
heartbeatInterval,
|
||||||
lastBootNotificationAt: dayjs().toDate(),
|
lastBootNotificationAt: dayjs().toDate(),
|
||||||
updatedAt: dayjs().toDate(),
|
updatedAt: dayjs().toDate(),
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@ export async function handleBootNotification(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
currentTime: dayjs().toISOString(),
|
currentTime: dayjs().toISOString(),
|
||||||
interval: DEFAULT_HEARTBEAT_INTERVAL,
|
interval: heartbeatInterval,
|
||||||
status,
|
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 = [
|
const settingsItems = [
|
||||||
{ href: "/dashboard/settings/user", label: "账号设置", icon: UserCog, adminOnly: false },
|
{ href: "/dashboard/settings/user", label: "账号设置", icon: UserCog, adminOnly: false },
|
||||||
|
{ href: "/dashboard/settings/parameters", label: "参数配置", icon: Gear, adminOnly: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
function NavContent({
|
function NavContent({
|
||||||
|
|||||||
@@ -202,6 +202,14 @@ export type TariffConfig = {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Ocpp16jSettings = {
|
||||||
|
heartbeatInterval: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SystemSettings = {
|
||||||
|
ocpp16j: Ocpp16jSettings;
|
||||||
|
};
|
||||||
|
|
||||||
// ── API functions ──────────────────────────────────────────────────────────
|
// ── API functions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export type ChartRange = "30d" | "7d" | "24h";
|
export type ChartRange = "30d" | "7d" | "24h";
|
||||||
@@ -349,4 +357,9 @@ export const api = {
|
|||||||
put: (data: { slots: TariffSlot[]; prices: Record<PriceTier, TierPricing> }) =>
|
put: (data: { slots: TariffSlot[]; prices: Record<PriceTier, TierPricing> }) =>
|
||||||
apiFetch<TariffConfig>("/api/tariff", { method: "PUT", body: JSON.stringify(data) }),
|
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
|
framework = arduino
|
||||||
lib_deps =
|
lib_deps =
|
||||||
matth-x/MicroOcpp@^1.2.0
|
matth-x/MicroOcpp@^1.2.0
|
||||||
matth-x/MicroOcppMongoose@^1.2.0
|
|
||||||
roboticsbrno/SmartLeds@^3.1.5
|
roboticsbrno/SmartLeds@^3.1.5
|
||||||
miguelbalboa/MFRC522@^1.4.12
|
miguelbalboa/MFRC522@^1.4.12
|
||||||
tzapu/WiFiManager@^2.0.17
|
tzapu/WiFiManager@^2.0.17
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
// For production: ws://csms.helios.bh8.ga:8180/steve/websocket/CentralSystemService
|
// For production: ws://csms.helios.bh8.ga:8180/steve/websocket/CentralSystemService
|
||||||
#define CFG_OCPP_BACKEND "wss://csms-server.uniiem.com/ocpp"
|
#define CFG_OCPP_BACKEND "wss://csms-server.uniiem.com/ocpp"
|
||||||
// #define CFG_CP_IDENTIFIER "CQWU_HHB_0001"
|
// #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_CB_SERIAL "REDAone_prototype00"
|
||||||
#define CFG_CP_FW_VERSION "1.0.0"
|
#define CFG_CP_FW_VERSION "1.0.0"
|
||||||
#define CFG_CP_MODAL "Helios DA One"
|
#define CFG_CP_MODAL "Helios DA One"
|
||||||
#define CFG_CP_VENDOR "RayineElec"
|
#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
|
// Set to nullptr to disable authentication
|
||||||
// #define CFG_OCPP_PASSWORD "my_password"
|
// #define CFG_OCPP_PASSWORD "my_password"
|
||||||
#define CFG_OCPP_PASSWORD nullptr
|
#define CFG_OCPP_PASSWORD nullptr
|
||||||
|
|||||||
@@ -145,8 +145,16 @@ void setup()
|
|||||||
"%02X%02X%02X%02X%02X%02X",
|
"%02X%02X%02X%02X%02X%02X",
|
||||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
|
|
||||||
// Auto-generate Charge Point Identifier based on MAC (e.g. HLCP_A1B2C3)
|
if (strlen(CFG_CP_IDENTIFIER) > 0)
|
||||||
snprintf(cp_identifier, sizeof(cp_identifier), "HLCP_%s", cpSerial + 6);
|
{
|
||||||
|
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
|
// reset LED
|
||||||
leds[0] = Rgb{0, 0, 0};
|
leds[0] = Rgb{0, 0, 0};
|
||||||
@@ -155,6 +163,7 @@ void setup()
|
|||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(1000);
|
delay(1000);
|
||||||
Serial.printf("\n\n%s(%s) made by %s\n", CFG_CP_MODAL, cpSerial, CFG_CP_VENDOR);
|
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");
|
Serial.println("Initializing firmware...\n");
|
||||||
|
|
||||||
// Initialize LED
|
// Initialize LED
|
||||||
@@ -174,8 +183,10 @@ void setup()
|
|||||||
Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str());
|
Serial.printf("\n[OCPP] Loaded Backend URL: %s\n", b.c_str());
|
||||||
Serial.printf("[OCPP] Loaded Password length: %d\n", p.length());
|
Serial.printf("[OCPP] Loaded Password length: %d\n", p.length());
|
||||||
|
|
||||||
strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend));
|
strncpy(ocpp_backend, b.c_str(), sizeof(ocpp_backend) - 1);
|
||||||
strncpy(ocpp_password, p.c_str(), sizeof(ocpp_password));
|
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;
|
WiFiManager wm;
|
||||||
wm.setSaveConfigCallback(saveConfigCallback);
|
wm.setSaveConfigCallback(saveConfigCallback);
|
||||||
@@ -184,7 +195,7 @@ void setup()
|
|||||||
|
|
||||||
// Use autocomplete=off to prevent browsers from autofilling old URLs after a reset
|
// 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_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_backend);
|
||||||
wm.addParameter(&custom_ocpp_password);
|
wm.addParameter(&custom_ocpp_password);
|
||||||
@@ -334,8 +345,10 @@ void setup()
|
|||||||
|
|
||||||
if (shouldSaveConfig)
|
if (shouldSaveConfig)
|
||||||
{
|
{
|
||||||
strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend));
|
strncpy(ocpp_backend, custom_ocpp_backend.getValue(), sizeof(ocpp_backend) - 1);
|
||||||
strncpy(ocpp_password, custom_ocpp_password.getValue(), sizeof(ocpp_password));
|
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("backend", ocpp_backend);
|
||||||
preferences.putString("ocpp_password", ocpp_password);
|
preferences.putString("ocpp_password", ocpp_password);
|
||||||
@@ -354,8 +367,40 @@ void setup()
|
|||||||
s_led_state = LED_WIFI_CONNECTED;
|
s_led_state = LED_WIFI_CONNECTED;
|
||||||
|
|
||||||
mg_mgr_init(&mgr);
|
mg_mgr_init(&mgr);
|
||||||
const char *final_ocpp_password = (strlen(ocpp_password) > 0) ? ocpp_password : nullptr;
|
const char *basic_auth_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));
|
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));
|
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)
|
// For development/recovery: Set up BOOT button (GPIO 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user