vda5050 gmr api server project init

This commit is contained in:
Tony Cao 2025-06-04 19:15:02 +08:00
commit 37e997d4c6
30 changed files with 9728 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules
deno.lock
*.exe
agv_api
simulator
release
device_registry.json
debug.log

212
agv_main.ts Normal file
View File

@ -0,0 +1,212 @@
// main.ts
/// <reference lib="deno.unstable" />
// 导入 UUID 库请根据实际情况调整URL
import { v4 as uuidv4 } from "npm:uuid";
import { setupAgvWorker, initAgvWorker } from "./agv_manager.ts";
import { reconnectAllWorker, setupMasterWorker, updateDeviceListFromConfig } from "./master_manager.ts";
import { globalEventManager } from "./event_manager.ts";
// Load config and mappings from JSON files at runtime
const configText = await Deno.readTextFile("./config.json");
const config = JSON.parse(configText);
const mappingFile = config.mappingFile || "mapping.json";
const mappingsText = await Deno.readTextFile(`./${mappingFile}`);
const mappings = JSON.parse(mappingsText);
// Generate a unique instance ID from manufacturer and UUID
const instanceId = uuidv4();
// 添加全局事件监听器
globalEventManager.addEventListener("reconnect-all", (event: Event) => {
const customEvent = event as CustomEvent;
console.log("🎯 主线程收到全局重连事件:", customEvent.detail);
handleReconnectAll();
});
globalEventManager.addEventListener("grpc-connection-lost", (event: Event) => {
const customEvent = event as CustomEvent;
console.log("🎯 主线程收到 gRPC 连接丢失事件:", customEvent.detail);
// 可以添加特殊的 gRPC 重连逻辑
});
globalEventManager.addEventListener("device-status-update", (event: Event) => {
const customEvent = event as CustomEvent;
console.log("🎯 主线程收到设备状态更新事件:", customEvent.detail.data);
});
// 重连处理函数
function handleReconnectAll() {
console.log("agv_main: 执行全局重连");
reconnectAllWorker(kv, webWorker, masterWorker, agvWorker, config, mappings, instanceId);
}
// 初始化 downWorker 并支持自身重启
let downWorker: Worker;
function startDownWorker() {
console.log("agv_main: 启动 downWorker");
downWorker = new Worker(
new URL("./vda5050_transformer_worker.ts", import.meta.url).href,
{ type: "module" }
);
// 注册到全局事件管理器
globalEventManager.registerWorker("downWorker", downWorker);
// 初始化 downWorker
downWorker.postMessage({
type: "init",
mqtt: config.mqtt,
mappings,
instanceId
});
// 监听重启请求
downWorker.onmessage = (event: MessageEvent) => {
const message = event.data;
// 只处理非全局事件相关的消息
if (message.type === "reconnect-all" && message.type !== "dispatchGlobalEvent") {
console.log("agv_main: 收到 downWorker reconnect-all重启所有 Workers");
handleReconnectAll();
} else if (message.type === "reconnect-down") {
console.log("agv_main: 收到 downWorker reconnect-down重启 downWorker");
downWorker.postMessage({ type: "stop" });
downWorker.terminate();
startDownWorker();
}
};
}
startDownWorker();
// 打开 Deno KV 数据库Deno KV 在最新版本中为内置特性)
const kv = await Deno.openKv();
// // 启动 Web Worker 用于可视化界面
const webWorker = new Worker(
new URL("./web_worker.ts", import.meta.url).href,
{ type: "module" }
);
console.log("Web Worker 已启动");
const masterWorker = new Worker(
new URL("./vda_worker.ts", import.meta.url).href,
{ type: "module" }
);
const agvWorker = new Worker(
new URL("./agv_worker.ts", import.meta.url).href,
{ type: "module" }
);
// 注册所有 Worker 到全局事件管理器
globalEventManager.registerWorker("webWorker", webWorker);
globalEventManager.registerWorker("masterWorker", masterWorker);
globalEventManager.registerWorker("agvWorker", agvWorker);
reconnectAllWorker(kv, webWorker, masterWorker, agvWorker, config, mappings, instanceId);
// 监听配置文件变化,动态更新设备列表
const watcher = Deno.watchFs("./devices.json");
(async () => {
console.log("开始监控配置文件变化...");
for await (const event of watcher) {
if (event.paths.some((p) => p.endsWith("devices.json"))) {
console.log("检测到设备配置文件变化,更新设备列表...");
await updateDeviceListFromConfig(masterWorker, config, instanceId);
}
}
})();
// 处理 Ctrl+CSIGINT退出
Deno.addSignalListener("SIGINT", () => {
console.log("接收到 Ctrl+C主程序通知 Worker 退出...");
// 向所有 Worker 发送 shutdown 消息
agvWorker.postMessage("shutdown");
masterWorker.postMessage({ type: "shutdown" });
webWorker.postMessage({ type: "shutdown" });
// 通知 downWorker 关闭
downWorker.postMessage({ type: "shutdown" });
// 延时后终止 Worker 并退出主程序
setTimeout(() => {
agvWorker.terminate();
masterWorker.terminate();
webWorker.terminate();
downWorker.terminate();
console.log("所有 Worker 已终止,程序退出");
Deno.exit(0);
}, 2000);
});
// Web Worker 错误处理
webWorker.onerror = (error) => {
console.error("Web Worker 执行错误:", error);
};
// 发送导航订单的函数(示例订单)
function sendNavigationOrder() {
// 使用 UUID v4 生成订单号
const orderId = uuidv4();
console.log(`创建新的导航订单 ${orderId}`);
masterWorker.postMessage({
type: "sendOrder",
orderId: orderId,
nodes: [
{
nodeId: "start",
nodePosition: {
x: 0.0,
y: 0.0,
mapId: "warehouse",
theta: 0,
},
actions: [],
},
{
nodeId: "waypoint1",
nodePosition: {
x: 5.0,
y: 0.0,
mapId: "warehouse",
theta: 0,
},
actions: [],
},
{
nodeId: "destination",
nodePosition: {
x: 10.0,
y: 5.0,
mapId: "warehouse",
theta: 0,
},
actions: [],
},
],
});
}
// 取消订单的函数
function cancelOrder(orderId: string) {
console.log(`取消订单 ${orderId}`);
masterWorker.postMessage({
type: "cancelOrder",
orderId: orderId,
});
}
// 发送自定义命令设置给 Worker 的函数
function sendCustomCommand(cmd: string) {
console.log(`发送自定义命令设置: ${cmd}`);
masterWorker.postMessage({
type: "setCommand",
command: cmd,
});
}
// CLI moved to cli.ts
import { startCli } from "./cli.ts";
startCli(masterWorker, agvWorker, sendNavigationOrder, cancelOrder, sendCustomCommand, instanceId);
console.log("VDA 5050 控制程序已启动,输入命令或按 Ctrl+C 退出程序");

104
agv_manager.ts Normal file
View File

@ -0,0 +1,104 @@
// agv_manager.ts
// Module to setup and handle the AGV simulation Worker
import { reconnectAllWorker } from './master_manager.ts';
/**
* Sends the device list to the AGV worker.
*/
export async function updateAgvDeviceListFromConfig(worker: Worker) {
try {
const text = await Deno.readTextFile("./devices.json");
const devices = JSON.parse(text);
if (Array.isArray(devices)) {
worker.postMessage({ type: "deviceList", data: devices });
} else {
console.error("agv 配置文件格式错误,要求为数组格式");
}
} catch (error) {
console.error("agv 读取设备配置文件失败:", error);
}
}
export function initAgvWorker(agvWorker: Worker, config: any){
// Send initial init to the AGV worker
agvWorker.postMessage({ type: "init", data: { serverUrl: config.serverUrl } });
}
/**
* Sets up the AGV Worker, wiring message handlers and sending initial init.
* @param kv Deno KV instance for storing/retrieving AGV state
* @param serverUrl The gRPC server URL to send to the AGV worker
* @param masterWorker The master VDA 5050 Worker to forward AGV events to
* @returns The initialized AGV Worker
*/
export function setupAgvWorker(kv: Deno.Kv, config: any, masterWorker: Worker, agvWorker: Worker, web_worker: Worker, mappings: any, instanceId: string): Worker {
console.log("AGV 模拟器 Worker 已启动");
agvWorker.onmessage = async (event: MessageEvent) => {
const message = event.data;
switch (message.type) {
case "inited":
console.log("agv inited");
await updateAgvDeviceListFromConfig(agvWorker);
break;
case "requestKVDataOnline": {
const deviceKey = message.key;
const entry = await kv.get([deviceKey]);
const deviceStatus = entry.value || null;
agvWorker.postMessage({
type: "requestKVDataOnline",
requestId: message.requestId,
data: deviceStatus,
});
break;
}
case "requestKVData": {
const deviceKey = message.key;
const entry = await kv.get([deviceKey]);
const deviceStatus = entry.value || null;
agvWorker.postMessage({
type: "requestKVData",
requestId: message.requestId,
data: deviceStatus,
});
break;
}
case "orderForwarded":
masterWorker.postMessage({
type: "orderForwarded",
data: { agvId: message.agvId, order: message.data },
});
break;
case "instantActionsForwarded":
masterWorker.postMessage({
type: "instantActionsForwarded",
data: { agvId: message.agvId, actions: message.data },
});
break;
case "devicesStatus":
// console.log("收到 Worker devicesStatus 消息,准备更新设备状态",message);
break;
case "reconnect-all": {
console.log("agv 收到 Worker reconnect 消息,准备重连");
agvWorker.postMessage({
type: "shutdown"
});
setTimeout( () => {
agvWorker.terminate();
setTimeout( () => {
reconnectAllWorker(kv, web_worker, masterWorker, agvWorker, config, mappings, instanceId);
}, 2000);
}, 2000);
break;
}
default:
console.warn("未知消息类型 from agvWorker:", message);
}
};
return agvWorker;
}

932
agv_worker.ts Normal file
View File

@ -0,0 +1,932 @@
// agv_worker.ts
// 捕获未处理的 promise避免 worker 因 grpc 异常退出
globalThis.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
event.preventDefault();
});
// Deno + npm 包
import * as grpc from "npm:@grpc/grpc-js@1.12.1";
import * as protoLoader from "npm:@grpc/proto-loader";
import { delay } from "https://deno.land/std/async/delay.ts";
import { createWorkerEventHelper } from "./worker_event_helper.ts";
// 创建事件助手
const eventHelper = createWorkerEventHelper("agvWorker");
// Proto 加载
const PROTO_PATH = "./proto/vda5050.proto";
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDefinition) as any;
const vda5050 = proto.vda5050 as any;
// 运行时参数 & 设备列表
let SERVER_URL: string;
let devices: Device[] = [];
// 全局 gRPC 流 & AGV 实例映射
let stream: grpc.ClientDuplexStream<any, any> | null = null;
const agvMap = new Map<string, AgvSimulator>();
// gRPC 连接状态标记
let isGrpcConnected = false;
// 全局自增 headerId
let globalHeaderId = 1;
// 设备接口
interface Device {
serialNumber: string;
manufacturer: string;
}
// 新增 AGV 模拟器状态类型定义
interface Position {
x: number;
y: number;
theta: number;
}
interface NodeState {
nodeId: string;
sequenceId: number;
nodeDescription: string;
nodePosition: Position;
released: boolean;
}
interface EdgeState {
edgeId: string;
sequenceId: number;
edgeDescription: string;
released: boolean;
trajectory: Position[];
}
interface AgvPosition {
x: number;
y: number;
theta: number;
mapId: string;
mapDescription: string;
positionInitialized: boolean;
deviationRange: number;
localizationScore: number;
}
interface Velocity {
vx: number;
vy: number;
omega: number;
}
interface BatteryState {
batteryCharge: number;
charging: boolean;
batteryVoltage?: number;
batteryHealth?: number;
reach?: number;
}
interface ErrorState {
errorType: string;
errorDescription: string;
errorLevel: string;
}
interface SafetyState {
eStop: boolean;
fieldViolation: boolean;
}
interface ActionState {
actionId: string;
actionType: string;
actionDescription: string;
actionStatus: string;
resultDescription: string;
}
interface AgvState {
headerId: number;
timestamp: string;
version: string;
manufacturer: string;
serialNumber: string;
orderId: string;
orderUpdateId: number;
lastNodeId: string;
lastNodeSequenceId: number;
driving: boolean;
waitingForInteractionZoneRelease: boolean;
paused: boolean;
forkState?: string;
newBaseRequest: boolean;
distanceSinceLastNode: number;
operatingMode: string;
nodeStates: NodeState[];
edgeStates: EdgeState[];
agvPosition: AgvPosition;
velocity: Velocity;
loads: any[];
batteryState: BatteryState;
errors: ErrorState[];
information: any[];
safetyState: SafetyState;
actionStates: ActionState[];
}
// 建立/重建 gRPC 流
function initGrpcStream() {
try {
if (stream) {
console.log("关闭旧 gRPC 流");
try {
// 移除所有事件监听,避免它们触发重复 reconnectAll
stream.removeAllListeners();
stream.end();
} catch (_) { /* ignore */ }
stream = null;
}
const client = new vda5050.AgvManagement(
SERVER_URL,
grpc.credentials.createInsecure(),
);
stream = client.CommunicationChannel();
console.log("gRPC 流已建立");
isGrpcConnected = true;
if (!stream) {
console.error("无法创建 gRPC 流");
return false;
}
stream.on("data", (msg: any) => {
if(msg) {
const sim = agvMap.get(msg.targetAgvId);
if (!sim) {
console.warn("未知 AGV丢弃消息:", msg.agvId);
return;
}else{
if (msg.order) {
// console.log(`${msg.targetAgvId} << Order`, msg);
sim.handleOrder(msg.order);
} else if (msg.instantActions) {
const acts = msg.instantActions.instantActions || [];
// console.log(`${msg.targetAgvId} << InstantActions`, msg);
sim.handleInstantActions(acts);
}
}
}else{
console.warn("AGV 消息为空,丢弃消息:", msg.agvId);
return;
}
});
stream.on("error", async (err: any) => {
console.error("gRPC 流错误,全体重连");
isGrpcConnected = false;
stream = null; // 清空引用,防止重复使用失效的流
// 发送 gRPC 连接丢失事件
eventHelper.dispatchEvent("grpc-connection-lost", {
error: err.message || "unknown error",
timestamp: Date.now()
});
await reconnectAll();
});
// stream.on("end", async () => {
// console.warn("gRPC 流结束,全体重连");
// isGrpcConnected = false;
// stream = null; // 清空引用,防止重复使用失效的流
// // 发送 gRPC 连接结束事件
// eventHelper.dispatchEvent("grpc-connection-ended", {
// timestamp: Date.now()
// });
// await reconnectAll();
// });
return true;
} catch (err) {
console.error("gRPC 流初始化失败:", err);
stream = null;
return false;
}
}
// 安全发送消息到流
function sendToStream(msg: any): boolean {
if (!stream || !isGrpcConnected) {
console.warn("无法发送消息gRPC 流未初始化");
return false;
}
try {
stream.write(msg);
return true;
} catch (err) {
console.error("发送消息失败:", err);
return false;
}
}
// 重连:停所有定时器,延迟后重建流,重发 Connection 并重启状态上报
async function reconnectAll() {
// 停止所有状态更新
agvMap.forEach((s) => s.stopStatusUpdates());
// 等待一段时间再重连
await delay(500);
// 尝试重建流
let retryCount = 0;
const maxRetries = 5;
while (retryCount < maxRetries) {
console.log(`正在尝试重建 gRPC 流 等待次数 (${retryCount + 1}/${maxRetries})...`);
// if (initGrpcStream()) {
// console.log("gRPC 流重建成功");
// break;
// }
retryCount++;
await delay(100 * retryCount); // 递增等待时间
}
if (!stream) {
console.error(`无法重建 gRPC 流,已达到最大重试等待次数 ${maxRetries}`);
// 使用全局事件系统发送重连请求
eventHelper.dispatchEvent("reconnect-all", {
reason: "grpc-stream-failed",
retryCount: maxRetries,
timestamp: Date.now()
});
self.postMessage({ type: "reconnect-all"});
await delay(10000);
return;
}
// 成功重建流,重新发送所有 Connection 并重启状态更新
for (const sim of agvMap.values()) {
// sim.sendConnection("OFFLINE"); // 主动发送 ONLINE 状态
sim.resetLastOnlineStatus();
sim.startStatusUpdates();
await delay(200);
}
}
// AGV 模拟器
class AgvSimulator {
private batteryLevel: number;
private position = { x: 0, y: 0, theta: 0 };
private orderId = "";
private orderUpdateId = 0;
private state!: AgvState;
private updateIntervalId: number | null = null;
private lastOperatingMode = "NONE";
private lastOnlineStatus = "OFFLINE";
private agvId: string;
private manufacturer: string;
private serialNumber: string;
constructor(
agvId: string,
manufacturer: string,
serialNumber: string,
) {
this.agvId = agvId;
this.manufacturer = manufacturer;
this.serialNumber = serialNumber;
// 随机初始化
this.batteryLevel = 75 + Math.random() * 25;
this.position = {
x: Math.random() * 100,
y: Math.random() * 100,
theta: Math.random() * Math.PI * 2,
};
this.initState();
}
// 构造初始 State
private initState() {
this.state = <AgvState>{
headerId: globalHeaderId++,
timestamp: new Date().toISOString(),
version: "2.0.0",
manufacturer: this.manufacturer,
serialNumber: this.serialNumber,
orderId: this.orderId,
orderUpdateId: this.orderUpdateId,
lastNodeId: "",
lastNodeSequenceId: 0,
driving: false,
waitingForInteractionZoneRelease: false,
paused: false,
forkState: undefined,
newBaseRequest: false,
distanceSinceLastNode: 0,
operatingMode: "NONE", // 初始离线
nodeStates: [],
edgeStates: [],
agvPosition: {
x: this.position.x,
y: this.position.y,
theta: this.position.theta,
mapId: "warehouse",
mapDescription: "",
positionInitialized: true,
deviationRange: 0,
localizationScore: 0.95,
},
velocity: { vx: 0, vy: 0, omega: 0 },
loads: [],
actionStates: [],
batteryState: {
batteryCharge: this.batteryLevel,
batteryVoltage: 24.5,
batteryHealth: 100,
charging: false,
reach: 0,
},
errors: [],
information: [],
safetyState: { eStop: false, fieldViolation: false },
};
}
// 上行 Connection默认为 ONLINE
public sendConnection(state: "ONLINE" | "OFFLINE" = "OFFLINE") {
const msg = {
agvId: this.agvId,
connection: {
headerId: globalHeaderId++,
timestamp: new Date().toISOString(),
version: "2.0.0",
manufacturer: this.manufacturer,
serialNumber: this.serialNumber,
connectionState: state,
},
};
// console.log("发送 Connection 消息", msg);
if (sendToStream(msg)) {
// console.log(`${this.agvId} >> Connection(${state})`);
return true;
}
return false;
}
public resetLastOnlineStatus() {
this.lastOnlineStatus = "OFFLINE";
}
// 定时上报 State
public startStatusUpdates() {
this.stopStatusUpdates();
this.updateIntervalId = setInterval(async () => {
// 检查在线/离线状态
this.checkOnlineStatus();
// console.log(`${this.agvId}:${this.state.operatingMode}:${this.lastOnlineStatus} 状态更新>>>>`);
if (this.lastOnlineStatus === "ONLINE") {
// console.log(`${this.agvId} 状态更新>>>>`);
this.updateAndSendState().catch(err => {
console.error(`${this.agvId} 状态更新失败:`, err);
});
}
}, 1000);
}
private async updateAndSendState() {
// 1) 请求 KV添加随机ID
const requestId = Math.random().toString(36).substring(2, 15);
self.postMessage({
type: "requestKVData",
requestId,
key: `device:${this.manufacturer}/${this.serialNumber}`,
});
}
sendStateToGrpc(data: any) {
// console.log("发送状态到 gRPC 服务器",this.agvId, this.state.serialNumber, data.agvId.serialNumber, data.state.serialNumber);
// console.log("更新状态并发送-->",this.agvId, this.state.serialNumber);
// 若请求失败则保持当前状态
if (data === null) return;
// console.log(`${this.agvId}: KV 数据获取成功`, data);
// 发送设备状态更新事件
// eventHelper.dispatchEvent("device-status-update", {
// agvId: this.agvId,
// batteryLevel: this.batteryLevel,
// operatingMode: data.state?.operatingMode || this.state.operatingMode,
// position: this.position,
// timestamp: Date.now()
// });
// 更新 headerId 和时间戳
this.state.headerId = globalHeaderId++;
this.state.timestamp = new Date(data.lastSeen).toISOString();
// 根据KV数据更新状态
if (data.state) {
// 更新操作模式
if (data.state.operatingMode) {
this.state.operatingMode = data.state.operatingMode;
this.lastOperatingMode = data.state.operatingMode;
}
// 更新电池状态
if (data.state.batteryState) {
const kvBattery = data.state.batteryState;
this.batteryLevel = kvBattery.batteryCharge !== undefined ?
kvBattery.batteryCharge : this.batteryLevel;
// 更新完整的电池状态
this.state.batteryState = {
batteryCharge: this.batteryLevel,
batteryVoltage: kvBattery.batteryVoltage || this.state.batteryState.batteryVoltage,
batteryHealth: kvBattery.batteryHealth || this.state.batteryState.batteryHealth,
charging: kvBattery.charging !== undefined ? kvBattery.charging : this.state.batteryState.charging,
reach: kvBattery.reach !== undefined ? kvBattery.reach : this.state.batteryState.reach
};
} else if (this.state.driving) {
// 如果KV中没有电池数据但AGV在移动模拟电量消耗
this.batteryLevel = Math.max(0, this.batteryLevel - 0.05);
this.state.batteryState.batteryCharge = this.batteryLevel;
}
// 更新位置信息
if (data.state.agvPosition) {
const kvPos = data.state.agvPosition;
// 使用KV中的位置数据
this.position = {
x: kvPos.x !== undefined ? kvPos.x : this.position.x,
y: kvPos.y !== undefined ? kvPos.y : this.position.y,
theta: kvPos.theta !== undefined ? kvPos.theta : this.position.theta
};
// 更新完整的位置状态
this.state.agvPosition = {
x: this.position.x,
y: this.position.y,
theta: this.position.theta,
mapId: kvPos.mapId || this.state.agvPosition.mapId,
mapDescription: kvPos.mapDescription || this.state.agvPosition.mapDescription,
positionInitialized: kvPos.positionInitialized !== undefined ?
kvPos.positionInitialized : this.state.agvPosition.positionInitialized,
deviationRange: kvPos.deviationRange !== undefined ?
kvPos.deviationRange : this.state.agvPosition.deviationRange,
localizationScore: kvPos.localizationScore !== undefined ?
kvPos.localizationScore : this.state.agvPosition.localizationScore
};
} else if (this.state.driving) {
// 如果KV中没有位置数据但AGV在移动模拟位置变化
this.position.x += (Math.random() - 0.5) * 0.5;
this.position.y += (Math.random() - 0.5) * 0.5;
this.position.theta += (Math.random() - 0.5) * 0.1;
this.state.agvPosition.x = this.position.x;
this.state.agvPosition.y = this.position.y;
this.state.agvPosition.theta = this.position.theta;
}
// 更新其他状态字段
if (data.state.orderId !== undefined) {
this.state.orderId = data.state.orderId;
}
if (data.state.orderUpdateId !== undefined) {
this.state.orderUpdateId = data.state.orderUpdateId;
}
if (data.state.lastNodeId !== undefined) {
this.state.lastNodeId = data.state.lastNodeId;
}
if (data.state.lastNodeSequenceId !== undefined) {
this.state.lastNodeSequenceId = data.state.lastNodeSequenceId;
}
if (data.state.driving !== undefined) {
this.state.driving = data.state.driving;
}
if (data.state.waitingForInteractionZoneRelease !== undefined) {
this.state.waitingForInteractionZoneRelease = data.state.waitingForInteractionZoneRelease;
}
if (data.state.paused !== undefined) {
this.state.paused = data.state.paused;
}
if (data.state.nodeStates) {
this.state.nodeStates = data.state.nodeStates;
}
if (data.state.edgeStates) {
this.state.edgeStates = data.state.edgeStates;
}
if (data.state.actionStates) {
this.state.actionStates = data.state.actionStates;
}
if (data.state.errors) {
this.state.errors = data.state.errors;
}
if (data.state.safetyState) {
this.state.safetyState = data.state.safetyState;
}
if (data.state.serialNumber) {
this.state.serialNumber = data.state.serialNumber;
}
if (data.state.manufacturer) {
this.state.manufacturer = data.state.manufacturer;
}
if (data.state.velocity) {
this.state.velocity = data.state.velocity;
}
if (data.state.loads) {
this.state.loads = data.state.loads;
}
if (data.state.information) {
this.state.information = data.state.information;
}
if (data.state.forkState) {
this.state.forkState = data.state.forkState;
}
if (data.state.safetyState) {
this.state.safetyState = data.state.safetyState;
}
if (data.state.actionStates) {
this.state.actionStates = data.state.actionStates;
}
if (data.state.headerId) {
this.state.headerId = data.state.headerId;
}
if (data.state.timestamp) {
this.state.timestamp = data.state.timestamp;
}
if (data.state.version) {
this.state.version = data.state.version;
}
if (data.state.operatingMode) {
this.state.operatingMode = data.state.operatingMode;
}
if (data.state.nodeStates) {
this.state.nodeStates = data.state.nodeStates;
}
if (data.state.edgeStates) {
this.state.edgeStates = data.state.edgeStates;
}
}
// 发送 State按照 gRPC 定义的 AgvMessage 结构
const agvMessage = {
agvId: this.state.serialNumber,
state: this.state
// message_type 会由 oneof 自动处理,我们只需提供 state 字段
};
if (!sendToStream(agvMessage)) {
console.error(`${this.agvId}: 发送状态到 gRPC 服务器失败`);
}
}
public stopStatusUpdates() {
if (this.updateIntervalId != null) {
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
}
}
// 下行 Order 处理
public handleOrder(order: any) {
// 1) 更新内部状态
// console.log("=====>",order);
this.orderId = order.orderId;
this.orderUpdateId = order.orderUpdateId;
this.state.orderId = this.orderId;
this.state.orderUpdateId = this.orderUpdateId;
this.state.nodeStates = order.nodes.map((n: any, i: number) => ({
nodeId: n.nodeId,
sequenceId: n.sequenceId ?? i,
nodeDescription: n.nodeDescription ?? "",
nodePosition: n.nodePosition,
released: true,
}));
this.state.edgeStates = order.edges.map((e: any, i: number) => ({
edgeId: e.edgeId,
sequenceId: e.sequenceId ?? i,
edgeDescription: e.edgeDescription ?? "",
released: true,
trajectory: e.trajectory,
}));
this.state.driving = true;
// 2) 主动通知主线程收到新 Order
self.postMessage({
type: "orderForwarded",
agvId: this.agvId,
data: {
manufacturer: order.manufacturer,
serialNumber: order.serialNumber,
orderId: this.orderId,
orderUpdateId: this.orderUpdateId,
nodes: this.state.nodeStates,
edges: this.state.edgeStates,
},
});
}
// 下行 InstantActions 处理
public handleInstantActions(actions: any[]) {
// 1) 更新内部状态
// console.log(`${this.agvId} 收到即时动作`, actions);
actions.forEach((a) => {
switch (a.actionType) {
case "STOP":
this.state.driving = false;
break;
case "RESUME":
this.state.driving = true;
break;
case "CHARGE":
this.state.batteryState.charging = true;
break;
case "pick":
console.log("pick");
break;
case "drop":
console.log("drop");
break;
case "stopPause":
this.state.paused = true;
this.state.driving = false;
console.log("stopPause");
break;
case "startPose":
console.log("startPose");
this.state.driving = true;
break;
case "factsheetRequest":
console.log("factsheetRequest");
break;
case "instantActions":
console.log("instantActions");
break;
case "finePositioning":
console.log("finePositioning");
break;
case "startCharging":
console.log("startCharging");
break;
case "stopCharging":
console.log("stopCharging");
break;
case "initPosition":
console.log("initPosition");
break;
case "stateRequst":
console.log("stateRequst");
break;
case "logReport":
console.log("logReport");
break;
case "detectObject":
console.log("detectObject");
break;
case "waitForRtigger":
console.log("waitForRtigger");
break;
case "Standard":
console.log("Standard");
break;
case "switchMap":
console.log("switchMap");
break;
case "reloc":
console.log("reloc");
break;
case "turn":
console.log("turn");
break;
case "confirmLoc":
console.log("confirmLoc");
break;
case "cancelReloc":
console.log("cancelReloc");
break;
case "rotateAgv":
console.log("rotateAgv");
break;
case "rotateLoad":
console.log("rotateLoad");
break;
case "deviceSetup":
console.log("deviceSetup:", a);
break;
case "deviceWrite":
console.log("deviceWrite:", a);
break;
case "deviceDelete":
console.log("deviceDelete:", a);
break;
default:
console.warn(`${this.agvId} 未知即时动作`, a);
}
});
// 2) 主动通知主线程收到 InstantActions
self.postMessage({
type: "instantActionsForwarded",
agvId: this.agvId,
data: actions.map((a) => ({
actionType: a.actionType,
actionParameters: a.actionParameters,
actionDescription: a.actionDescription,
actionId: a.actionId,
blockingType: a.blockingType,
// …你需要的其它字段…
})),
});
}
sendOnlineStatusGrpc(data: any) {
// 若请求失败则保持当前状态
if (data === null) return;
// agvId: { manufacturer: "gateway", serialNumber: "ZKG-2" }
if (data.agvId.manufacturer === this.manufacturer
&& data.agvId.serialNumber === this.agvId
&& this.agvId === this.serialNumber) {
// console.log(`${this.agvId}: KV 数据请求成功`, data);
// 3) 计算是否在线
const lastSeen = data?.lastSeen
? Date.now() - new Date(data.lastSeen).getTime() < 3180 //如果上次 KV 中的 lastSeen 距今超过 3.18 秒,就算离线(无 connection
: false;
const isOnline = lastSeen && data.state === "ONLINE";
const newMode = isOnline ? "ONLINE" : "OFFLINE";
// 只有状态变化时才发送 Connection
if (newMode !== this.lastOnlineStatus) {
this.lastOnlineStatus = newMode;
this.sendConnection(this.lastOnlineStatus as any);
}
}
}
// 检查在线/离线:从主线程取 KV再比较时间若状态变更则发 Connection
private async checkOnlineStatus() {
// console.log(`${this.agvId} 检查在线状态`);
try {
// 1) 请求 KV
const requestId = Math.random().toString(36).substring(2, 15);
self.postMessage({
type: "requestKVDataOnline",
requestId,
key: `device-online:${this.manufacturer}/${this.serialNumber}`,
});
} catch (err) {
console.error(`${this.agvId}: 检查在线状态失败:`, err);
}
}
// 简易状态用于 UI
public getStatus() {
return {
agvId: this.agvId,
batteryCharge: this.batteryLevel,
timestamp: this.state.timestamp,
position: this.state.agvPosition,
driving: this.state.driving,
charging: this.state.batteryState.charging,
operatingMode: this.state.operatingMode,
};
}
}
// 主流程
async function main() {
console.log("agv_workermain");
// 设置事件监听器
eventHelper.addEventListener("test-event", (event) => {
console.log("🧪 AGV Worker 收到测试事件:", event);
});
eventHelper.addEventListener("grpc-reconnect", (event) => {
console.log("🔄 AGV Worker 收到 gRPC 重连指令:", event);
reconnectAll();
});
if (!SERVER_URL || devices.length === 0) {
console.error("缺少 SERVER_URL 或 无设备,无法启动");
return;
}
// 初始化 gRPC 流
if (!initGrpcStream()) {
console.error("gRPC 流初始化失败,无法启动");
return;
}
// 初始化所有 AGV 并发送上线消息
for (const dev of devices) {
const sim = new AgvSimulator(dev.serialNumber, dev.manufacturer, dev.serialNumber);
agvMap.set(dev.serialNumber, sim);
sim.sendConnection("OFFLINE"); // 发送 OFFLINE 状态
sim.startStatusUpdates();
await delay(200);
}
console.log("所有 AGV 已上线并开始状态上报");
// 发送 AGV Worker 启动完成事件
eventHelper.dispatchEvent("agv-worker-ready", {
deviceCount: devices.length,
timestamp: Date.now()
});
// 回主线程汇总状态
setInterval(() => {
const statuses = Array.from(agvMap.values()).map((s) => s.getStatus());
self.postMessage({ type: "devicesStatus", data: statuses });
}, 1000);
}
// worker 接收消息
self.addEventListener("message", (evt: MessageEvent) => {
const msg = evt.data;
if (msg.type === "init") {
SERVER_URL = msg.data.serverUrl;
self.postMessage({ type: "inited"});
console.log("设置 SERVER_URL =", SERVER_URL);
} else if (msg.type === "deviceList") {
devices = msg.data as Device[];
console.log("devices", devices[0]);
main().catch(err => console.error("主流程启动失败:", err));
} else if (msg.type === "shutdown") {
console.log("Shutdown关闭 gRPC 流 & 停止所有 AGV");
if (stream) {
try { stream.end(); } catch (e) { /* 忽略关闭错误 */ }
}
agvMap.forEach((s) => s.stopStatusUpdates());
setTimeout(() => self.close(), 500);
} else if (msg.type === "factsheetResponse") {
// const agvMessage = {
// agvId: msg.data.agvId,
// state: msg.data.factsheet
// // message_type 会由 oneof 自动处理,我们只需提供 state 字段
// };
// message Factsheet {
// uint32 headerId = 1;
// string timestamp = 2;
// string version = 3;
// string manufacturer = 4;
// string serialNumber = 5;
// TypeSpecification typeSpecification = 6;
// PhysicalParameters physicalParameters = 7;
// ProtocolLimits protocolLimits = 8;
// ProtocolFeatures protocolFeatures = 9;
// AgvGeometry agvGeometry = 10;
// LoadSpecification loadSpecification = 11;
// VehicleConfig vehicleConfig = 12;
// }
const factsheetMsg = {
agvId: msg.data.agvId.serialNumber,
factsheet: {
headerId: globalHeaderId++,
timestamp: new Date().toISOString(),
version: "2.0.0",
manufacturer: msg.data.agvId.manufacturer,
serialNumber: msg.data.agvId.serialNumber,
typeSpecification: msg.data.factsheet.typeSpecification,
physicalParameters: msg.data.factsheet.physicalParameters,
},
};
// console.log("==>factsheetResponse", factsheetMsg);
if (!sendToStream(factsheetMsg)) {
console.error(`${msg.data.agvId}: 发送状态到 factsheet 的 gRPC 服务器失败`);
}
} else if (msg.type === "requestKVData") {
// console.log("收到 Worker requestKVData 消息,准备更新设备状态",msg.data.agvId);
if(msg && msg.data && msg.data.agvId && msg.data.agvId.serialNumber) {
const agv = agvMap.get(msg.data.agvId.serialNumber);
if (agv) {
agv.sendStateToGrpc(msg.data);
}
}
} else if (msg.type === "requestKVDataOnline") {
// console.log("收到 Worker requestKVDataOnline 消息,准备更新设备状态",msg.data.agvId);
if(msg && msg.data && msg.data.agvId && msg.data.agvId.serialNumber) {
const agv = agvMap.get(msg.data.agvId.serialNumber);
if (agv) {
agv.sendOnlineStatusGrpc(msg.data);
}
}
}
});

188
cli.ts Normal file
View File

@ -0,0 +1,188 @@
import { DeviceManager } from "./device_manager.ts";
import { updateDeviceListFromConfig, removeDeviceFromWorker } from "./master_manager.ts";
// 处理设备管理命令
async function handleDeviceCommand(args: string[], masterWorker: Worker, instanceId: string) {
const deviceManager = new DeviceManager();
const subCommand = args[0];
try {
switch (subCommand) {
case "add":
if (args.length >= 3) {
await deviceManager.loadDevices();
const success = await deviceManager.addDevice(args[1], args[2]);
if (success) {
console.log("🔄 正在重新加载设备配置...");
// 读取配置文件并获取config对象
const configText = await Deno.readTextFile("./config.json");
const config = JSON.parse(configText);
await updateDeviceListFromConfig(masterWorker, config, instanceId);
}
} else {
console.log("用法: device add <manufacturer> <serialNumber>");
}
break;
case "remove":
if (args.length >= 3) {
await deviceManager.loadDevices();
const success = await deviceManager.removeDevice(args[1], args[2]);
if (success) {
console.log("🔄 正在重新加载设备配置...");
const configText = await Deno.readTextFile("./config.json");
const config = JSON.parse(configText);
await updateDeviceListFromConfig(masterWorker, config, instanceId);
}
} else {
console.log("用法: device remove <manufacturer> <serialNumber>");
}
break;
case "force-remove":
if (args.length >= 3) {
await deviceManager.loadDevices();
// 强制从配置文件中删除
await deviceManager.forceRemoveDevice(args[1], args[2]);
// 立即从Worker中移除
await removeDeviceFromWorker(masterWorker, args[1], args[2]);
console.log("✅ 设备已完全移除(配置文件 + 运行时状态)");
} else {
console.log("用法: device force-remove <manufacturer> <serialNumber>");
}
break;
case "instant-remove":
if (args.length >= 3) {
// 仅从运行时移除,不修改配置文件
await removeDeviceFromWorker(masterWorker, args[1], args[2]);
console.log("✅ 设备已从运行时移除(配置文件保持不变)");
} else {
console.log("用法: device instant-remove <manufacturer> <serialNumber>");
}
break;
case "list":
await deviceManager.loadDevices();
deviceManager.listDevices();
break;
case "status":
// 查询运行时设备状态
console.log("🔍 查询运行时设备状态...");
masterWorker.postMessage({ type: "discoverDevices" });
break;
case "reload":{
console.log("🔄 正在重新加载设备配置...");
const configText = await Deno.readTextFile("./config.json");
const config = JSON.parse(configText);
await updateDeviceListFromConfig(masterWorker, config, instanceId);
break;
}
case "generate":{
const count = parseInt(args[1]) || 10;
const prefix = args[2] || "TEST";
await deviceManager.loadDevices();
await deviceManager.generateTestDevices(count, prefix);
console.log("🔄 正在重新加载设备配置...");
const configText2 = await Deno.readTextFile("./config.json");
const config2 = JSON.parse(configText2);
await updateDeviceListFromConfig(masterWorker, config2, instanceId);
break;
}
default:
console.log(`
🔧 :
device add <manufacturer> <serialNumber> -
device remove <manufacturer> <serialNumber> - +
device force-remove <manufacturer> <serialNumber> - +
device instant-remove <manufacturer> <serialNumber> -
device list -
device status -
device reload -
device generate <count> [prefix] -
`);
}
} catch (error) {
console.error("❌ 设备命令执行失败:", error);
}
}
export function startCli(
masterWorker: Worker,
agvWorker: Worker,
sendNavigationOrder: () => void,
cancelOrder: (orderId: string) => void,
sendCustomCommand: (cmd: string) => void,
instanceId: string
) {
const decoder = new TextDecoder();
const reader = Deno.stdin.readable.getReader();
console.log("\n可用命令:");
console.log(" send - 发送新的导航订单");
console.log(" cancel <orderId> - 取消指定的订单");
console.log(" discover - 请求当前所有发现的设备信息");
console.log(" setcmd <command> - 发送自定义命令设置到 Worker");
console.log(" requestKV - 请求从 KV 中获取设备数据,并发送给 agvWorker");
console.log(" device add <manufacturer> <serialNumber> - 动态添加设备");
console.log(" device remove <manufacturer> <serialNumber> - 删除设备(配置+重载)");
console.log(" device force-remove <manufacturer> <serialNumber> - 强制删除设备");
console.log(" device instant-remove <manufacturer> <serialNumber> - 即时删除设备");
console.log(" device list - 列出所有配置的设备");
console.log(" device reload - 重新加载设备配置");
console.log(" device generate <count> [prefix] - 生成测试设备");
console.log(" exit - 退出程序");
(async () => {
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value);
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
const [cmd, ...args] = trimmed.split(/\s+/);
switch (cmd) {
case "send":
sendNavigationOrder();
break;
case "cancel":
if (args[0]) cancelOrder(args[0]);
else console.log("请提供订单ID");
break;
case "discover":
masterWorker.postMessage({ type: "discoverDevices" });
break;
case "setcmd":
if (args.length) sendCustomCommand(args.join(" "));
else console.log("请提供自定义命令内容");
break;
case "requestKV":
agvWorker.postMessage({ type: "requestKVData" });
break;
case "device":
await handleDeviceCommand(args, masterWorker, instanceId);
break;
case "exit":
masterWorker.postMessage({ type: "shutdown" });
agvWorker.postMessage("shutdown");
setTimeout(() => {
agvWorker.terminate();
masterWorker.terminate();
console.log("所有 Worker 已终止,程序退出");
Deno.exit(0);
}, 2000);
return;
default:
if (trimmed) console.log(`未知命令: ${trimmed}`);
console.log("可用命令: send, cancel <orderId>, discover, setcmd <command>, requestKV, device <subcommand>, exit");
break;
}
}
}
})();
}

12
config.json Normal file
View File

@ -0,0 +1,12 @@
{
"mqtt": {
"brokerUrl": "mqtt://10.2.0.6:1883",
"clientId": "transformer13",
"reconnectInterval": 5000,
"qos": 1
},
"serverUrl": "127.0.0.1:50052",
"interfaceName": "oagv",
"manufacturer": "gateway",
"mappingFile": "mapping.json"
}

283
debug_logger.ts Normal file
View File

@ -0,0 +1,283 @@
// debug_logger.ts - 调试日志模块
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
TRACE = 4
}
export interface LogConfig {
level: LogLevel;
enableTimestamp: boolean;
enableColors: boolean;
enableFileOutput: boolean;
logFilePath?: string;
modules: {
[moduleName: string]: LogLevel;
};
}
const DEFAULT_CONFIG: LogConfig = {
level: LogLevel.INFO,
enableTimestamp: true,
enableColors: true,
enableFileOutput: false,
modules: {}
};
class DebugLogger {
private config: LogConfig;
private logFile?: Deno.FsFile;
constructor(config: Partial<LogConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
if (this.config.enableFileOutput && this.config.logFilePath) {
this.initLogFile();
}
}
private async initLogFile() {
try {
this.logFile = await Deno.open(this.config.logFilePath!, {
create: true,
write: true,
append: true
});
} catch (error) {
console.error(`Failed to open log file: ${error}`);
}
}
private getTimestamp(): string {
if (!this.config.enableTimestamp) return "";
return `[${new Date().toISOString()}] `;
}
private getColorCode(level: LogLevel): string {
if (!this.config.enableColors) return "";
switch (level) {
case LogLevel.ERROR: return "\x1b[31m"; // Red
case LogLevel.WARN: return "\x1b[33m"; // Yellow
case LogLevel.INFO: return "\x1b[36m"; // Cyan
case LogLevel.DEBUG: return "\x1b[32m"; // Green
case LogLevel.TRACE: return "\x1b[35m"; // Magenta
default: return "";
}
}
private getResetCode(): string {
return this.config.enableColors ? "\x1b[0m" : "";
}
private getLevelName(level: LogLevel): string {
switch (level) {
case LogLevel.ERROR: return "ERROR";
case LogLevel.WARN: return "WARN ";
case LogLevel.INFO: return "INFO ";
case LogLevel.DEBUG: return "DEBUG";
case LogLevel.TRACE: return "TRACE";
default: return "UNKNOWN";
}
}
private shouldLog(level: LogLevel, module?: string): boolean {
// 检查模块特定的日志级别
if (module && this.config.modules[module] !== undefined) {
return level <= this.config.modules[module];
}
// 使用全局日志级别
return level <= this.config.level;
}
private formatMessage(level: LogLevel, module: string, message: string, ...args: any[]): string {
const timestamp = this.getTimestamp();
const colorCode = this.getColorCode(level);
const resetCode = this.getResetCode();
const levelName = this.getLevelName(level);
const formattedArgs = args.length > 0 ? " " + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(" ") : "";
return `${timestamp}${colorCode}[${levelName}]${resetCode} [${module}] ${message}${formattedArgs}`;
}
private async writeLog(formattedMessage: string) {
// 输出到控制台
console.log(formattedMessage);
// 输出到文件
if (this.logFile) {
try {
const encoder = new TextEncoder();
await this.logFile.write(encoder.encode(formattedMessage + "\n"));
await this.logFile.sync();
} catch (error) {
console.error(`Failed to write to log file: ${error}`);
}
}
}
log(level: LogLevel, module: string, message: string, ...args: any[]) {
if (!this.shouldLog(level, module)) return;
const formattedMessage = this.formatMessage(level, module, message, ...args);
this.writeLog(formattedMessage);
}
error(module: string, message: string, ...args: any[]) {
this.log(LogLevel.ERROR, module, message, ...args);
}
warn(module: string, message: string, ...args: any[]) {
this.log(LogLevel.WARN, module, message, ...args);
}
info(module: string, message: string, ...args: any[]) {
this.log(LogLevel.INFO, module, message, ...args);
}
debug(module: string, message: string, ...args: any[]) {
this.log(LogLevel.DEBUG, module, message, ...args);
}
trace(module: string, message: string, ...args: any[]) {
this.log(LogLevel.TRACE, module, message, ...args);
}
// 设置模块特定的日志级别
setModuleLevel(module: string, level: LogLevel) {
this.config.modules[module] = level;
}
// 设置全局日志级别
setGlobalLevel(level: LogLevel) {
this.config.level = level;
}
// 获取当前配置
getConfig(): LogConfig {
return { ...this.config };
}
// 关闭日志文件
async close() {
if (this.logFile) {
await this.logFile.close();
}
}
}
// 创建全局调试器实例
export const debugLogger = new DebugLogger({
level: LogLevel.DEBUG,
enableTimestamp: true,
enableColors: true,
enableFileOutput: false,
logFilePath: "./debug.log",
modules: {
"DEVICE_SIMULATOR": LogLevel.ERROR,
"MODBUS": LogLevel.ERROR,
"MQTT": LogLevel.ERROR,
"REGISTER_CONFIG": LogLevel.ERROR,
"DEVICE_MANAGER": LogLevel.ERROR,
"SIMULATOR_MAIN": LogLevel.ERROR
}
});
// 便捷的模块日志器创建函数
export function createModuleLogger(moduleName: string) {
return {
error: (message: string, ...args: any[]) => debugLogger.error(moduleName, message, ...args),
warn: (message: string, ...args: any[]) => debugLogger.warn(moduleName, message, ...args),
info: (message: string, ...args: any[]) => debugLogger.info(moduleName, message, ...args),
debug: (message: string, ...args: any[]) => debugLogger.debug(moduleName, message, ...args),
trace: (message: string, ...args: any[]) => debugLogger.trace(moduleName, message, ...args)
};
}
// 性能监控工具
export class PerformanceMonitor {
private timers: Map<string, number> = new Map();
private logger = createModuleLogger("PERFORMANCE");
start(label: string) {
this.timers.set(label, performance.now());
this.logger.trace(`⏱️ Started timer: ${label}`);
}
end(label: string): number {
const startTime = this.timers.get(label);
if (!startTime) {
this.logger.trace(`⚠️ Timer not found: ${label}`);
return 0;
}
const duration = performance.now() - startTime;
this.timers.delete(label);
this.logger.trace(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
return duration;
}
measure<T>(label: string, fn: () => T): T {
this.start(label);
try {
const result = fn();
this.end(label);
return result;
} catch (error) {
this.end(label);
this.logger.error(`❌ Error in ${label}:`, error);
throw error;
}
}
async measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
this.start(label);
try {
const result = await fn();
this.end(label);
return result;
} catch (error) {
this.end(label);
this.logger.error(`❌ Error in ${label}:`, error);
throw error;
}
}
}
export const perfMonitor = new PerformanceMonitor();
// 内存使用监控
export function logMemoryUsage(module: string) {
if (typeof Deno !== 'undefined' && Deno.memoryUsage) {
const memory = Deno.memoryUsage();
const logger = createModuleLogger(module);
logger.debug(`💾 Memory usage:`, {
rss: `${(memory.rss / 1024 / 1024).toFixed(2)}MB`,
heapTotal: `${(memory.heapTotal / 1024 / 1024).toFixed(2)}MB`,
heapUsed: `${(memory.heapUsed / 1024 / 1024).toFixed(2)}MB`,
external: `${(memory.external / 1024 / 1024).toFixed(2)}MB`
});
}
}
// 调试配置管理
export function setDebugLevel(level: LogLevel | string, module?: string) {
const logLevel = typeof level === 'string' ?
LogLevel[level.toUpperCase() as keyof typeof LogLevel] : level;
if (module) {
debugLogger.setModuleLevel(module, logLevel);
console.log(`🔧 Set debug level for ${module}: ${LogLevel[logLevel]}`);
} else {
debugLogger.setGlobalLevel(logLevel);
console.log(`🔧 Set global debug level: ${LogLevel[logLevel]}`);
}
}
// 调试工具已在上面导出

53
deno.json Normal file
View File

@ -0,0 +1,53 @@
{
"tasks": {
"check": "deno check agv_main.ts",
"check:watch": "deno check --watch agv_main.ts",
"run": "deno run --log-level=info --inspect --allow-net --allow-read --unstable-kv --allow-env --node-modules-dir agv_main.ts",
"run:watch": "deno run --watch --allow-net --allow-read --allow-hrtime --unstable-kv --allow-env --node-modules-dir agv_main.ts",
"compile": "deno compile --include agv_worker.ts --include agv_manager.ts --include master_manager.ts --include web_worker.ts --include cli.ts --include vda_worker.ts --include vda5050_transformer_worker.ts --allow-net --allow-read --unstable-kv --allow-env --node-modules-dir --output agv_api agv_main.ts",
"compile:all": "deno task compile:linux && deno task compile:windows && deno task compile:mac",
"compile:linux": "deno compile --allow-net --allow-read --allow-hrtime --unstable-kv --allow-env --node-modules-dir --target x86_64-unknown-linux-gnu --output agv_simulator_linux agv_main.ts",
"compile:windows": "deno compile --allow-net --allow-read --allow-hrtime --unstable-kv --allow-env --node-modules-dir --target x86_64-pc-windows-msvc --output agv_simulator_windows agv_main.ts",
"compile:mac": "deno compile --allow-net --allow-read --allow-hrtime --unstable-kv --allow-env --node-modules-dir --target x86_64-apple-darwin --output agv_simulator_mac agv_main.ts",
"clean": "rm -f agv_simulator agv_simulator_linux agv_simulator_windows agv_simulator_mac",
"install:deps": "deno cache --reload agv_main.ts",
"bundle": "deno bundle agv_main.ts > agv_simulator.js",
"sim": "deno run --allow-net --allow-read --allow-env --allow-write --allow-run simulator_main.ts",
"sim:compile": "deno compile --include simulator.ts --include device_simulator.ts --include simulator_config.ts --include debug_logger.ts --allow-net --allow-read --allow-env --allow-write --allow-run --output simulator simulator_main.ts",
"sim:compile:all": "deno task sim:compile:linux && deno task sim:compile:windows && deno task sim:compile:mac",
"sim:compile:linux": "deno compile --include simulator.ts --include device_simulator.ts --include simulator_config.ts --include debug_logger.ts --allow-net --allow-read --allow-env --allow-write --allow-run --target x86_64-unknown-linux-gnu --output simulator_linux simulator_main.ts",
"sim:compile:windows": "deno compile --include simulator.ts --include device_simulator.ts --include simulator_config.ts --include debug_logger.ts --allow-net --allow-read --allow-env --allow-write --allow-run --target x86_64-pc-windows-msvc --output simulator_windows simulator_main.ts",
"sim:compile:mac": "deno compile --include simulator.ts --include device_simulator.ts --include simulator_config.ts --include debug_logger.ts --allow-net --allow-read --allow-env --allow-write --allow-run --target x86_64-apple-darwin --output simulator_mac simulator_main.ts",
"sim:clean": "rm -f simulator simulator_linux simulator_windows simulator_mac"
},
"env": {
"DEBUG": "*"
},
"compilerOptions": {
"allowJs": true,
"lib": ["deno.window", "deno.worker"],
"strict": true
},
"importMap": "import_map.json",
"fmt": {
"files": {
"include": ["*.ts", "*.js", "*.json"]
},
"options": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": true,
"proseWrap": "preserve"
}
},
"lint": {
"files": {
"include": ["*.ts", "*.js"]
},
"rules": {
"tags": ["recommended"]
}
}
}

279
device_manager.ts Normal file
View File

@ -0,0 +1,279 @@
// device_manager.ts - 动态设备管理工具
interface DeviceConfig {
manufacturer: string;
serialNumber: string;
}
export class DeviceManager {
private devicesFilePath: string;
private devices: DeviceConfig[] = [];
constructor(devicesFilePath = "./devices.json") {
this.devicesFilePath = devicesFilePath;
}
/**
*
*/
async loadDevices(): Promise<DeviceConfig[]> {
try {
const text = await Deno.readTextFile(this.devicesFilePath);
this.devices = JSON.parse(text);
console.log(`📱 已加载 ${this.devices.length} 个设备`);
return this.devices;
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log("📄 devices.json 不存在,创建新文件");
this.devices = [];
await this.saveDevices();
return this.devices;
}
throw error;
}
}
/**
*
*/
async saveDevices(): Promise<void> {
const json = JSON.stringify(this.devices, null, 2);
await Deno.writeTextFile(this.devicesFilePath, json);
console.log(`💾 已保存 ${this.devices.length} 个设备到 ${this.devicesFilePath}`);
}
/**
*
*/
async addDevice(manufacturer: string, serialNumber: string): Promise<boolean> {
const deviceKey = `${manufacturer}-${serialNumber}`;
const exists = this.devices.some(d =>
d.manufacturer === manufacturer && d.serialNumber === serialNumber
);
if (exists) {
console.log(`⚠️ 设备 ${deviceKey} 已存在`);
return false;
}
this.devices.push({ manufacturer, serialNumber });
await this.saveDevices();
console.log(` 已添加设备: ${deviceKey}`);
return true;
}
/**
*
*/
async addDevices(newDevices: DeviceConfig[]): Promise<number> {
let addedCount = 0;
for (const device of newDevices) {
const exists = this.devices.some(d =>
d.manufacturer === device.manufacturer && d.serialNumber === device.serialNumber
);
if (!exists) {
this.devices.push(device);
addedCount++;
console.log(` 已添加设备: ${device.manufacturer}-${device.serialNumber}`);
} else {
console.log(`⚠️ 设备 ${device.manufacturer}-${device.serialNumber} 已存在,跳过`);
}
}
if (addedCount > 0) {
await this.saveDevices();
}
console.log(`✅ 批量添加完成: 新增 ${addedCount} 个设备`);
return addedCount;
}
/**
*
*/
async removeDevice(manufacturer: string, serialNumber: string): Promise<boolean> {
const deviceKey = `${manufacturer}-${serialNumber}`;
const initialLength = this.devices.length;
this.devices = this.devices.filter(d =>
!(d.manufacturer === manufacturer && d.serialNumber === serialNumber)
);
if (this.devices.length < initialLength) {
await this.saveDevices();
console.log(` 已删除设备: ${deviceKey}`);
return true;
} else {
console.log(`⚠️ 设备 ${deviceKey} 不存在`);
return false;
}
}
/**
* 使
*/
async forceRemoveDevice(manufacturer: string, serialNumber: string): Promise<boolean> {
const deviceKey = `${manufacturer}-${serialNumber}`;
const initialLength = this.devices.length;
// 从内存中移除
this.devices = this.devices.filter(d =>
!(d.manufacturer === manufacturer && d.serialNumber === serialNumber)
);
// 总是保存文件,确保一致性
await this.saveDevices();
if (this.devices.length < initialLength) {
console.log(`🗑️ 已强制删除设备: ${deviceKey}`);
} else {
console.log(`🗑️ 强制删除设备: ${deviceKey} (配置文件中不存在,但已清理运行时状态)`);
}
return true;
}
/**
*
*/
listDevices(): DeviceConfig[] {
console.log(`📋 当前设备列表 (${this.devices.length} 个):`);
this.devices.forEach((device, index) => {
console.log(` ${index + 1}. ${device.manufacturer}/${device.serialNumber}`);
});
return this.devices;
}
/**
*
*/
async clearDevices(): Promise<void> {
this.devices = [];
await this.saveDevices();
console.log("🗑️ 已清空所有设备");
}
/**
*
*/
async generateTestDevices(count: number, prefix = "TEST"): Promise<void> {
const testDevices: DeviceConfig[] = [];
for (let i = 0; i < count; i++) {
testDevices.push({
manufacturer: "SEER",
serialNumber: `${prefix}-${i}`
});
}
await this.addDevices(testDevices);
console.log(`🧪 已生成 ${count} 个测试设备`);
}
/**
*
*/
deviceExists(manufacturer: string, serialNumber: string): boolean {
return this.devices.some(d =>
d.manufacturer === manufacturer && d.serialNumber === serialNumber
);
}
/**
*
*/
getDeviceCount(): number {
return this.devices.length;
}
}
// 导出便捷函数
export async function addDeviceToConfig(manufacturer: string, serialNumber: string): Promise<boolean> {
const manager = new DeviceManager();
await manager.loadDevices();
return await manager.addDevice(manufacturer, serialNumber);
}
export async function removeDeviceFromConfig(manufacturer: string, serialNumber: string): Promise<boolean> {
const manager = new DeviceManager();
await manager.loadDevices();
return await manager.removeDevice(manufacturer, serialNumber);
}
export async function listConfigDevices(): Promise<DeviceConfig[]> {
const manager = new DeviceManager();
await manager.loadDevices();
return manager.listDevices();
}
// CLI 使用示例
if (import.meta.main) {
const manager = new DeviceManager();
await manager.loadDevices();
const args = Deno.args;
const command = args[0];
switch (command) {
case "add":
if (args.length >= 3) {
await manager.addDevice(args[1], args[2]);
} else {
console.log("用法: deno run device_manager.ts add <manufacturer> <serialNumber>");
}
break;
case "remove":
if (args.length >= 3) {
await manager.removeDevice(args[1], args[2]);
} else {
console.log("用法: deno run device_manager.ts remove <manufacturer> <serialNumber>");
}
break;
case "force-remove":
if (args.length >= 3) {
await manager.forceRemoveDevice(args[1], args[2]);
} else {
console.log("用法: deno run device_manager.ts force-remove <manufacturer> <serialNumber>");
}
break;
case "list":
manager.listDevices();
break;
case "clear":
await manager.clearDevices();
break;
case "generate":
const count = parseInt(args[1]) || 10;
const prefix = args[2] || "TEST";
await manager.generateTestDevices(count, prefix);
break;
default:
console.log(`
🔧
:
deno run --allow-read --allow-write device_manager.ts <command> [args]
:
add <manufacturer> <serialNumber> -
remove <manufacturer> <serialNumber> -
force-remove <manufacturer> <serialNumber> -
list -
clear -
generate <count> [prefix] -
:
deno run --allow-read --allow-write device_manager.ts add SEER AGV-001
deno run --allow-read --allow-write device_manager.ts list
deno run --allow-read --allow-write device_manager.ts generate 5 TEST
`);
}
}

190
device_protocol_config.ts Normal file
View File

@ -0,0 +1,190 @@
// device_protocol_config.ts - 设备协议配置示例
// 展示如何根据设备信息自动配置Modbus参数
import { createModuleLogger } from "./debug_logger.ts";
const logger = createModuleLogger("REGISTER_CONFIG");
// 获取默认轮询间隔(基于设备品牌优化)
export function getDefaultPollInterval(brandName?: string): number {
// 根据设备品牌返回优化的轮询间隔
const interval = (() => {
switch (brandName) {
case "西门子": return 500; // 西门子设备响应较快
case "台达": return 1000; // 台达设备标准间隔
case "三菱": return 1500; // 三菱设备较保守间隔
default: return 2000; // 通用设备默认间隔
}
})();
logger.debug("⏱️ Poll interval determined", {
brandName: brandName || "unknown",
interval: `${interval}ms`,
reason: brandName ? "brand_specific" : "default"
});
return interval;
}
// 寄存器定义接口
export interface RegisterDefinition {
fnCode: string; // 功能码1=读线圈2=读离散输入3=读保持寄存器4=读输入寄存器6=写单个寄存器
name: string; // 寄存器名称
bind?: string; // 可选:绑定 ID
regCount: string; // 寄存器数量
regAddress: string; // 寄存器地址
}
// 功能码映射到Modbus函数
const FUNCTION_CODE_MAP: Record<string, "readHoldingRegisters" | "readInputRegisters" | "readCoils" | "readDiscreteInputs"> = {
"1": "readCoils", // 读线圈
"2": "readDiscreteInputs", // 读离散输入
"3": "readHoldingRegisters", // 读保持寄存器
"4": "readInputRegisters", // 读输入寄存器
"6": "readHoldingRegisters" // 写单个寄存器(读取时用保持寄存器)
};
// 从寄存器定义创建轮询配置
export function createModbusPollConfigFromRegisters(registers: RegisterDefinition[]) {
logger.info("🔄 Creating Modbus polling configuration", {
totalRegisters: registers.length,
registers: registers.map(r => ({ name: r.name, fnCode: r.fnCode, address: r.regAddress }))
});
const pollConfig = [];
const skippedRegisters = [];
for (const reg of registers) {
const fnCode = reg.fnCode;
const modbusFunction = FUNCTION_CODE_MAP[fnCode];
logger.trace("🔍 Processing register", {
name: reg.name,
fnCode,
address: reg.regAddress,
count: reg.regCount,
modbusFunction
});
if (!modbusFunction) {
const warning = `Unsupported function code: ${fnCode} for register ${reg.name}`;
logger.warn("⚠️ " + warning, {
register: reg,
supportedCodes: Object.keys(FUNCTION_CODE_MAP)
});
console.warn(warning);
skippedRegisters.push({ register: reg.name, reason: "unsupported_function_code" });
continue;
}
// 只为读取功能码创建轮询配置功能码6是写操作但我们也可以读取它的当前值
if (["1", "2", "3", "4", "6"].includes(fnCode)) {
const pollItem = {
fn: modbusFunction,
start: parseInt(reg.regAddress),
len: parseInt(reg.regCount),
mqttKey: `device/register/${reg.name}`,
bind: reg.bind || reg.name
};
logger.debug("✅ Created poll config for register", {
register: reg.name,
pollItem
});
pollConfig.push(pollItem);
} else {
logger.warn("⚠️ Function code not supported for polling", {
register: reg.name,
fnCode,
reason: "not_readable"
});
skippedRegisters.push({ register: reg.name, reason: "not_readable" });
}
}
logger.info("🎯 Polling configuration creation completed", {
totalInput: registers.length,
totalOutput: pollConfig.length,
skipped: skippedRegisters.length,
skippedDetails: skippedRegisters,
successRate: `${((pollConfig.length / registers.length) * 100).toFixed(1)}%`
});
return pollConfig;
}
// 解析寄存器字符串
export function parseRegistersFromString(registersStr: string): RegisterDefinition[] {
logger.debug("📝 Parsing registers string", {
inputLength: registersStr.length,
inputPreview: registersStr.substring(0, 100) + (registersStr.length > 100 ? '...' : '')
});
try {
const parsed = JSON.parse(registersStr);
logger.trace("🔍 JSON parsing successful", {
parsedType: typeof parsed,
isArray: Array.isArray(parsed),
length: Array.isArray(parsed) ? parsed.length : 'not_array'
});
if (!Array.isArray(parsed)) {
logger.error("❌ Parsed data is not an array", { parsedType: typeof parsed, parsed });
return [];
}
const registers = parsed as RegisterDefinition[];
const validRegisters = [];
const invalidRegisters = [];
for (const reg of registers) {
const isValid = reg.fnCode && reg.name && reg.regCount && reg.regAddress;
logger.trace("🔍 Validating register", {
register: reg,
isValid,
missingFields: {
fnCode: !reg.fnCode,
name: !reg.name,
regCount: !reg.regCount,
regAddress: !reg.regAddress
}
});
if (isValid) {
validRegisters.push(reg);
} else {
invalidRegisters.push({
register: reg,
missingFields: Object.keys(reg).filter(key => !reg[key as keyof RegisterDefinition])
});
}
}
logger.info("✅ Register parsing completed", {
totalInput: registers.length,
validRegisters: validRegisters.length,
invalidRegisters: invalidRegisters.length,
invalidDetails: invalidRegisters,
successRate: `${((validRegisters.length / registers.length) * 100).toFixed(1)}%`
});
return validRegisters;
} catch (error) {
logger.error("❌ Failed to parse registers string", {
error: (error as Error).message,
inputString: registersStr
});
console.error("Failed to parse registers string:", error);
return [];
}
}
export default {
getDefaultPollInterval,
createModbusPollConfigFromRegisters,
parseRegistersFromString
};

1374
device_simulator.ts Normal file

File diff suppressed because it is too large Load Diff

538
device_simulator_main.ts Normal file
View File

@ -0,0 +1,538 @@
// device_simulator_main.ts - 设备模拟器管理器
import { loadConfig, RawConfig } from "./simulator_config.ts";
import { connect, MqttClient } from "npm:mqtt@5.10.1";
interface DeviceInfo {
ip: string;
port: string;
slaveId: string;
deviceName: string;
protocolType: string;
brandName: string;
}
interface DeviceWorkerMessage {
type: "init" | "close" | "reconnect" | "action";
data?: any;
}
interface DeviceMainMessage {
type: "ready" | "error" | "status" | "log" | "reset";
data?: any;
}
interface DeviceWorker {
worker: Worker;
deviceId: string;
status: "starting" | "running" | "error" | "stopped";
config: any;
deviceInfo: DeviceInfo;
}
class DeviceSimulatorManager {
private workers: Map<string, DeviceWorker> = new Map();
private config!: RawConfig;
private mqttClient!: MqttClient;
private isConnected = false;
async initialize() {
this.config = await loadConfig();
console.log("📋 Device Simulator Manager initialized");
// 连接MQTT并监听instantActions消息
await this.connectMqtt();
}
private async connectMqtt() {
const brokerUrl = `mqtt://${this.config.mqttBroker.host}:${this.config.mqttBroker.port}`;
console.log(`🔌 Connecting to MQTT broker: ${brokerUrl}`);
this.mqttClient = connect(brokerUrl, {
clean: true,
keepalive: 60,
});
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("MQTT connection timeout"));
}, 10000);
this.mqttClient.on("connect", () => {
clearTimeout(timeout);
this.isConnected = true;
console.log("✅ Connected to MQTT broker");
this.setupMqttHandlers();
resolve();
});
this.mqttClient.on("error", (error: Error) => {
clearTimeout(timeout);
console.error(`❌ MQTT connection error: ${error.message}`);
reject(error);
});
});
}
private setupMqttHandlers() {
// 订阅所有设备的instantActions主题
const instantActionsTopic = `${this.config.mqttBroker.vdaInterface}/+/instantActions`;
this.mqttClient.subscribe(instantActionsTopic, (err: Error | null) => {
if (err) {
console.error(`❌ Failed to subscribe to ${instantActionsTopic}: ${err.message}`);
} else {
console.log(`📡 Subscribed to ${instantActionsTopic}`);
}
});
this.mqttClient.on("message", (topic: string, message: Uint8Array) => {
try {
const data = JSON.parse(new TextDecoder().decode(message));
console.log(`📥 Received instantActions message on ${topic}`);
if (topic.includes("/instantActions")) {
this.handleInstantActionsMessage(data);
}
} catch (error) {
console.error(`❌ Error parsing MQTT message: ${(error as Error).message}`);
}
});
this.mqttClient.on("disconnect", () => {
this.isConnected = false;
console.log("⚠️ Disconnected from MQTT broker");
});
this.mqttClient.on("error", (error: Error) => {
console.error(`❌ MQTT error: ${error.message}`);
});
}
private async handleInstantActionsMessage(message: any) {
console.log("🔍 Processing instantActions message...");
try {
// 支持两种格式标准VDA 5050的instantActions和实际使用的actions
const actions = message.instantActions || message.actions || [];
if (!Array.isArray(actions)) {
console.error("❌ Invalid actions format - not an array");
return;
}
for (const action of actions) {
if (action.actionType === "deviceSetup") {
await this.handleDeviceAction(action);
}
}
} catch (error) {
console.error(`❌ Error processing instantActions: ${(error as Error).message}`);
}
}
// 从actionParameters中提取设备信息
private extractDeviceInfo(actionParameters: any[]): DeviceInfo | null {
const params: Record<string, string> = {};
for (const param of actionParameters) {
if (param.key && param.value) {
params[param.key] = param.value;
}
}
const required = ['ip', 'port', 'slaveId'];
for (const field of required) {
if (!params[field]) {
console.error(`❌ Missing required parameter: ${field}`);
return null;
}
}
return {
ip: params.ip,
port: params.port,
slaveId: params.slaveId,
deviceName: params.deviceName || 'Unknown Device',
protocolType: params.protocolType || 'Unknown Protocol',
brandName: params.brandName || 'Unknown Brand'
};
}
// 生成设备ID
private generateDeviceId(deviceInfo: DeviceInfo): string {
return `${deviceInfo.ip}-${deviceInfo.port}-${deviceInfo.slaveId}`;
}
// 处理instantActions消息动态创建设备模拟器
async handleDeviceAction(actionMessage: any): Promise<void> {
console.log("🔧 Processing device action:", actionMessage.actionType);
if (!actionMessage.actionParameters || !Array.isArray(actionMessage.actionParameters)) {
console.error("❌ Invalid action parameters");
return;
}
const deviceInfo = this.extractDeviceInfo(actionMessage.actionParameters);
if (!deviceInfo) {
console.error("❌ Failed to extract device info");
return;
}
const deviceId = this.generateDeviceId(deviceInfo);
// 检查设备模拟器是否已存在
if (this.workers.has(deviceId)) {
console.log(`📱 Device ${deviceId} already exists, sending action to existing worker`);
const worker = this.workers.get(deviceId)!;
worker.worker.postMessage({
type: "action",
data: actionMessage
} as DeviceWorkerMessage);
return;
}
// 创建新的设备模拟器
console.log(`🆕 Creating new device simulator for ${deviceId}`);
console.log(`📱 Device: ${deviceInfo.deviceName} (${deviceInfo.brandName})`);
console.log(`🔌 Protocol: ${deviceInfo.protocolType}`);
await this.startDeviceWorker(deviceId, deviceInfo, actionMessage);
}
private async startDeviceWorker(deviceId: string, deviceInfo: DeviceInfo, initialAction: any): Promise<void> {
console.log(`🔧 Creating device worker for ${deviceId}`);
const worker = new Worker(new URL("./device_simulator.ts", import.meta.url).href, {
type: "module",
});
const workerConfig = {
deviceId,
deviceInfo,
vdaInterface: this.config.mqttBroker.vdaInterface,
mqtt: {
brokerUrl: `mqtt://${this.config.mqttBroker.host}:${this.config.mqttBroker.port}`,
options: {
clean: true,
keepalive: 60,
}
},
vehicle: {
manufacturer: deviceInfo.brandName,
serialNumber: deviceId,
vdaVersion: this.config.vehicle.vdaVersion,
}
};
const deviceWorker: DeviceWorker = {
worker,
deviceId,
status: "starting",
config: workerConfig,
deviceInfo,
};
this.workers.set(deviceId, deviceWorker);
this.setupDeviceWorkerHandlers(deviceWorker);
// 发送初始化消息
worker.postMessage({ type: "init", data: workerConfig } as DeviceWorkerMessage);
// 等待worker准备就绪然后发送初始action
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
deviceWorker.status = "error";
reject(new Error(`Device worker ${deviceId} failed to start within timeout`));
}, 15000);
const originalHandler = worker.onmessage;
worker.onmessage = (event: MessageEvent<DeviceMainMessage>) => {
if (event.data.type === "ready") {
clearTimeout(timeout);
deviceWorker.status = "running";
worker.onmessage = originalHandler;
console.log(`✅ Device worker ${deviceId} started successfully`);
// 发送初始action
worker.postMessage({
type: "action",
data: initialAction
} as DeviceWorkerMessage);
resolve();
} else if (event.data.type === "error") {
clearTimeout(timeout);
deviceWorker.status = "error";
worker.onmessage = originalHandler;
reject(new Error(`Device worker ${deviceId} failed to start: ${event.data.data?.error}`));
}
};
});
}
private setupDeviceWorkerHandlers(deviceWorker: DeviceWorker) {
const { worker, deviceId } = deviceWorker;
worker.onmessage = (event: MessageEvent<DeviceMainMessage>) => {
const { type, data } = event.data;
switch (type) {
case "ready":
deviceWorker.status = "running";
console.log(`✅ Device ${deviceId} is ready`);
break;
case "error":
deviceWorker.status = "error";
console.error(`❌ Device ${deviceId} error:`, data?.error);
break;
case "status":
console.log(`📊 Device ${deviceId} status:`, data?.status);
if (data?.status === "closed") {
deviceWorker.status = "stopped";
}
break;
case "log":
// 转发日志到控制台
console.log(data?.message);
break;
case "reset":
console.log(`🔄 Worker reset requested for device ${deviceId}: ${data?.reason}`);
console.log(`Error: ${data?.error}`);
this.handleDeviceWorkerReset(deviceWorker, data);
break;
default:
console.warn(`Unknown message type from device ${deviceId}:`, type);
}
};
worker.onerror = (error) => {
deviceWorker.status = "error";
console.error(`❌ Device worker ${deviceId} error:`, error.message);
};
worker.onmessageerror = (error) => {
console.error(`❌ Message error from device worker ${deviceId}:`, error);
};
}
private async handleDeviceWorkerReset(deviceWorker: DeviceWorker, resetData: any): Promise<void> {
const { deviceId } = deviceWorker;
console.log(`🔄 Resetting device worker ${deviceId} due to: ${resetData?.reason}`);
try {
// 终止当前worker
deviceWorker.worker.terminate();
// 创建新的worker
const newWorker = new Worker(new URL("./device_simulator.ts", import.meta.url).href, {
type: "module",
});
// 更新worker引用
deviceWorker.worker = newWorker;
deviceWorker.status = "starting";
// 重新设置消息处理
this.setupDeviceWorkerHandlers(deviceWorker);
// 重新初始化
newWorker.postMessage({ type: "init", data: deviceWorker.config } as DeviceWorkerMessage);
console.log(`✅ Device worker ${deviceId} reset completed`);
} catch (error) {
console.error(`❌ Failed to reset device worker ${deviceId}:`, (error as Error).message);
deviceWorker.status = "error";
}
}
async stopDeviceWorker(deviceId: string): Promise<void> {
const deviceWorker = this.workers.get(deviceId);
if (!deviceWorker) {
console.warn(`⚠️ Device worker ${deviceId} not found`);
return;
}
console.log(`🛑 Stopping device worker ${deviceId}`);
return new Promise((resolve) => {
const timeout = setTimeout(() => {
deviceWorker.worker.terminate();
this.workers.delete(deviceId);
console.log(`🔄 Force terminated device worker ${deviceId}`);
resolve();
}, 5000);
deviceWorker.worker.onmessage = (event: MessageEvent<DeviceMainMessage>) => {
if (event.data.type === "status" && event.data.data?.status === "closed") {
clearTimeout(timeout);
deviceWorker.worker.terminate();
this.workers.delete(deviceId);
console.log(`✅ Device worker ${deviceId} stopped gracefully`);
resolve();
}
};
deviceWorker.worker.postMessage({ type: "close" } as DeviceWorkerMessage);
});
}
async stopAllDeviceWorkers(): Promise<void> {
console.log("🛑 Stopping all device workers...");
const stopPromises = Array.from(this.workers.keys()).map(deviceId => this.stopDeviceWorker(deviceId));
await Promise.all(stopPromises);
console.log("✅ All device workers stopped");
// 关闭MQTT连接
if (this.mqttClient && this.isConnected) {
try {
await this.mqttClient.endAsync();
console.log("✅ MQTT connection closed");
} catch (error) {
console.error(`❌ Error closing MQTT connection: ${(error as Error).message}`);
}
}
}
getDeviceWorkerStatus(deviceId?: string) {
if (deviceId) {
const worker = this.workers.get(deviceId);
return worker ? { deviceId, status: worker.status, deviceInfo: worker.deviceInfo } : null;
}
return Array.from(this.workers.entries()).map(([id, worker]) => ({
deviceId: id,
status: worker.status,
deviceInfo: worker.deviceInfo
}));
}
// 命令行接口
async handleCommand(command: string, args: string[] = []) {
const cmd = command.toLowerCase();
switch (cmd) {
case "status":{
const statuses = this.getDeviceWorkerStatus();
console.log("📊 Device Worker Status:");
if (Array.isArray(statuses)) {
statuses.forEach((s: any) => {
console.log(` ${s.deviceId}: ${s.status}`);
console.log(` Device: ${s.deviceInfo.deviceName} (${s.deviceInfo.brandName})`);
console.log(` Address: ${s.deviceInfo.ip}:${s.deviceInfo.port}/${s.deviceInfo.slaveId}`);
});
}
break;
}
case "stop":
if (args[0]) {
await this.stopDeviceWorker(args[0]);
} else {
await this.stopAllDeviceWorkers();
}
break;
case "list":{
const devices = this.getDeviceWorkerStatus();
console.log("📱 Active Device Simulators:");
if (Array.isArray(devices)) {
devices.forEach((d: any) => {
console.log(` ${d.deviceId} - ${d.deviceInfo.deviceName}`);
});
}
break;
}
case "help":
console.log(`
📖 Device Simulator Commands:
status [deviceId] - Show device worker status
stop [deviceId] - Stop device worker(s)
list - List all active devices
quit/exit - Exit the manager
help - Show this help
`);
break;
case "quit":
case "exit":
await this.stopAllDeviceWorkers();
Deno.exit(0);
break;
default:
console.log(`❌ Unknown command: ${command}. Type 'help' for available commands.`);
}
}
async startInteractiveMode() {
console.log(`
🎮 Device Simulator Manager - Interactive Mode
Type 'help' for available commands
Type 'quit' or 'exit' to stop all workers and exit
`);
const encoder = new TextEncoder();
const decoder = new TextDecoder();
while (true) {
await Deno.stdout.write(encoder.encode("device-sim> "));
const buf = new Uint8Array(1024);
const n = await Deno.stdin.read(buf);
if (n === null) break;
const input = decoder.decode(buf.subarray(0, n)).trim();
if (!input) continue;
const [command, ...args] = input.split(/\s+/);
try {
await this.handleCommand(command, args);
} catch (error) {
console.error(`❌ Command failed:`, (error as Error).message);
}
}
}
}
// 主程序入口
if (import.meta.main) {
const manager = new DeviceSimulatorManager();
try {
await manager.initialize();
console.log(`
🎯 Device Simulator Manager started!
📡 Listening for instantActions messages on MQTT
🔧 Will automatically create device simulators when deviceSetup actions are received
`);
// 处理进程信号
Deno.addSignalListener("SIGINT", async () => {
console.log("\n⚠ Received SIGINT, shutting down...");
await manager.stopAllDeviceWorkers();
Deno.exit(0);
});
if (Deno.build.os !== "windows") {
Deno.addSignalListener("SIGTERM", async () => {
console.log("\n⚠ Received SIGTERM, shutting down...");
await manager.stopAllDeviceWorkers();
Deno.exit(0);
});
}
// 启动交互模式
await manager.startInteractiveMode();
} catch (error) {
console.error("❌ Failed to start device simulator manager:", (error as Error).message);
Deno.exit(1);
}
}

111
devices.json Normal file
View File

@ -0,0 +1,111 @@
[
{
"manufacturer": "SEER",
"serialNumber": "ZKG-0"
},
{
"manufacturer": "SEER",
"serialNumber": "ZKG-1"
},
{
"manufacturer": "SEER",
"serialNumber": "ZKG-2"
},
{
"manufacturer": "SEER",
"serialNumber": "ZKG-3"
},
{
"manufacturer": "SEER",
"serialNumber": "ZKG-4"
},
{
"manufacturer": "SEER",
"serialNumber": "ZKG-5"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-01"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-02"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-03"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-04"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-05"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-06"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-07"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-08"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-09"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-010"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-011"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-012"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-013"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-014"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-015"
},
{
"manufacturer": "SEER",
"serialNumber": "SIM-016"
},
{
"manufacturer": "SEER",
"serialNumber": "EXT-0"
},
{
"manufacturer": "SEER",
"serialNumber": "EXT-1"
},
{
"manufacturer": "SEER",
"serialNumber": "10.2.0.10-5020-17"
},
{
"manufacturer": "SEER",
"serialNumber": "10.2.0.10-5021-17"
},
{
"manufacturer": "SEER",
"serialNumber": "10.2.0.10-5022-17"
}
]

139
event_manager.ts Normal file
View File

@ -0,0 +1,139 @@
// event_manager.ts
// 全局事件管理器
export interface GlobalEvent {
type: string;
source: string;
target?: string;
data?: any;
timestamp?: number;
}
export class GlobalEventManager extends EventTarget {
private workers: Map<string, Worker> = new Map();
private eventLog: GlobalEvent[] = [];
private maxLogSize = 1000;
constructor() {
super();
console.log("🎯 全局事件管理器已启动");
}
/**
* Worker
*/
registerWorker(name: string, worker: Worker) {
this.workers.set(name, worker);
console.log(`📝 已注册 Worker: ${name}`);
// 监听来自 Worker 的事件请求
worker.onmessage = (event: MessageEvent) => {
const message = event.data;
if (message.type === "dispatchGlobalEvent") {
this.dispatchGlobalEvent(message.event);
} else if (message.type === "addEventListener") {
this.addWorkerListener(name, message.eventType, message.listenerId);
}
};
}
/**
*
*/
dispatchGlobalEvent(eventData: GlobalEvent) {
eventData.timestamp = Date.now();
// 记录事件日志
this.logEvent(eventData);
console.log(`🚀 分发全局事件: ${eventData.type} (来源: ${eventData.source})`);
// 创建自定义事件
const customEvent = new CustomEvent(eventData.type, {
detail: eventData
});
// 在主线程分发事件
this.dispatchEvent(customEvent);
// 转发给指定的 Worker 或所有 Worker
if (eventData.target) {
const targetWorker = this.workers.get(eventData.target);
if (targetWorker) {
targetWorker.postMessage({
type: "globalEvent",
event: eventData
});
}
} else {
// 广播给所有 Worker
this.workers.forEach((worker, name) => {
if (name !== eventData.source) { // 不发回给发送者
worker.postMessage({
type: "globalEvent",
event: eventData
});
}
});
}
}
/**
* Worker
*/
private addWorkerListener(workerName: string, eventType: string, listenerId: string) {
this.addEventListener(eventType, (event: Event) => {
const customEvent = event as CustomEvent<GlobalEvent>;
const worker = this.workers.get(workerName);
if (worker) {
worker.postMessage({
type: "globalEventReceived",
eventType,
listenerId,
event: customEvent.detail
});
}
});
}
/**
*
*/
private logEvent(event: GlobalEvent) {
this.eventLog.push(event);
if (this.eventLog.length > this.maxLogSize) {
this.eventLog.shift();
}
}
/**
*
*/
getEventLog(): GlobalEvent[] {
return [...this.eventLog];
}
/**
*
*/
getRecentEvents(count: number = 10): GlobalEvent[] {
return this.eventLog.slice(-count);
}
/**
*
*/
clearEventLog() {
this.eventLog = [];
}
/**
* Worker
*/
getRegisteredWorkers(): string[] {
return Array.from(this.workers.keys());
}
}
// 创建全局单例
export const globalEventManager = new GlobalEventManager();

12
generate_devices.ts Normal file
View File

@ -0,0 +1,12 @@
// Script to generate 3000 devices
const devices = [];
for (let i = 0; i < 3000; i++) {
devices.push({
manufacturer: "SEER",
serialNumber: `ZKG-${i}`
});
}
await Deno.writeTextFile("devices.json", JSON.stringify(devices, null, 2));
console.log("Generated 3000 devices in devices.json");

12
import_map.json Normal file
View File

@ -0,0 +1,12 @@
{
"imports": {
"vda-5050-lib": "npm:vda-5050-lib",
"mqtt": "npm:mqtt",
"@grpc/": "npm:@grpc/",
"uuid": "npm:uuid",
"std/": "https://deno.land/std/",
"@hono/hono": "jsr:@hono/hono@^4.6.11",
"@hono/hono/serve-static": "jsr:@hono/hono@^4.6.11/serve-static",
"@hono/hono/html": "jsr:@hono/hono@^4.6.11/html"
}
}

144
mapping.json Normal file
View File

@ -0,0 +1,144 @@
[
{
"sourceTopic": "uagv/v2/SEER/+/state",
"targetTopicTemplate": "oagv/v2/{instanceId}/{agvId}/state",
"mapping": {
"version": { "op": "const", "value": "2.0.0", "source": "version" },
"headerId": "headerId",
"timestamp": "timestamp",
"manufacturer": "manufacturer",
"serialNumber": "serialNumber",
"orderId": "orderId",
"orderUpdateId": "orderUpdateId",
"zoneSetId": "zoneSetId",
"lastNodeId": "lastNodeId",
"lastNodeSequenceId": "lastNodeSequenceId",
"nodeStates": "nodeStates",
"edgeStates": {
"type": "array",
"source": "edgeStates",
"mapping": {
"edgeId": "edgeId",
"sequenceId": "sequenceId",
"edgeDescription": "edgeDescription",
"released": "released"
}
},
"driving": "driving",
"waitingForInteractionZoneRelease": "waitingForInteractionZoneRelease",
"paused": "paused",
"actionStates": "actionStates",
"agvPosition": "agvPosition",
"operatingMode": "operatingMode",
"batteryState": "batteryState",
"errors": "errors",
"safetyState": "safetyState",
"information": {
"type": "array",
"source": "information",
"mapping": {
"infoType": "infoType",
"infoLevel": "infoLevel",
"infoDescription": "infoDescription",
"infoReferences": {
"type": "array",
"source": "infoReferences",
"mapping": {
"referenceKey": "referenceKey",
"referenceValue": {
"type": "array",
"source": "referenceValue",
"op": "toString"
}
}
}
}
}
}
},
{
"sourceTopic": "uagv/v2/SEER/+/connection",
"targetTopicTemplate": "oagv/v2/{instanceId}/{agvId}/connection",
"mapping": {
"version": { "op": "const", "value": "2.0.0", "source": "version" },
"headerId": "headerId",
"timestamp": "timestamp",
"manufacturer": "manufacturer",
"serialNumber": "serialNumber",
"connectionState": "connectionState"
}
},
{
"sourceTopic": "uagv/v2/SEER/+/factsheet",
"targetTopicTemplate": "oagv/v2/{instanceId}/{agvId}/factsheet",
"mapping": {
"version": { "op": "const", "value": "2.0.0", "source": "version" },
"headerId": "headerId",
"timestamp": "timestamp",
"manufacturer": "manufacturer",
"serialNumber": "serialNumber",
"typeSpecification": {
"type": "object",
"source": "typeSpecification",
"mapping": {
"seriesName": "seriesName",
"seriesDescription": "seriesDescription",
"agvKinematic": "agvKinematic",
"agvClass": "agvClass",
"maxLoadMass": "maxLoadMass",
"localizationTypes": {
"op": "const",
"value": ["NATURAL"],
"source": "localizationTypes"
},
"navigationTypes": "navigationTypes"
}
},
"physicalParameters": {
"type": "object",
"source": "physicalParameters",
"mapping": {
"speedMin": "speedMin",
"speedMax": "speedMax",
"accelerationMax": "accelerationMax",
"decelerationMax": "decelerationMax",
"heightMin": "heightMin",
"heightMax": "heightMax",
"width": "width",
"length": "length"
}
},
"localizationParameters": "localizationParameters"
}
},
{
"sourceTopic": "oagv/v2/{instanceId}/+/instantActions",
"targetTopicTemplate": "uagv/v2/SEER/{agvId}/instantActions",
"mapping": {
"version": "version",
"headerId": "headerId",
"timestamp": "timestamp",
"manufacturer": { "op": "const", "value": "SEER", "source": "manufacturer" },
"serialNumber": "serialNumber",
"actions": "actions",
"actionParameters": "actionParameters"
}
},
{
"sourceTopic": "oagv/v2/{instanceId}/+/order",
"targetTopicTemplate": "uagv/v2/SEER/{agvId}/order",
"mapping": {
"version": "version",
"headerId": "headerId",
"timestamp": "timestamp",
"manufacturer": { "op": "const", "value": "SEER", "source": "manufacturer" },
"serialNumber": "serialNumber",
"edges": "edges",
"nodes": "nodes",
"released": "released",
"sequenceId": "sequenceId",
"orderId": "orderId",
"orderUpdateId": "orderUpdateId"
}
}
]

309
master_manager.ts Normal file
View File

@ -0,0 +1,309 @@
// master_manager.ts
// Module to setup and handle the Master Controller (VDA 5050) Worker
import { initAgvWorker, setupAgvWorker } from './agv_manager.ts';
/**
* Sets up the Master Controller Worker, wiring message handlers and returning the instance.
* @param kv Deno KV instance for storing/retrieving AGV state
* @param webWorker Web Worker for UI updates
* @param masterWorker The master VDA 5050 Worker to forward AGV events to
* @param agvWorker The AGV Worker instance
* @returns The initialized Master Controller Worker
*/
export function setupMasterWorker(
kv: Deno.Kv,
webWorker: Worker,
masterWorker: Worker,
agvWorker: Worker,
config: any,
mappings: any,
instanceId: string
): Worker {
console.log("VDA 5050 Master Controller Worker 已启动", instanceId);
masterWorker.onmessage = async (event: MessageEvent) => {
const message = event.data;
try {
switch (message.type) {
case "started":
// 同步通知 Master Worker 初始化完成
masterWorker.postMessage({ type: "started" });
break;
case "factsheet": {
// console.log("收到 master Worker factsheet 消息", message);
const { agvId, factsheet, timestamp } = message.data;
const deviceKey = `device-factsheet:${agvId.manufacturer}/${agvId.serialNumber}`;
const res = await kv.get([deviceKey]);
const newData = { ...(res.value || {}), agvId, factsheet, timestamp };
await kv.set([deviceKey], newData);
agvWorker.postMessage({ type: "factsheetResponse", data: { agvId, factsheet, timestamp } });
break
}
case "connectionState": {
const { agvId, state, timestamp } = message.data;
const deviceKey = `device-online:${agvId.manufacturer}/${agvId.serialNumber}`;
const res = await kv.get([deviceKey]);
const newData = { ...(res.value || {}), agvId, state, lastSeen: timestamp };
let needsUpdate = false;
if (!res.value) {
needsUpdate = true;
} else {
const existing = res.value as any;
if (existing.state !== state || existing.lastSeen !== timestamp ||
JSON.stringify(existing.agvId) !== JSON.stringify(agvId)) {
needsUpdate = true;
}
}
if (needsUpdate) {
// console.log("1----------------更新设备状态---------------<",deviceKey, newData);
await kv.set([deviceKey], newData);
}
masterWorker.postMessage({ type: "connectionState", data: { agvId, state, timestamp } });
break;
}
case "stateUpdate": {
const { agvId, state, timestamp } = message.data;
const deviceKey = `device:${agvId.manufacturer}/${agvId.serialNumber}`;
const res = await kv.get([deviceKey]);
const newData = { ...(res.value || {}), agvId, state, lastSeen: timestamp };
let needsUpdate = false;
if (!res.value) {
needsUpdate = true;
} else {
const existing = res.value as any;
if (existing.lastSeen !== timestamp ||
JSON.stringify(existing.state) !== JSON.stringify(state)) {
needsUpdate = true;
}
}
if (needsUpdate) {
// console.log("2----------------更新设备状态---------------<",deviceKey, newData);
await kv.set([deviceKey], newData);
}
masterWorker.postMessage({ type: "stateUpdate", data: { agvId, state, timestamp } });
webWorker.postMessage({ type: "positionUpdate", data: {
agvId: { manufacturer: agvId.manufacturer, serialNumber: agvId.serialNumber },
position: { x: state.agvPosition.x, y: state.agvPosition.y, theta: state.agvPosition.theta }
}});
break;
}
case "deviceDiscovered": {
// console.log("收到 deviceDiscovered 消息", message);
const { agvId, timestamp, isOnline } = message.data;
const deviceKey = `device-discovered:${agvId.manufacturer}/${agvId.serialNumber}`;
const res = await kv.get([deviceKey]);
const newData = { agvId, lastSeen: timestamp, isOnline };
let needsUpdate = false;
if (!res.value) {
needsUpdate = true;
} else {
const existing = res.value as any;
if (existing.lastSeen !== timestamp || existing.isOnline !== isOnline ||
JSON.stringify(existing.agvId) !== JSON.stringify(agvId)) {
needsUpdate = true;
}
}
if (needsUpdate) {
// console.log("3----------------更新设备状态---------------<",deviceKey, newData);
await kv.set([deviceKey], newData);
}
masterWorker.postMessage({ type: "deviceDiscovered", data: { agvId, timestamp, isOnline } });
break;
}
case "devicesList":
masterWorker.postMessage({ type: "devicesList", data: message.data });
break;
case "orderSent":
masterWorker.postMessage({ type: "orderSent", orderId: message.orderId });
break;
case "orderCompleted":
masterWorker.postMessage({
type: "orderCompleted",
orderId: message.orderId,
withError: message.withError,
byCancelation: message.byCancelation,
active: message.active
});
break;
case "nodeTraversed":
masterWorker.postMessage({ type: "nodeTraversed", data: message.data });
break;
case "edgeTraversing":
masterWorker.postMessage({ type: "edgeTraversing", data: message.data });
break;
case "edgeTraversed":
masterWorker.postMessage({ type: "edgeTraversed", data: message.data });
break;
case "orderCancelled":
masterWorker.postMessage({ type: "orderCancelled", orderId: message.orderId });
break;
case "commandSettings":
masterWorker.postMessage({ type: "commandSettings", data: message.data });
break;
case "reconnect-all":
console.log("收到 master1 Worker reconnect-all 消息,准备重连");
reconnectAllWorker(kv, webWorker, masterWorker, agvWorker, config, mappings, instanceId);
break;
case "deviceRemoved": {
const { manufacturer, serialNumber, deviceKey, remainingDevices } = message.data;
console.log(`🗑️ 设备已从VDA Worker中移除: ${manufacturer}/${serialNumber}`);
console.log(`📊 剩余设备数量: ${remainingDevices}`);
// 清理KV存储中的设备数据
try {
const keysToDelete = [
`device:${manufacturer}/${serialNumber}`,
`device-online:${manufacturer}/${serialNumber}`,
`device-discovered:${manufacturer}/${serialNumber}`,
`device-factsheet:${manufacturer}/${serialNumber}`
];
for (const key of keysToDelete) {
await kv.delete([key]);
console.log(`🧹 已清理KV数据: ${key}`);
}
} catch (error) {
console.error(`❌ 清理KV数据失败: ${error}`);
}
break;
}
case "deviceListUpdated": {
const { total, added, updated, removed, devices } = message.data;
console.log(`📊 设备列表更新统计: 总数=${total}, 新增=${added}, 更新=${updated}, 移除=${removed}`);
break;
}
case "shutdown":
masterWorker.postMessage({ type: "shutdown" });
break;
default:
console.warn("收到未知类型的消息:", message);
}
} catch (err) {
console.error("处理消息时发生异常:", err);
}
};
masterWorker.onerror = (error: any) => {
console.error("Master Controller Worker 执行错误:", error);
};
return masterWorker;
}
// 定义读取并发送设备列表更新消息的函数
// 直接移除单个设备(不依赖文件更新)
export async function removeDeviceFromWorker(masterWorker: Worker, manufacturer: string, serialNumber: string) {
console.log(`🗑️ 正在从VDA Worker中移除设备: ${manufacturer}/${serialNumber}`);
masterWorker.postMessage({
type: "removeDevice",
data: { manufacturer, serialNumber }
});
}
export async function updateDeviceListFromConfig(masterWorker: Worker, config: any, instanceId: string) {
try {
console.log("🔄 开始读取设备配置文件...");
const text = await Deno.readTextFile("./devices.json");
const devices = JSON.parse(text);
if (Array.isArray(devices)) {
console.log(`📱 读取到 ${devices.length} 个设备配置:`);
devices.forEach((device, index) => {
console.log(` ${index + 1}. ${device.manufacturer}/${device.serialNumber}`);
});
// 发送初始化消息(如果需要)
masterWorker.postMessage({
type: "init",
data: {
brokerUrl: config.mqtt.brokerUrl,
interfaceName: config.interfaceName,
manufacturer: config.manufacturer,
instanceId: instanceId
},
});
// 将最新设备列表发送给 masterWorker
masterWorker.postMessage({ type: "updateDeviceList", data: devices });
console.log("✅ 设备列表已成功发送到 VDA Worker");
} else {
console.error("❌ 配置文件格式错误,要求为数组格式");
}
} catch (error) {
console.error("❌ 读取设备配置文件失败:", error);
if (error instanceof Deno.errors.NotFound) {
console.log("💡 提示devices.json 文件不存在,将使用空设备列表");
// 发送空设备列表
masterWorker.postMessage({ type: "updateDeviceList", data: [] });
}
}
}
// Flag to prevent concurrent full-worker reconnects
let isReconnectingAll = false;
export function reconnectAllWorker(
kv: Deno.Kv,
webWorker: Worker,
masterWorker: Worker,
agvWorker: Worker,
config: any,
mappings: any,
instanceId: string
): Worker {
if (isReconnectingAll) {
console.log("reconnectAllWorker: 已在重连中,忽略重复调用");
return masterWorker;
}
isReconnectingAll = true;
console.log("vda 收到 master Worker reconnect 消息,准备重连");
agvWorker.postMessage({
type: "shutdown"
});
masterWorker.postMessage({
type: "shutdown"
});
masterWorker.terminate();
agvWorker.terminate();
setTimeout( () => {
masterWorker.terminate();
agvWorker.terminate();
setTimeout( () => {
const agvWorker = new Worker(
new URL("./agv_worker.ts", import.meta.url).href,
{ type: "module" }
);
const masterWorker = new Worker(
new URL("./vda_worker.ts", import.meta.url).href,
{ type: "module" }
);
setupAgvWorker(kv, config, masterWorker, agvWorker, webWorker, mappings, instanceId);
initAgvWorker(agvWorker, config);
setupMasterWorker(kv, webWorker, masterWorker, agvWorker, config, mappings, instanceId);
updateDeviceListFromConfig(masterWorker, config, instanceId);
// Reset flag after reconnect completes
isReconnectingAll = false;
}, 2000);
}, 2000);
return masterWorker;
}

240
modbus_manager.ts Normal file
View File

@ -0,0 +1,240 @@
// modbus_manager.ts - Modbus TCP 管理器,支持写操作高优先级
import Modbus from "npm:jsmodbus@4.0.10";
import net from "node:net";
type ReadJob = {
fn: "readHoldingRegisters" | "readInputRegisters" | "readCoils" | "readDiscreteInputs";
start: number;
len: number;
resolve: (vals: number[]) => void;
reject: (err: Error) => void;
};
type WriteJob = {
fn: "writeSingleRegister" | "writeMultipleRegisters";
start: number;
payload: number | number[];
resolve: () => void;
reject: (err: Error) => void;
};
export class ModbusManager {
private socket!: net.Socket;
private client!: any;
// 双队列实现写操作高优先级
private writeQueue: WriteJob[] = [];
private readQueue: ReadJob[] = [];
private reconnectTimer?: number;
private isConnected = false;
private isRunning = false;
constructor(
private host: string,
private port: number,
private unitId: number,
private logger: (msg: string) => void
) {}
async init() {
this.logger("Initializing ModbusManager...");
await this.connect();
this.runLoop();
this.logger("ModbusManager initialized successfully");
}
private connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.socket = new net.Socket();
this.client = new Modbus.client.TCP(this.socket, this.unitId);
// 设置socket选项
this.socket.setKeepAlive(true, 60000);
this.socket.setTimeout(5000);
const cleanup = () => {
this.socket.off("error", onError);
this.socket.off("close", onClose);
this.socket.off("timeout", onTimeout);
};
const onError = (error: Error) => {
cleanup();
this.isConnected = false;
this.logger(`Modbus socket error: ${error.message}`);
reject(error);
this.scheduleReconnect();
};
const onClose = () => {
cleanup();
this.isConnected = false;
this.logger("Modbus socket closed");
this.scheduleReconnect();
};
const onTimeout = () => {
cleanup();
this.isConnected = false;
this.logger("Modbus socket timeout");
this.socket.destroy();
this.scheduleReconnect();
};
this.socket.once("connect", () => {
cleanup();
this.isConnected = true;
this.logger(`Modbus connected to ${this.host}:${this.port}, unitId: ${this.unitId}`);
resolve();
});
this.socket.once("error", onError);
this.socket.once("close", onClose);
this.socket.once("timeout", onTimeout);
// 连接到Modbus设备
this.socket.connect({ host: this.host, port: this.port });
} catch (error) {
this.logger(`Failed to create Modbus connection: ${(error as Error).message}`);
reject(error);
}
});
}
private scheduleReconnect() {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = undefined;
this.logger("Attempting Modbus reconnect...");
try {
await this.connect();
this.logger("Modbus reconnected successfully");
} catch (error) {
this.logger(`Modbus reconnect failed: ${(error as Error).message}`);
// 会继续重试
}
}, 3000);
}
enqueueRead(
fn: ReadJob["fn"],
start: number,
len: number
): Promise<number[]> {
return new Promise((resolve, reject) => {
this.readQueue.push({ fn, start, len, resolve, reject });
});
}
enqueueWrite(
fn: WriteJob["fn"],
start: number,
payload: number | number[]
): Promise<void> {
return new Promise((resolve, reject) => {
this.writeQueue.push({ fn, start, payload, resolve, reject });
});
}
private async runLoop() {
if (this.isRunning) return;
this.isRunning = true;
this.logger("Starting Modbus job processing loop");
while (this.isRunning) {
let job: ReadJob | WriteJob | undefined;
// 写操作高优先级:永远先检查写队列
if (this.writeQueue.length > 0) {
job = this.writeQueue.shift();
} else if (this.readQueue.length > 0) {
job = this.readQueue.shift();
}
if (!job) {
// 没有任务时短暂休眠
await new Promise((resolve) => setTimeout(resolve, 10));
continue;
}
// 检查连接状态
if (!this.isConnected) {
job.reject(new Error("Modbus not connected"));
continue;
}
try {
if ("len" in job) {
// 读操作
const response = await this.client[job.fn](job.start, job.len);
const values = Array.from(response.response.body.values as number[]);
job.resolve(values);
this.logger(`Read ${job.fn} at ${job.start}, length ${job.len}: [${values.join(', ')}]`);
} else {
// 写操作
await this.client[job.fn](job.start, job.payload);
job.resolve();
this.logger(`Write ${job.fn} at ${job.start}, payload: ${Array.isArray(job.payload) ? `[${job.payload.join(', ')}]` : job.payload}`);
}
} catch (error) {
const errorMsg = (error as Error).message;
this.logger(`Modbus operation failed: ${errorMsg}`);
job.reject(error as Error);
// 如果是连接相关错误,标记连接断开
if (errorMsg.includes("ECONNRESET") || errorMsg.includes("ENOTCONN") || errorMsg.includes("ETIMEDOUT")) {
this.isConnected = false;
this.scheduleReconnect();
}
}
// 操作间隔,避免过于频繁的请求
await new Promise((resolve) => setTimeout(resolve, 5));
}
this.logger("Modbus job processing loop stopped");
}
getQueueStatus() {
return {
writeQueue: this.writeQueue.length,
readQueue: this.readQueue.length,
isConnected: this.isConnected
};
}
async close() {
this.logger("Closing ModbusManager...");
this.isRunning = false;
// 清空队列,拒绝所有等待的任务
[...this.writeQueue, ...this.readQueue].forEach(job => {
job.reject(new Error("ModbusManager is closing"));
});
this.writeQueue = [];
this.readQueue = [];
// 清理重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
}
// 关闭socket连接
if (this.socket) {
try {
this.socket.destroy();
this.logger("Modbus socket closed");
} catch (error) {
this.logger(`Error closing Modbus socket: ${(error as Error).message}`);
}
}
this.isConnected = false;
this.logger("ModbusManager closed");
}
}

519
proto/vda5050.proto Normal file
View File

@ -0,0 +1,519 @@
syntax = "proto3";
package vda5050;
//
service AgvManagement {
//
rpc CommunicationChannel(stream AgvMessage) returns (stream ControlMessage) {}
}
//
message NodePosition {
float x = 1;
float y = 2;
optional float theta = 3;
optional float allowedDeviationXy = 4;
optional float allowedDeviationTheta = 5;
string mapId = 6;
optional string mapDescription = 7;
}
//
message Node {
string nodeId = 1;
uint32 sequenceId = 2;
optional string nodeDescription = 3;
bool released = 4;
optional NodePosition nodePosition = 5;
repeated Action actions = 6;
}
//
message Edge {
string edgeId = 1;
uint32 sequenceId = 2;
optional string edgeDescription = 3;
bool released = 4;
string startNodeId = 5;
string endNodeId = 6;
optional float maxSpeed = 7;
optional float maxHeight = 8;
optional float minHeight = 9;
optional float orientation = 10;
optional string direction = 11;
optional bool rotationAllowed = 12;
optional float maxRotationSpeed = 13;
optional Trajectory trajectory = 14;
optional float length = 15;
}
//
message Action {
string actionType = 1;
string actionId = 2;
optional string actionDescription = 3;
string blockingType = 4;
repeated ActionParameter actionParameters = 5;
}
//
message ActionParameter {
string key = 1;
string value = 2;
}
//
message InstantActions {
uint32 headerId = 1;
string timestamp = 2;
string version = 3;
string manufacturer = 4;
string serialNumber = 5;
repeated Action instantActions = 6;
}
//
message Order {
uint32 headerId = 1;
string timestamp = 2;
string version = 3;
string manufacturer = 4;
string serialNumber = 5;
string orderId = 6;
uint32 orderUpdateId = 7;
optional string zoneSetId = 8;
repeated Node nodes = 9;
repeated Edge edges = 10;
}
//
message Connection {
uint32 headerId = 1;
string timestamp = 2;
string version = 3;
string manufacturer = 4;
string serialNumber = 5;
string connectionState = 6;
}
//
message State {
uint32 headerId = 1;
string timestamp = 2;
string version = 3;
string manufacturer = 4;
string serialNumber = 5;
string orderId = 6;
uint32 orderUpdateId = 7;
string lastNodeId = 8;
uint32 lastNodeSequenceId = 9;
bool driving = 10;
optional bool waitingForInteractionZoneRelease = 11;
optional bool paused = 12;
optional ForkState forkState = 13;
optional bool newBaseRequest = 14;
optional float distanceSinceLastNode = 15;
string operatingMode = 16;
repeated NodeState nodeStates = 17;
repeated EdgeState edgeStates = 18;
optional AgvPosition agvPosition = 19;
optional Velocity velocity = 20;
repeated Load loads = 21;
repeated ActionState actionStates = 22;
BatteryState batteryState = 23;
repeated Error errors = 24;
repeated Information information = 25;
SafetyState safetyState = 26;
}
// AGV位置
message AgvPosition {
float x = 1;
float y = 2;
float theta = 3;
string mapId = 4;
optional string mapDescription = 5;
bool positionInitialized = 6;
optional float deviationRange = 7;
optional float localizationScore = 8;
}
//
message Velocity {
optional float vx = 1;
optional float vy = 2;
optional float omega = 3;
}
//
message Error {
string errorType = 1;
repeated ErrorReference errorReferences = 2;
optional string errorDescription = 3;
string errorLevel = 4;
}
//
message ErrorReference {
string referenceKey = 1;
string referenceValue = 2;
}
//
message Information {
string infoType = 1;
repeated InfoReference infoReferences = 2;
optional string infoDescription = 3;
string infoLevel = 4;
}
//
message InfoReference {
string referenceKey = 1;
string referenceValue = 2;
}
//
message ActionState {
string actionId = 1;
optional string actionType = 2;
optional string actionDescription = 3;
string actionStatus = 4;
optional string resultDescription = 5;
}
//
message NodeState {
string nodeId = 1;
uint32 sequenceId = 2;
optional string nodeDescription = 3;
optional NodePosition nodePosition = 4;
bool released = 5;
}
//
message EdgeState {
string edgeId = 1;
uint32 sequenceId = 2;
optional string edgeDescription = 3;
bool released = 4;
optional Trajectory trajectory = 5;
}
//
message Load {
optional string loadId = 1;
optional string loadType = 2;
optional string loadPosition = 3;
optional BoundingBoxReference boundingBoxReference = 4;
optional LoadDimensions loadDimensions = 5;
optional float weight = 6;
}
//
message BoundingBoxReference {
float x = 1;
float y = 2;
float z = 3;
optional float theta = 4;
}
//
message LoadDimensions {
float length = 1;
float width = 2;
optional float height = 3;
}
//
message ControlPoint {
float x = 1;
float y = 2;
optional float weight = 3;
optional float orientation = 4;
}
//
message Trajectory {
int64 degree = 1;
repeated float knotVector = 2;
repeated ControlPoint controlPoints = 3;
}
//
message ForkState {
float forkHeight = 1;
}
//
message SafetyState {
string eStop = 1;
bool fieldViolation = 2;
}
//
message BatteryState {
float batteryCharge = 1;
optional float batteryVoltage = 2;
optional uint32 batteryHealth = 3;
bool charging = 4;
optional float reach = 5;
}
//
message Visualization {
uint32 headerId = 1;
string timestamp = 2;
string version = 3;
string manufacturer = 4;
string serialNumber = 5;
optional AgvPosition agvPosition = 6;
optional Velocity velocity = 7;
}
// AGV
message ControlMessage {
string targetAgvId = 1;
oneof message_type {
Order order = 2;
InstantActions instantActions = 3;
}
}
// AGV消息AGV到控制系统
message AgvMessage {
string agvId = 1;
oneof message_type {
State state = 2;
Connection connection = 3;
Error error = 4;
Visualization visualization = 5;
Factsheet factsheet = 6;
}
}
// AGV
service AgvDataService {
// AGV
rpc GetAgvData (GetAgvDataRequest) returns (AgvDataResponse) {}
// AGV
rpc GetAllAgvData (GetAllAgvDataRequest) returns (GetAllAgvDataResponse) {}
}
//
message GetAgvDataRequest {
string agvId = 1;
}
message GetAllAgvDataRequest {
//
bool includeRealAgvs = 1;
bool includeSimulatedAgvs = 2;
}
//
message AgvDataResponse {
string agvId = 1;
State state = 2;
uint32 timestamp = 3;
bool isReal = 4;
}
message GetAllAgvDataResponse {
repeated AgvDataResponse agvs = 1;
}
// Factsheet provides AGV factsheet information to master control
message Factsheet {
uint32 headerId = 1;
string timestamp = 2;
string version = 3;
string manufacturer = 4;
string serialNumber = 5;
TypeSpecification typeSpecification = 6;
PhysicalParameters physicalParameters = 7;
ProtocolLimits protocolLimits = 8;
ProtocolFeatures protocolFeatures = 9;
AgvGeometry agvGeometry = 10;
LoadSpecification loadSpecification = 11;
VehicleConfig vehicleConfig = 12;
}
message TypeSpecification {
string seriesName = 1;
optional string seriesDescription = 2;
string agvKinematic = 3;
string agvClass = 4;
double maxLoadMass = 5;
repeated string localizationTypes = 6;
repeated string navigationTypes = 7;
}
message PhysicalParameters {
double speedMin = 1;
double speedMax = 2;
optional double angularSpeedMin = 3;
optional double angularSpeedMax = 4;
double accelerationMax = 5;
double decelerationMax = 6;
double heightMin = 7;
double heightMax = 8;
double width = 9;
double length = 10;
}
message ProtocolLimits {
MaxStringLens maxStringLens = 1;
MaxArrayLens maxArrayLens = 2;
Timing timing = 3;
}
message MaxStringLens {
optional uint32 msgLen = 1;
optional uint32 topicSerialLen = 2;
optional uint32 topicElemLen = 3;
optional uint32 idLen = 4;
optional bool idNumericalOnly = 5;
optional uint32 enumLen = 6;
optional uint32 loadIdLen = 7;
}
message MaxArrayLens {
optional uint32 orderNodes = 1;
optional uint32 orderEdges = 2;
optional uint32 nodeActions = 3;
optional uint32 edgeActions = 4;
optional uint32 actionParameters = 5;
optional uint32 instantActions = 6;
optional uint32 trajectoryKnotVector = 7;
optional uint32 trajectoryControlPoints = 8;
optional uint32 stateNodeStates = 9;
optional uint32 stateEdgeStates = 10;
optional uint32 stateLoads = 11;
optional uint32 stateActionStates = 12;
optional uint32 stateErrors = 13;
optional uint32 stateInformation = 14;
optional uint32 errorErrorReferences = 15;
optional uint32 informationInfoReferences = 16;
}
message Timing {
optional float minOrderInterval = 1;
optional float minStateInterval = 2;
optional float defaultStateInterval = 3;
optional float visualizationInterval = 4;
}
message ProtocolFeatures {
repeated OptionalParameter optionalParameters = 1;
repeated AgvAction agvActions = 2;
}
message OptionalParameter {
string parameter = 1;
string support = 2;
optional string description = 3;
}
message AgvAction {
string actionType = 1;
optional string actionDescription = 2;
repeated string actionScopes = 3;
repeated ActionParameterDefinition actionParameters = 4;
optional string resultDescription = 5;
repeated string blockingTypes = 6;
}
message ActionParameterDefinition {
string key = 1;
string valueDataType = 2;
optional string description = 3;
optional bool isOptional = 4;
}
message AgvGeometry {
repeated WheelDefinition wheelDefinitions = 1;
repeated Envelope2d envelopes2d = 2;
repeated Envelope3d envelopes3d = 3;
}
message WheelDefinition {
string type = 1;
bool isActiveDriven = 2;
bool isActiveSteered = 3;
Position position = 4;
double diameter = 5;
double width = 6;
optional double centerDisplacement = 7;
optional string constraints = 8;
}
message Position {
double x = 1;
double y = 2;
optional double theta = 3;
}
message Envelope2d {
string set = 1;
repeated PolygonPoint polygonPoints = 2;
optional string description = 3;
}
message PolygonPoint {
double x = 1;
double y = 2;
}
message Envelope3d {
string set = 1;
string format = 2;
string data = 3;
optional string url = 4;
optional string description = 5;
}
message LoadSpecification {
repeated string loadPositions = 1;
repeated LoadSet loadSets = 2;
}
message LoadSet {
string setName = 1;
string loadType = 2;
repeated string loadPositions = 3;
BoundingBoxReference boundingBoxReference = 4;
LoadDimensions loadDimensions = 5;
optional double maxWeight = 6;
optional double minLoadhandlingHeight = 7;
optional double maxLoadhandlingHeight = 8;
optional double minLoadhandlingDepth = 9;
optional double maxLoadhandlingDepth = 10;
optional double minLoadhandlingTilt = 11;
optional double maxLoadhandlingTilt = 12;
optional double agvSpeedLimit = 13;
optional double agvAccelerationLimit = 14;
optional double agvDecelerationLimit = 15;
optional double pickTime = 16;
optional double dropTime = 17;
optional string description = 18;
}
message VehicleConfig {
repeated VersionInfo versions = 1;
optional NetworkInfo network = 2;
}
message VersionInfo {
string key = 1;
string value = 2;
}
message NetworkInfo {
repeated string dnsServers = 1;
repeated string ntpServers = 2;
string localIpAddress = 3;
string netmask = 4;
string defaultGateway = 5;
}

796
simulator.ts Normal file
View File

@ -0,0 +1,796 @@
// simulator.ts - Worker版本
import mqtt, { IClientOptions, MqttClient } from "npm:mqtt";
import { v4 as uuidv4 } from "npm:uuid";
import { loadConfig, RawConfig } from "./simulator_config.ts";
interface SimulatorConfig {
vdaInterface: string;
zoneSetId: string;
mapId: string;
vehicle: {
manufacturer: string;
serialNumber: string;
vdaVersion: string;
};
mqtt: {
brokerUrl: string;
options: IClientOptions;
};
settings: {
robotCount: number;
stateFrequency: number;
visualizationFrequency: number;
speed: number;
};
}
interface Header {
headerId: number;
timestamp: string;
version: string;
manufacturer: string;
serialNumber: string;
}
interface BatteryState {
batteryCharge: number;
charging: boolean;
batteryHealth?: number;
batteryVoltage?: number;
reach?: number;
}
interface SafetyState {
eStop: string;
fieldViolation: boolean;
}
interface AGVPosition {
x: number;
y: number;
theta: number;
mapId: string;
positionInitialized: boolean;
mapDescription?: string;
localizationScore?: number;
deviationRange?: number;
}
interface Velocity {
vx: number;
vy: number;
omega: number;
}
type ActionStatus = "WAITING" | "INITIALIZING" | "RUNNING" | "PAUSED" | "FINISHED" | "FAILED";
interface ActionState {
actionId: string;
actionType?: string;
actionStatus: ActionStatus;
actionDescription?: string;
resultDescription?: string;
actionParameters?: Array<{ key: string; value: string }>;
blockingType?: "NONE" | "SOFT" | "HARD";
}
interface NodeState {
nodeId: string;
sequenceId: string;
released: boolean;
nodeDescription?: string;
nodePosition: AGVPosition;
}
interface EdgeState {
edgeId: string;
sequenceId: string;
released: boolean;
edgeDescription?: string;
trajectory?: Array<Record<string,unknown>>;
}
interface Factsheet {
headerId: number;
timestamp: string;
version: string;
manufacturer: string;
serialNumber: string;
typeSpecification: {
seriesName: string;
seriesDescription: string;
agvKinematic: string;
agvClass: string;
maxLoadMass: number;
localizationTypes: string[];
navigationTypes: string[];
};
physicalParameters: {
speedMin: number;
speedMax: number;
accelerationMax: number;
decelerationMax: number;
heightMin: number;
heightMax: number;
width: number;
length: number;
};
protocolLimits: any;
protocolFeatures: any;
agvGeometry: any;
loadSpecification: any;
localizationParameters: any;
}
interface State {
headerId: number;
timestamp: string;
version: string;
manufacturer: string;
serialNumber: string;
orderId: string;
orderUpdateId: number;
zoneSetId: string;
lastNodeId: string;
lastNodeSequenceId: number;
nodeStates: NodeState[];
edgeStates: EdgeState[];
actionStates: ActionState[];
batteryState: BatteryState;
operatingMode: string;
errors: { errorType: string; errorDescription?: string; errorLevel: string }[];
safetyState: SafetyState;
driving: boolean;
paused: boolean;
newBaseRequest: boolean;
waitingForInteractionZoneRelease: boolean;
agvPosition: AGVPosition;
velocity: Velocity;
loads: any[];
information?: any[];
forkState?: { forkHeight?: number };
}
interface Visualization {
header: Header;
agvPosition: AGVPosition;
velocity: Velocity;
driving: boolean;
}
interface Connection {
headerId: number;
timestamp: string;
version: string;
manufacturer: string;
serialNumber: string;
connectionState: "ONLINE" | "OFFLINE" | "CONNECTIONBROKEN";
}
interface ActionParamValue {
Float?: number;
Str?: string;
}
interface Action {
actionId: string;
actionType?: string;
actionParameters: { key: string; value: ActionParamValue }[];
}
interface InstantActions {
instantActions: Action[];
}
interface Order {
orderId: string;
orderUpdateId: number;
nodes: any[];
edges: any[];
}
// Worker消息类型
interface WorkerMessage {
type: "init" | "close" | "reconnect";
data?: any;
}
interface MainMessage {
type: "ready" | "error" | "status" | "log" | "device_request";
data?: any;
}
class VehicleSimulator {
connectionTopic: string = "";
stateTopic: string = "";
visualizationTopic: string = "";
factsheetTopic: string = "";
connection!: Connection;
state!: State;
visualization!: Visualization;
factsheet!: Factsheet;
private lastUpdate = Date.now();
private speed: number = 0;
private boundary = 40.0;
private mqttClient?: MqttClient;
private intervals: number[] = [];
private isRunning = false;
constructor(private cfg: SimulatorConfig) {
this.speed = cfg.settings.speed;
this.initializeSimulator();
}
private initializeSimulator() {
const { manufacturer, serialNumber, vdaVersion } = this.cfg.vehicle;
const base = `${this.cfg.vdaInterface}/${vdaVersion}/${manufacturer}/${serialNumber}`;
this.connectionTopic = `${base}/connection`;
this.stateTopic = `${base}/state`;
this.visualizationTopic = `${base}/visualization`;
this.factsheetTopic = `${base}/factsheet`;
const now = () => new Date().toISOString();
const header0: Header = {
headerId: 0,
timestamp: now(),
version: vdaVersion,
manufacturer,
serialNumber
};
this.connection = {
headerId: 0,
timestamp: now(),
version: vdaVersion,
manufacturer,
serialNumber,
connectionState: "CONNECTIONBROKEN"
};
// 随机初始位置
const x0 = (Math.random() * 2 - 1) * 30;
const y0 = (Math.random() * 2 - 1) * 30;
const th0 = (Math.random() * 2 - 1) * Math.PI;
// VDA 5050 兼容的扁平化 State
this.state = {
headerId: 0,
timestamp: now(),
version: vdaVersion,
manufacturer,
serialNumber,
orderId: "",
orderUpdateId: 0,
zoneSetId: this.cfg.zoneSetId,
lastNodeId: "",
lastNodeSequenceId: 0,
nodeStates: [],
edgeStates: [],
actionStates: [],
batteryState: { batteryCharge: 1.0, charging: false },
operatingMode: "AUTOMATIC",
errors: [],
safetyState: { eStop: "NONE", fieldViolation: false },
driving: true,
paused: false,
newBaseRequest: true,
waitingForInteractionZoneRelease: false,
agvPosition: {
x: x0,
y: y0,
theta: th0,
mapId: this.cfg.mapId,
positionInitialized: true
},
velocity: {
vx: this.speed * Math.cos(th0),
vy: this.speed * Math.sin(th0),
omega: 0
},
loads: [],
information: [],
forkState: { forkHeight: 0 }
};
this.visualization = {
header: { ...header0 },
agvPosition: { ...this.state.agvPosition },
velocity: { ...this.state.velocity },
driving: true
};
this.factsheet = {
headerId: 0,
timestamp: now(),
version: "v3.4.7.1005",
manufacturer,
serialNumber,
typeSpecification: {
seriesName: serialNumber,
seriesDescription: serialNumber,
agvKinematic: "DIFF",
agvClass: "FORKLIFT",
maxLoadMass: 0.5,
localizationTypes: ["NATURAL"],
navigationTypes: ["PHYSICAL_LINE_GUIDED"]
},
physicalParameters: {
speedMin: 0.01,
speedMax: 0.8,
accelerationMax: 0.5,
decelerationMax: 0.15,
heightMin: 1.83,
heightMax: 1.83,
width: 0.885,
length: 1.5145
},
protocolLimits: null,
protocolFeatures: null,
agvGeometry: null,
loadSpecification: null,
localizationParameters: null
};
}
async start() {
if (this.isRunning) {
this.log("warn", "Simulator already running");
return;
}
try {
this.isRunning = true;
await this.connectMqtt();
await this.subscribeVda();
await this.publishConnection();
this.publishFactsheet(); // 启动时发布一次 factsheet
this.startIntervals();
this.postMessage({ type: "ready", data: { agvId: this.cfg.vehicle.serialNumber } });
this.log("info", `🚛 AGV ${this.cfg.vehicle.serialNumber} started successfully`);
} catch (error) {
this.isRunning = false;
this.postMessage({ type: "error", data: { error: (error as Error).message, agvId: this.cfg.vehicle.serialNumber } });
this.log("error", `Failed to start AGV ${this.cfg.vehicle.serialNumber}: ${(error as Error).message}`);
}
}
async stop() {
if (!this.isRunning) return;
this.isRunning = false;
this.clearIntervals();
if (this.mqttClient) {
this.connection.connectionState = "OFFLINE";
this.publishConnectionOnline();
this.mqttClient.end();
this.mqttClient = undefined;
}
this.log("info", `🛑 AGV ${this.cfg.vehicle.serialNumber} stopped`);
}
async reconnect() {
this.log("info", `🔄 Reconnecting AGV ${this.cfg.vehicle.serialNumber}`);
await this.stop();
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
await this.start();
}
private async connectMqtt() {
const clientOptions: IClientOptions = {
clientId: `deno-agv-sim-${this.cfg.vehicle.serialNumber}-${uuidv4()}`,
clean: true,
keepalive: 60,
};
this.mqttClient = mqtt.connect(this.cfg.mqtt.brokerUrl, clientOptions);
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("MQTT connection timeout"));
}, 10000);
this.mqttClient!.on("connect", () => {
clearTimeout(timeout);
this.log("info", `✅ MQTT connected: ${this.cfg.vehicle.serialNumber}`);
resolve();
});
this.mqttClient!.on("error", (error) => {
clearTimeout(timeout);
reject(error as Error);
});
});
}
private async subscribeVda() {
if (!this.mqttClient) throw new Error("MQTT client not connected");
const b = `${this.cfg.vdaInterface}/${this.cfg.vehicle.vdaVersion}/${this.cfg.vehicle.manufacturer}/${this.cfg.vehicle.serialNumber}`;
const topics = [`${b}/instantActions`, `${b}/order`];
await new Promise<void>((res, rej) =>
this.mqttClient!.subscribe(topics, { qos: 1 }, err => err ? rej(err) : res())
);
this.mqttClient.on("message", (topic, payload) => {
try {
const msg = JSON.parse(payload.toString());
this.log("info", `📨 Received MQTT message on topic: ${topic}`);
if (topic.endsWith("instantActions")) {
this.instantActionsAccept(msg);
} else if (topic.endsWith("order")) {
this.orderAccept(msg);
}
} catch (error) {
this.log("error", `Error processing MQTT message: ${(error as Error).message}`);
this.log("error", `Topic: ${topic}, Payload: ${payload.toString()}`);
}
});
}
private startIntervals() {
const stateInterval = setInterval(() => {
this.stateIterate();
this.publishState();
}, 2000);
const visInterval = setInterval(() => {
this.publishVisualization();
}, Math.floor(1000 / this.cfg.settings.visualizationFrequency));
const connectionInterval = setInterval(() => {
this.publishConnectionOnline();
}, 1000);
// const factsheetInterval = setInterval(() => {
// this.publishFactsheet();
// }, 30000); // 每30秒发布一次 factsheet
this.intervals = [stateInterval, visInterval, connectionInterval];
}
private clearIntervals() {
this.intervals.forEach(interval => clearInterval(interval));
this.intervals = [];
}
stateIterate() {
const nowMs = Date.now();
const dt = (nowMs - this.lastUpdate) / 1000;
this.lastUpdate = nowMs;
this.state.headerId++;
this.state.timestamp = new Date().toISOString();
if (this.state.driving) {
let { x, y, theta } = this.state.agvPosition;
const vx = this.speed * Math.cos(theta);
const vy = this.speed * Math.sin(theta);
let nx = x + vx * dt;
let ny = y + vy * dt;
let bounced = false;
if (nx > this.boundary) {
nx = this.boundary - (nx - this.boundary);
bounced = true;
}
if (nx < -this.boundary) {
nx = -this.boundary - (-this.boundary - nx);
bounced = true;
}
if (ny > this.boundary) {
ny = this.boundary - (ny - this.boundary);
bounced = true;
}
if (ny < -this.boundary) {
ny = -this.boundary - (-this.boundary - ny);
bounced = true;
}
if (bounced) {
theta += Math.PI;
if (theta > Math.PI) theta -= 2 * Math.PI;
if (theta < -Math.PI) theta += 2 * Math.PI;
}
this.state.agvPosition = {
x: nx,
y: ny,
theta,
mapId: this.state.agvPosition.mapId,
positionInitialized: true
};
this.state.velocity = {
vx: this.speed * Math.cos(theta),
vy: this.speed * Math.sin(theta),
omega: 0
};
} else {
this.state.velocity = { vx: 0, vy: 0, omega: 0 };
}
// 同步 Visualization header
this.visualization.header.headerId = this.state.headerId + 1;
this.visualization.header.timestamp = this.state.timestamp;
this.visualization.agvPosition = { ...this.state.agvPosition };
this.visualization.velocity = { ...this.state.velocity };
this.visualization.driving = this.state.driving;
}
async publishConnection() {
if (!this.mqttClient) return;
const pub = (msg: Connection) => new Promise<void>(res => {
this.mqttClient!.publish(this.connectionTopic, JSON.stringify(msg), { qos: 1 }, () => res());
});
// Broken
this.connection.headerId++;
this.connection.timestamp = new Date().toISOString();
this.connection.connectionState = "CONNECTIONBROKEN";
await pub(this.connection);
// Online
await new Promise(r => setTimeout(r, 500));
this.connection.headerId++;
this.connection.timestamp = new Date().toISOString();
this.connection.connectionState = "ONLINE";
await pub(this.connection);
}
publishConnectionOnline() {
if (!this.mqttClient) return;
this.connection.headerId++;
this.connection.timestamp = new Date().toISOString();
this.connection.connectionState = "ONLINE";
this.mqttClient.publish(this.connectionTopic, JSON.stringify(this.connection), { qos: 1 });
}
publishState() {
if (!this.mqttClient) return;
this.mqttClient.publish(this.stateTopic, JSON.stringify(this.state), { qos: 1 });
}
publishVisualization() {
if (!this.mqttClient) return;
this.mqttClient.publish(this.visualizationTopic, JSON.stringify(this.visualization), { qos: 1 });
}
publishFactsheet() {
if (!this.mqttClient) return;
this.factsheet.headerId++;
this.factsheet.timestamp = new Date().toISOString();
this.mqttClient.publish(this.factsheetTopic, JSON.stringify(this.factsheet), { qos: 1 });
}
async instantActionsAccept(req: any) {
this.log("info", `📥 Received instantActions: ${JSON.stringify(req, null, 2)}`);
// 验证消息格式
if (!req || typeof req !== 'object') {
this.log("error", "Invalid instantActions request: not an object");
return;
}
// 检查actions或instantActions字段支持两种格式
let actionsArray: any[];
if (req.instantActions && Array.isArray(req.instantActions)) {
actionsArray = req.instantActions;
} else if (req.actions && Array.isArray(req.actions)) {
actionsArray = req.actions;
this.log("info", "Using 'actions' field instead of 'instantActions'");
} else {
this.log("error", "Missing instantActions or actions field in request");
return;
}
this.log("info", `📋 Processing ${actionsArray.length} instant actions`);
for (const act of actionsArray) {
if (!act || !act.actionId) {
this.log("error", "Invalid action: missing actionId");
continue;
}
// 检查是否是设备相关的action
if (act.actionType === "deviceSetup") {
this.log("info", `🔧 Processing deviceSetup action: ${act.actionId}`);
// 提取设备信息
const deviceInfo = this.extractDeviceInfo(act.actionParameters || []);
if (deviceInfo) {
// 通知主进程创建设备模拟器
this.postMessage({
type: "device_request",
data: {
action: "create",
deviceInfo,
originalAction: act
}
});
this.log("info", `📡 Requested device simulator creation for ${deviceInfo.ip}-${deviceInfo.port}-${deviceInfo.slaveId}`);
} else {
this.log("error", `❌ Failed to extract device info from action ${act.actionId}`);
}
} else if (act.actionType === "deviceStop") {
this.log("info", `🛑 Processing deviceStop action: ${act.actionId}`);
// 提取设备ID或信息
const deviceInfo = this.extractDeviceInfo(act.actionParameters || []);
if (deviceInfo) {
const deviceId = `${deviceInfo.ip}-${deviceInfo.port}-${deviceInfo.slaveId}`;
// 通知主进程停止设备模拟器
this.postMessage({
type: "device_request",
data: {
action: "stop",
deviceId,
originalAction: act
}
});
this.log("info", `📡 Requested device simulator stop for ${deviceId}`);
}
} else if (act.actionType === "deviceDelete") {
this.log("info", `🗑️ Processing deviceDelete action: ${act.actionId}`);
// 提取设备ID或信息
const deviceInfo = this.extractDeviceInfo(act.actionParameters || []);
if (deviceInfo) {
const deviceId = `${deviceInfo.ip}-${deviceInfo.port}-${deviceInfo.slaveId}`;
// 通知主进程删除设备模拟器
this.postMessage({
type: "device_request",
data: {
action: "delete",
deviceId,
originalAction: act
}
});
this.log("info", `📡 Requested device simulator deletion for ${deviceId}`);
}
} else if (act.actionType === "deviceWrite" || act.actionType === "deviceRead") {
this.log("info", `🔧 Processing device action: ${act.actionType} - ${act.actionId}`);
// 提取设备信息
const deviceInfo = this.extractDeviceInfo(act.actionParameters || []);
if (deviceInfo) {
const deviceId = `${deviceInfo.ip}-${deviceInfo.port}-${deviceInfo.slaveId}`;
// 通知主进程转发action到对应的设备模拟器
this.postMessage({
type: "device_request",
data: {
action: "forward",
deviceId,
originalAction: act
}
});
this.log("info", `📡 Forwarded device action to ${deviceId}`);
}
}
this.state.actionStates.push({
actionId: act.actionId,
actionType: act.actionType || "unknown",
actionStatus: "WAITING"
});
this.log("info", `✅ Added action: ${act.actionId} (${act.actionType || "unknown"})`);
}
}
// 从actionParameters中提取设备信息
private extractDeviceInfo(actionParameters: any[]): any | null {
const params: Record<string, string> = {};
for (const param of actionParameters) {
if (param.key && param.value) {
// 处理不同的value格式
let value: string;
if (typeof param.value === 'string') {
value = param.value;
} else if (param.value?.Str) {
value = param.value.Str;
} else if (param.value?.Float) {
value = param.value.Float.toString();
} else {
value = String(param.value);
}
params[param.key] = value;
}
}
// 检查必需的字段
const required = ['ip', 'port', 'slaveId'];
for (const field of required) {
if (!params[field]) {
this.log("error", `❌ Missing required parameter: ${field}`);
return null;
}
}
return {
ip: params.ip,
port: params.port,
slaveId: params.slaveId,
deviceName: params.deviceName || 'Unknown Device',
protocolType: params.protocolType || 'Unknown Protocol',
brandName: params.brandName || 'Unknown Brand',
registers: params.registers || '[]'
};
}
async orderAccept(req: Order) {
this.log("info", `📥 Received order: ${JSON.stringify(req, null, 2)}`);
this.state.orderId = req.orderId;
this.state.orderUpdateId = req.orderUpdateId;
this.state.nodeStates = [];
this.state.edgeStates = [];
this.state.actionStates = [];
this.log("info", `✅ Accepted order: ${req.orderId} (updateId: ${req.orderUpdateId})`);
}
private log(level: "info" | "warn" | "error", message: string) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${this.cfg.vehicle.serialNumber}] ${message}`;
console.log(logMessage);
this.postMessage({
type: "log",
data: { level, message: logMessage, agvId: this.cfg.vehicle.serialNumber }
});
}
private postMessage(message: MainMessage) {
self.postMessage(message);
}
}
// Worker全局变量
let simulator: VehicleSimulator | null = null;
// Worker消息处理
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
const { type, data } = event.data;
try {
switch (type) {
case "init":
if (simulator) {
await simulator.stop();
}
simulator = new VehicleSimulator(data);
await simulator.start();
break;
case "close":
if (simulator) {
await simulator.stop();
simulator = null;
}
self.postMessage({ type: "status", data: { status: "closed" } });
break;
case "reconnect":
if (simulator) {
await simulator.reconnect();
}
break;
default:
console.warn(`Unknown message type: ${type}`);
}
} catch (error) {
self.postMessage({
type: "error",
data: { error: (error as Error).message, context: type }
});
}
};

23
simulator_config.json Normal file
View File

@ -0,0 +1,23 @@
{
"mqtt_broker": {
"host": "10.2.0.6",
"port": "1883",
"vda_interface": "uagv"
},
"vehicle": {
"serial_number": "EXT-",
"channel_name": "r10256",
"channel_manufacturer": "vendor",
"manufacturer": "SEER",
"vda_version": "v2",
"vda_full_version": "2.0.0"
},
"settings": {
"map_id": "mapcc",
"state_frequency": 1,
"visualization_frequency": 1,
"action_time": 1.0,
"robot_count": 2,
"speed": 0.05
}
}

127
simulator_config.ts Normal file
View File

@ -0,0 +1,127 @@
// config.ts
export interface MqttBrokerConfig {
host: string;
port: number;
vdaInterface: string;
}
export interface VehicleConfig {
serialNumber: string;
channelName: string;
channelManufacturer: string;
manufacturer: string;
vdaVersion: string;
vdaFullVersion: string;
}
export interface Settings {
mapId: string;
stateFrequency: number;
visualizationFrequency: number;
actionTime: number;
robotCount: number;
speed: number;
}
export interface RawConfig {
mqttBroker: MqttBrokerConfig;
vehicle: VehicleConfig;
settings: Settings;
}
export async function loadConfig(): Promise<RawConfig> {
// 配置文件可能的路径列表
const configPaths = [
// 1. 当前工作目录
"./simulator_config.json",
// 2. 相对于模块的路径(开发环境)
new URL("./simulator_config.json", import.meta.url).pathname,
// 3. 可执行文件同目录
"./simulator_config.json",
// 4. 上级目录
"../simulator_config.json",
// 5. 用户主目录
`${Deno.env.get("HOME") || Deno.env.get("USERPROFILE")}/simulator_config.json`,
];
let configContent: string | null = null;
let usedPath: string = "";
// 尝试从多个路径加载配置文件
for (const configPath of configPaths) {
try {
console.log(`🔍 Trying to load config from: ${configPath}`);
configContent = await Deno.readTextFile(configPath);
usedPath = configPath;
console.log(`✅ Successfully loaded config from: ${configPath}`);
break;
} catch (error) {
console.log(`❌ Failed to load config from ${configPath}: ${(error as Error).message}`);
continue;
}
}
// 如果所有路径都失败,创建默认配置
if (!configContent) {
console.log("⚠️ No config file found, creating default configuration...");
const defaultConfig = {
mqtt_broker: {
host: "localhost",
port: "1883",
vda_interface: "uagv"
},
vehicle: {
serial_number: "AGV",
channel_name: "AGV_Channel",
channel_manufacturer: "Default",
manufacturer: "Default Manufacturer",
vda_version: "2.0.0",
vda_full_version: "2.0.0"
},
settings: {
map_id: "default_map",
state_frequency: 1000,
visualization_frequency: 1000,
action_time: 5000,
robot_count: 1,
speed: 1.0
}
};
// 尝试保存默认配置到当前目录
try {
const defaultConfigJson = JSON.stringify(defaultConfig, null, 2);
await Deno.writeTextFile("./simulator_config.json", defaultConfigJson);
console.log("📝 Created default config file: ./simulator_config.json");
configContent = defaultConfigJson;
usedPath = "./simulator_config.json";
} catch (error) {
console.log("⚠️ Could not save default config file, using in-memory configuration");
configContent = JSON.stringify(defaultConfig);
usedPath = "default (in-memory)";
}
}
console.log(`📋 Using configuration from: ${usedPath}`);
const raw = JSON.parse(configContent);
return {
mqttBroker: {
host: raw.mqtt_broker.host,
port: parseInt(raw.mqtt_broker.port, 10),
vdaInterface: raw.mqtt_broker.vda_interface,
},
vehicle: {
serialNumber: raw.vehicle.serial_number,
channelName: raw.vehicle.channel_name,
channelManufacturer: raw.vehicle.channel_manufacturer,
manufacturer: raw.vehicle.manufacturer,
vdaVersion: raw.vehicle.vda_version,
vdaFullVersion: raw.vehicle.vda_full_version,
},
settings: {
mapId: raw.settings.map_id,
stateFrequency: raw.settings.state_frequency,
visualizationFrequency: raw.settings.visualization_frequency,
actionTime: raw.settings.action_time,
robotCount: raw.settings.robot_count,
speed: raw.settings.speed,
},
};
}

1303
simulator_main.ts Normal file

File diff suppressed because it is too large Load Diff

72
test_events.ts Normal file
View File

@ -0,0 +1,72 @@
// test_events.ts
// 测试全局事件系统
import { globalEventManager } from "./event_manager.ts";
// 测试主线程事件监听
globalEventManager.addEventListener("test-event", (event: Event) => {
const customEvent = event as CustomEvent;
console.log("🧪 主线程收到测试事件:", customEvent.detail);
});
globalEventManager.addEventListener("agv-worker-ready", (event: Event) => {
const customEvent = event as CustomEvent;
console.log("✅ AGV Worker 已就绪:", customEvent.detail);
});
// 创建测试 Worker
const testWorker = new Worker(
new URL("./test_worker.ts", import.meta.url).href,
{ type: "module" }
);
// 注册测试 Worker
globalEventManager.registerWorker("testWorker", testWorker);
// 延迟发送一些测试事件
setTimeout(() => {
console.log("🚀 开始事件测试...");
// 发送主线程事件
globalEventManager.dispatchGlobalEvent({
type: "test-event",
source: "main",
data: { message: "Hello from main thread!", timestamp: Date.now() }
});
// 发送给特定 Worker 的事件
globalEventManager.dispatchGlobalEvent({
type: "ping",
source: "main",
target: "testWorker",
data: { message: "Ping to test worker!" }
});
}, 2000);
// 定期显示事件日志
setInterval(() => {
const recentEvents = globalEventManager.getRecentEvents(5);
if (recentEvents.length > 0) {
console.log("\n📊 最近的事件:");
recentEvents.forEach(event => {
console.log(` ${event.type} (${event.source} -> ${event.target || '广播'}) [${new Date(event.timestamp!).toLocaleTimeString()}]`);
});
console.log(`📝 已注册的 Workers: ${globalEventManager.getRegisteredWorkers().join(', ')}\n`);
}
}, 5000);
// 处理 Ctrl+C 退出
Deno.addSignalListener("SIGINT", () => {
console.log("\n📋 最终事件日志:");
const allEvents = globalEventManager.getEventLog();
allEvents.forEach(event => {
console.log(` ${event.type} (${event.source} -> ${event.target || '广播'}) [${new Date(event.timestamp!).toLocaleTimeString()}]`);
});
testWorker.terminate();
console.log("🔚 测试结束");
Deno.exit(0);
});
console.log("🎯 事件系统测试已启动,按 Ctrl+C 退出");

55
test_worker.ts Normal file
View File

@ -0,0 +1,55 @@
// test_worker.ts
// 测试 Worker
import { createWorkerEventHelper } from "./worker_event_helper.ts";
// 创建事件助手
const eventHelper = createWorkerEventHelper("testWorker");
console.log("🧪 测试 Worker 已启动");
// 添加事件监听器
eventHelper.addEventListener("ping", (event) => {
console.log("🏓 Test Worker 收到 ping:", event.data);
// 回复 pong
eventHelper.dispatchEvent("pong", {
message: "Pong from test worker!",
originalPing: event.data.message,
timestamp: Date.now()
});
});
eventHelper.addEventListener("test-event", (event) => {
console.log("🧪 Test Worker 收到测试事件:", event.data);
});
// 定期发送心跳事件
let heartbeatCount = 0;
setInterval(() => {
heartbeatCount++;
eventHelper.dispatchEvent("worker-heartbeat", {
workerName: "testWorker",
count: heartbeatCount,
timestamp: Date.now()
});
}, 3000);
// 延迟发送测试事件
setTimeout(() => {
eventHelper.dispatchEvent("test-event", {
message: "Hello from test worker!",
timestamp: Date.now()
});
}, 1000);
// 监听关闭信号
self.addEventListener("message", (event: MessageEvent) => {
const message = event.data;
if (message.type === "shutdown") {
console.log("🔚 Test Worker 收到关闭信号");
self.close();
}
});
console.log("✅ 测试 Worker 事件系统已初始化");

View File

@ -0,0 +1,428 @@
import mqtt, { MqttClient, IClientOptions } from "npm:mqtt";
// ==== 消息/配置类型 ====
/** init 消息接口,从主线程传入 */
interface InitMessage {
type: "init";
mqtt: {
brokerUrl: string;
clientId: string;
username?: string;
password?: string;
reconnectInterval?: number;
qos?: 0 | 1 | 2;
};
/**
* topic
*/
mappings: Array<{
/** MQTT 订阅的源 topic可以包含 + 或 # 通配符 */
sourceTopic: string;
/** 发布时使用的目标 topic 模板,可包含 {agvId} 等占位符 */
targetTopicTemplate: string;
/** transformGeneric 用到的映射规则 */
mapping: Record<string, MappingRule>;
}>;
instanceId: string;
}
/** 停止 Worker 的消息 */
interface StopMessage {
type: "stop" | "shutdown"; // 支持 stop 和 shutdown
}
// transformGeneric 中用到的规则
type MappingRule =
| string // 简单字符串映射,直接从源路径复制
| {
op: "const";
value: any;
source?: string;
}
| {
type: "object";
op?: "object";
source?: string;
mapping: Record<string, MappingRule>;
}
| {
type: "array";
op?: "array";
source: string;
mapping: Record<string, MappingRule>;
}
| {
op: "toString";
source: string;
}
| {
// 通用对象映射(没有 type 字段)
mapping: Record<string, MappingRule>;
source?: string;
};
// ==== 通用工具函数 ====
/** 按路径 a.b.c 从 obj 取值 */
function getByPath(obj: any, path: string): any {
return path.split(".").reduce((o, key) => (o != null ? o[key] : undefined), obj);
}
/** 按路径 a.b.c 在 target 上写入 value */
function setByPath(target: any, path: string, value: any) {
const keys = path.split(".");
let cur = target;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in cur) || typeof cur[k] !== "object") {
cur[k] = {};
}
cur = cur[k];
}
cur[keys[keys.length - 1]] = value;
}
/** 通用的 JSON 转换器 */
function transformGeneric(raw: any, mapping: Record<string, MappingRule>): any {
const out: any = {};
for (const [outKey, rule] of Object.entries(mapping)) {
try {
// 处理简单字符串映射
if (typeof rule === "string") {
const value = getByPath(raw, rule);
if (value !== undefined) {
setByPath(out, outKey, value);
}
continue;
}
// 处理对象规则
if (typeof rule === "object" && rule !== null) {
// 常量值映射
if ("op" in rule && rule.op === "const") {
setByPath(out, outKey, rule.value);
continue;
}
// toString 操作
if ("op" in rule && rule.op === "toString") {
const sourceValue = getByPath(raw, rule.source);
if (sourceValue !== undefined) {
// 如果已经是字符串,直接使用;否则进行 JSON 序列化
const stringValue = typeof sourceValue === "string"
? sourceValue
: JSON.stringify(sourceValue);
setByPath(out, outKey, stringValue);
}
continue;
}
// 对象映射
if ("type" in rule && rule.type === "object") {
const subRaw = rule.source ? getByPath(raw, rule.source) : raw;
if (subRaw !== undefined) {
const transformedObj = transformGeneric(subRaw, rule.mapping);
setByPath(out, outKey, transformedObj);
}
continue;
}
// 数组映射
if ("type" in rule && rule.type === "array") {
const sourceArray = getByPath(raw, rule.source);
if (Array.isArray(sourceArray)) {
const transformedArray = sourceArray.map(item =>
transformGeneric(item, rule.mapping)
);
setByPath(out, outKey, transformedArray);
} else {
setByPath(out, outKey, []);
}
continue;
}
// 如果是对象但没有特殊操作,尝试作为嵌套映射处理
if ("mapping" in rule && typeof rule.mapping === "object") {
const mappingRule = rule as { mapping: Record<string, MappingRule>; source?: string };
const subRaw = mappingRule.source ? getByPath(raw, mappingRule.source) : raw;
if (subRaw !== undefined) {
const transformedObj = transformGeneric(subRaw, mappingRule.mapping);
setByPath(out, outKey, transformedObj);
}
continue;
}
}
console.warn(`Unknown mapping rule for key "${outKey}":`, rule);
} catch (error) {
console.error(`Error processing mapping rule for key "${outKey}":`, error);
}
}
return out;
}
/**
* topic topic +, #
*/
function topicMatches(pattern: string, topic: string): boolean {
const patSeg = pattern.split("/");
const topSeg = topic.split("/");
for (let i = 0; i < patSeg.length; i++) {
const p = patSeg[i];
const t = topSeg[i];
if (p === "#") return true; // 后面的全部都匹配
if (p === "+") continue; // 单层通配符
if (t === undefined) return false;
if (p !== t) return false;
}
return patSeg.length === topSeg.length;
}
/**
* topic pattern '+'
* {agvId}
*/
function extractAgvId(pattern: string, topic: string): string | null {
const patSeg = pattern.split("/");
const topSeg = topic.split("/");
for (let i = 0; i < patSeg.length; i++) {
if (patSeg[i] === "+") {
return topSeg[i];
}
}
return null;
}
// ==== 全局状态 ====
let mqttClient: MqttClient | null = null;
let reconnectTimer: any = null;
const CONFIG: {
mqtt: {
brokerUrl: string;
clientId: string;
username?: string;
password?: string;
reconnectInterval?: number;
qos?: 0 | 1 | 2;
};
mappings: Array<{
sourceTopic: string;
targetTopicTemplate: string;
mapping: Record<string, MappingRule>;
}>;
} = {
mqtt: {
brokerUrl: "",
clientId: "",
username: undefined,
password: undefined,
reconnectInterval: 5000,
qos: 1,
},
mappings: [],
};
// instance identifier generated from config.manufacturer and UUID
let INSTANCE_ID: string = "";
// 连续 MQTT 错误计数只有超过10次才触发重连
let consecutiveErrors = 0;
// ==== MQTT 逻辑 ====
async function connectAndSubscribe() {
try {
const opts: IClientOptions = {
clientId: CONFIG.mqtt.clientId,
username: CONFIG.mqtt.username,
password: CONFIG.mqtt.password,
reconnectPeriod: 3000, // 我们自己重连
};
mqttClient = mqtt.connect(CONFIG.mqtt.brokerUrl, opts);
// 预先注册错误、断开和关闭处理器
mqttClient.on("error", onMqttError);
mqttClient.on("disconnect", onMqttDisconnect);
mqttClient.on("close", onMqttClose);
mqttClient.on("connect", async () => {
// 连接成功后重置错误计数
consecutiveErrors = 0;
// 订阅所有 mappings 里配置的源 topic
for (const m of CONFIG.mappings) {
const inTopic = m.sourceTopic
.replace("{instanceId}", INSTANCE_ID);
// console.log("inTopic", inTopic);
try {
await mqttClient!.subscribe(inTopic, { qos: CONFIG.mqtt.qos! });
} catch (err) {
console.error(`✗ MQTT subscribe failed for ${inTopic}:`, err);
self.postMessage({ type: "error", error: "subscribe_failed", details: (err as Error).message });
scheduleReconnect();
return; // 订阅失败,中断后续处理,尝试重连
}
}
// 订阅完成后监听消息
mqttClient!.on("message", onMqttMessage);
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
self.postMessage({ type: "status", status: "connected" });
});
} catch (err) {
console.error("✗ MQTT connect failed:", err);
self.postMessage({
type: "error",
error: "connect_failed",
details: err instanceof Error ? err.message : String(err),
});
scheduleReconnect();
}
}
async function onMqttMessage(topic: string, payload: Uint8Array) {
try {
const raw = JSON.parse(new TextDecoder().decode(payload));
// 对每个匹配的 mappingConfig做一次转换并发布
for (const m of CONFIG.mappings) {
const inTopic = m.sourceTopic
.replace("{instanceId}", INSTANCE_ID);
if (!topicMatches(inTopic, topic)) continue;
// 提取占位符 agvId
const agvId = extractAgvId(inTopic, topic) ?? "";
// 转换
const transformed = transformGeneric(raw, m.mapping);
// 填充目标 topic
const outTopic = m.targetTopicTemplate
.replace("{instanceId}", INSTANCE_ID)
.replace("{agvId}", agvId);
// 发布
// console.log("outTopic", outTopic);
await mqttClient!.publish(outTopic, JSON.stringify(transformed), {
qos: CONFIG.mqtt.qos!,
});
self.postMessage({
type: "published",
topic: outTopic,
agvId,
sourceTopic: topic,
});
}
} catch (err) {
console.error("✗ onMqttMessage error:", err);
self.postMessage({
type: "error",
error: "process_message",
details: err instanceof Error ? err.message : String(err),
});
}
}
function onMqttClose() {
console.log("vda onMqttClose");
self.postMessage({ type: "status", status: "closed" });
// 确保在 close 时释放底层 socket
if (mqttClient) {
mqttClient.end(true);
mqttClient = null;
}
self.postMessage({ type: "reconnect-down" });
// connectAndSubscribe();
}
function onMqttDisconnect() {
self.postMessage({ type: "status", status: "disconnected" });
console.log("vda onMqttDisconnect");
// 断开时也关闭客户端,避免累积未关闭的连接
if (mqttClient) {
mqttClient.end(true);
mqttClient = null;
}
scheduleReconnect();
}
function onMqttError(err: Error) {
// 如果是连接被拒绝错误,通知主线程重启所有 worker
const anyErr = err as any;
if (anyErr.code === 'ECONNREFUSED' || anyErr.code === 'ECONNRESET') {
console.error('❌ MQTT connection refused, requesting full restart:', anyErr);
self.postMessage({ type: 'reconnect-all' });
}
// 增加错误计数只在超过10次后才重连
consecutiveErrors++;
console.error(`! MQTT error (#${consecutiveErrors}):`, err);
self.postMessage({
type: "error",
error: "mqtt_error",
details: err.message,
});
if (consecutiveErrors >= 5) {
// 重置错误计数
consecutiveErrors = 0;
if (mqttClient) {
mqttClient.end(true);
mqttClient = null;
}
scheduleReconnect();
}
}
function scheduleReconnect() {
if (!reconnectTimer) {
const t = CONFIG.mqtt.reconnectInterval;
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
console.log("vda schedule Worker 自身重连");
self.postMessage({ type: "reconnect-down" });
connectAndSubscribe();
}, t);
}
}
// ==== 主线程消息处理 ====
self.onmessage = (e: MessageEvent<InitMessage | StopMessage>) => {
const msg = e.data;
if (msg.type === "init") {
// 覆盖配置
CONFIG.mqtt = {
...CONFIG.mqtt,
brokerUrl: msg.mqtt.brokerUrl,
clientId: msg.mqtt.clientId,
username: msg.mqtt.username,
password: msg.mqtt.password,
reconnectInterval: msg.mqtt.reconnectInterval ?? CONFIG.mqtt.reconnectInterval,
qos: msg.mqtt.qos ?? CONFIG.mqtt.qos,
};
CONFIG.mappings = msg.mappings;
// set our instance identifier
INSTANCE_ID = msg.instanceId;
console.log("vda5050_transformer_worker: instanceId", INSTANCE_ID);
// 启动连接
connectAndSubscribe();
} else if (msg.type === "stop" || msg.type === "shutdown") {
// 支持 stop 和 shutdown统一关闭
if (mqttClient) {
mqttClient.end(true);
mqttClient = null;
}
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
self.postMessage({ type: "status", status: "stopped" });
}
};

837
vda_worker.ts Normal file
View File

@ -0,0 +1,837 @@
/// <reference lib="deno.worker" />
import {
MasterController,
AgvId,
ClientOptions,
Topic,
Order,
State,
Headerless,
InstantActions,
BlockingType,
Connection,
Factsheet,
} from "vda-5050-lib";
import { v4 as uuidv4 } from "npm:uuid";
import { createWorkerEventHelper } from "./worker_event_helper.ts";
// 创建事件助手
const eventHelper = createWorkerEventHelper("vdaWorker");
console.log("VDA 5050 Worker initialized");
// 预注册设备列表(后续主程序可动态更新)
const preRegisteredDevices: AgvId[] = [
];
// 用于保存所有设备状态及动态更新设备列表
const currentDevices: Map<
string,
{ agvId: AgvId; lastSeen: number; isOnline: boolean; state?: State }
> = new Map();
// 内部方法:生成设备唯一 key
function getDeviceKey(agvId: AgvId) {
return `${agvId.manufacturer}-${agvId.serialNumber}`;
}
// 先将预注册设备保存到 currentDevices初始状态为离线
preRegisteredDevices.forEach((device) => {
const key = getDeviceKey(device);
currentDevices.set(key, {
agvId: device,
lastSeen: 0,
isOnline: false,
});
});
// 用于保存已经订阅状态的设备,避免重复订阅
const subscribedDevices: Set<string> = new Set();
// 全局保存 MasterController 实例
let masterController: MasterController | null = null;
// 从初始化参数读取的接口名和制造商
let interfaceNameValue = "oagv";
let manufacturerValue = "gateway";
// 标记 Master Controller 是否启动完成
let clientStarted = false;
/**
* init MQTT broker
*/
function initializeControllerWithOptions(brokerUrl: string, iValue: string) {
// Validate input parameters
if (!brokerUrl || !iValue) {
console.error("❌ Invalid initialization parameters:");
console.error("brokerUrl:", brokerUrl);
console.error("interfaceName:", iValue);
return;
}
// Reset previous state
clientStarted = false;
if (masterController) {
try {
masterController.stop().catch(() => {});
} catch (e) {
// Ignore stop errors
}
masterController = null;
}
const clientOptions: ClientOptions = {
interfaceName: iValue,
transport: {
// 使用启动参数传入的 MQTT server 地址
brokerUrl: brokerUrl,
heartbeat:5,
reconnectPeriod:1000,
connectTimeout:5000,
},
vdaVersion: "2.0.0",
};
console.log(`🚀 正在初始化VDA 5050 Master Controller...`);
console.log(`📡 MQTT Broker: ${brokerUrl}`);
console.log(`🏷️ Interface: ${iValue}`);
console.log(`🏭 Manufacturer: ${manufacturerValue}`);
// Test MQTT broker connectivity
console.log(`🔍 Testing MQTT broker connectivity...`);
try {
const url = new URL(brokerUrl);
console.log(`📍 Broker host: ${url.hostname}, port: ${url.port || 1883}`);
} catch (urlError) {
console.error("❌ Invalid broker URL format:", urlError);
return;
}
try {
masterController = new MasterController(clientOptions, {});
console.log("📦 MasterController instance created successfully");
} catch (error) {
console.error("❌ Failed to create MasterController instance:", error);
return;
}
// Add timeout to prevent hanging
Promise.race([
masterController.start(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Master controller start timeout after 30 seconds")), 30000)
)
])
.then(() => {
clientStarted = true;
console.log("✅ VDA 5050 master controller started successfully");
self.postMessage({ type: "started" });
// 跟踪所有AGV连接状态
masterController!.trackAgvs((trackAgvId, connectionState, timestamp) => {
const key = getDeviceKey(trackAgvId);
// 如果设备不在设备列表中,则不予上线
// console.log("->", key, connectionState);
// if (!currentDevices.has(key)) {
// console.warn(
// `收到未知设备 ${trackAgvId.manufacturer}/${trackAgvId.serialNumber} 的状态消息,忽略上线。`
// );
// return;
// }
const ts = Number(timestamp);
const lastSeen = isNaN(ts) ? Date.now() : ts;
// 自动添加新设备到currentDevices无需预先配置
let record = currentDevices.get(key);
if (!record) {
// 动态添加新发现的设备
record = { agvId: trackAgvId, lastSeen: 0, isOnline: false };
currentDevices.set(key, record);
console.log(`🆕 自动添加新设备: ${trackAgvId.manufacturer}/${trackAgvId.serialNumber}`);
}
const wasOffline = !record.isOnline;
// 无条件置为 ONLINE 并更新最后更新时间
currentDevices.set(key, { ...record, lastSeen, isOnline: true });
// 发送上线状态更新信息
self.postMessage({
type: "connectionState",
data: { agvId: trackAgvId, state: "ONLINE", timestamp: lastSeen },
});
if (wasOffline) {
// console.log(
// `设备 ${trackAgvId.manufacturer}/${trackAgvId.serialNumber} 新上线`
// );
// 额外通知一次"新上线"
self.postMessage({
type: "connectionState",
data: { agvId: trackAgvId, state: "ONLINE", timestamp: lastSeen },
});
}
// 若设备首次出现,则订阅其状态更新
if (!subscribedDevices.has(key)) {
subscribedDevices.add(key);
try {
// 使用 trackAgvId 本身作为 AgvId 订阅
masterController!.subscribe(
Topic.State,
{ manufacturer: manufacturerValue, serialNumber: trackAgvId.serialNumber },
(state: State) => {
const subKey = getDeviceKey(trackAgvId);
// 自动添加新设备,无需预先配置
let existing = currentDevices.get(subKey);
if (!existing) {
existing = { agvId: trackAgvId, lastSeen: 0, isOnline: false };
currentDevices.set(subKey, existing);
console.log(`🆕 状态订阅中自动添加新设备: ${trackAgvId.manufacturer}/${trackAgvId.serialNumber}`);
}
const wasOfflineInSub = !existing.isOnline;
currentDevices.set(subKey, {
...existing,
lastSeen: Date.now(),
state,
isOnline: true,
});
if (wasOfflineInSub) {
// console.log(
// `设备 ${trackAgvId.manufacturer}/${trackAgvId.serialNumber} 新上线(订阅)`
// );
self.postMessage({
type: "connectionState",
data: {
agvId: trackAgvId,
state: "ONLINE",
timestamp: Date.now(),
},
});
}
self.postMessage({
type: "stateUpdate",
data: {
agvId: trackAgvId,
state: state,
timestamp: Date.now(),
},
});
}
);
// Subscribe to Factsheet topic
masterController!.subscribe(
Topic.Factsheet,
{ manufacturer: manufacturerValue, serialNumber: trackAgvId.serialNumber },
(factsheet: Factsheet) => {
// console.log("收到 factsheet 消息", factsheet);
self.postMessage({
type: "factsheet",
data: { agvId: trackAgvId, factsheet, timestamp: Date.now() },
});
}
);
// Subscribe to Connection topic
masterController!.subscribe(
Topic.Connection,
{ manufacturer: manufacturerValue, serialNumber: trackAgvId.serialNumber },
(connection: Connection) => {
self.postMessage({
type: "deviceDiscovered",
data: { agvId: trackAgvId, connection, timestamp: Date.now() },
});
}
);
} catch (error) {
console.error(`Failed to subscribe to tracked device ${trackAgvId.manufacturer}/${trackAgvId.serialNumber}:`, error);
// 移除已添加的订阅标记,以便重试
subscribedDevices.delete(key);
}
}
});
// 定时检测设备状态,超时则标记为离线
const stateUpdateCycle = 5000;
const offlineThreshold = stateUpdateCycle * 3;
setInterval(() => {
const now = Date.now();
currentDevices.forEach((device, key) => {
// console.log(
// `设备 ${device.agvId.manufacturer}/${device.agvId.serialNumber} - lastSeen: ${device.lastSeen}, isOnline: ${device.isOnline}`
// );
if (now - device.lastSeen > offlineThreshold && device.isOnline) {
device.isOnline = false;
currentDevices.set(key, device);
// console.log(
// `设备 ${device.agvId.manufacturer}/${device.agvId.serialNumber} 超过 ${offlineThreshold} 毫秒未更新,标记为下线`
// );
}
self.postMessage({
type: "connectionState",
data: { agvId: device.agvId, state: device.isOnline ? "ONLINE" : "OFFLINE", timestamp: now },
});
});
}, stateUpdateCycle);
})
.catch((error) => {
console.error("❌ Failed to start VDA 5050 master controller:");
console.error("Error details:", error);
console.error("Error message:", error?.message || "Unknown error");
console.error("Error stack:", error?.stack || "No stack trace");
// Reset client state
clientStarted = false;
masterController = null;
// Schedule retry after delay
console.log("🔄 Scheduling retry in 10 seconds...");
setTimeout(() => {
console.log("🔄 Retrying VDA 5050 master controller initialization...");
initializeControllerWithOptions(brokerUrl, iValue);
eventHelper.dispatchEvent("reconnect-all", {
reason: "grpc-stream-failed",
retryCount: 5,
timestamp: Date.now()
});
self.postMessage({ type: "reconnect-all" });
}, 10000);
});
}
/**
*
*
*/
function subscribeWithRetry(device: AgvId, retryCount = 0) {
const key = getDeviceKey(device);
// console.log("订阅设备->", key);
// 自动添加新设备,无需预先配置
if (!currentDevices.has(key)) {
currentDevices.set(key, { agvId: device, lastSeen: 0, isOnline: false });
console.log(`🆕 订阅时自动添加新设备: ${device.manufacturer}/${device.serialNumber}`);
}
if (!clientStarted) {
if (retryCount < 50000) {
// console.warn("Client not started, retry subscribing after delay...", device);
setTimeout(() => {
subscribeWithRetry(device, retryCount + 1);
}, 3000);
} else {
console.error("订阅失败:超过最大重试次数", device);
}
return;
}
if (!subscribedDevices.has(key)) {
subscribedDevices.add(key);
try {
// 双重检查客户端状态
if (!clientStarted || !masterController) {
subscribedDevices.delete(key);
setTimeout(() => {
subscribeWithRetry(device, retryCount + 1);
}, 3000);
return;
}
// Subscribe to State topic
masterController!.subscribe(
Topic.State,
{ manufacturer: manufacturerValue, serialNumber: device.serialNumber },
(state: State) => {
const subKey = getDeviceKey(device);
// 自动添加新设备,无需预先配置
let existing = currentDevices.get(subKey);
if (!existing) {
existing = { agvId: device, lastSeen: 0, isOnline: false };
currentDevices.set(subKey, existing);
console.log(`🆕 状态更新中自动添加新设备: ${device.manufacturer}/${device.serialNumber}`);
}
const wasOfflineInSub = !existing.isOnline;
currentDevices.set(subKey, {
...existing,
lastSeen: Date.now(),
state,
isOnline: true,
});
if (wasOfflineInSub) {
self.postMessage({
type: "connectionState",
data: { agvId: device, state: "ONLINE", timestamp: Date.now() },
});
}
self.postMessage({
type: "stateUpdate",
data: { agvId: device, state, timestamp: Date.now() },
});
}
);
// Subscribe to Factsheet topic
masterController!.subscribe(
Topic.Factsheet,
{ manufacturer: manufacturerValue, serialNumber: device.serialNumber },
(factsheet: Factsheet) => {
// console.log("收到 factsheet 消息", factsheet);
self.postMessage({
type: "factsheet",
data: { agvId: device, factsheet, timestamp: Date.now() },
});
}
);
// Subscribe to Connection topic
masterController!.subscribe(
Topic.Connection,
{ manufacturer: manufacturerValue, serialNumber: device.serialNumber },
(connection: Connection) => {
self.postMessage({
type: "deviceDiscovered",
data: { agvId: device, connection, timestamp: Date.now() },
});
}
);
} catch (error) {
console.error(`Failed to subscribe to device ${device.manufacturer}/${device.serialNumber}:`, error);
// 移除已添加的订阅标记,以便重试
subscribedDevices.delete(key);
// 如果客户端未启动,重新调度重试
if (!clientStarted) {
setTimeout(() => {
subscribeWithRetry(device, 0);
}, 5000);
}
}
}
}
// 处理来自主线程的消息
self.onmessage = async (event) => {
const message = event.data;
// console.log("Received message from main thread:", message);
// 如果收到 init 消息,从启动参数中传入 MQTT server 地址
if (message.type === "init") {
// 从主线程传入初始化参数
// data: {
// brokerUrl: config.mqtt.brokerUrl,
// interfaceName: config.interfaceName,
// manufacturer: config.manufacturer,
// instanceId: config.instanceId
// },
console.log("event", message);
const { brokerUrl, interfaceName, manufacturer, instanceId } = message.data;
console.log(`init params → brokerUrl: ${brokerUrl}, interfaceName: ${interfaceName}, manufacturer: ${manufacturer}, instanceId: ${instanceId}`);
manufacturerValue = instanceId;
interfaceNameValue = interfaceName;
initializeControllerWithOptions(brokerUrl, interfaceNameValue);
}
// 处理单个设备移除
if (message.type === "removeDevice") {
const { manufacturer, serialNumber } = message.data;
const deviceKey = getDeviceKey({ manufacturer, serialNumber });
if (currentDevices.has(deviceKey)) {
// 移除设备
currentDevices.delete(deviceKey);
subscribedDevices.delete(deviceKey);
console.log(`🗑️ 已移除设备: ${manufacturer}/${serialNumber}`);
// 通知主线程设备已移除
self.postMessage({
type: "deviceRemoved",
data: {
manufacturer,
serialNumber,
deviceKey,
remainingDevices: currentDevices.size
}
});
} else {
console.log(`⚠️ 设备 ${manufacturer}/${serialNumber} 不存在,无法移除`);
}
}
// 动态更新设备列表(主程序从配置文件发送更新消息)
if (message.type === "updateDeviceList") {
const newDeviceList: AgvId[] = message.data;
console.log(`🔄 VDA Worker 收到设备列表更新,包含 ${newDeviceList.length} 个设备`);
// 构造新设备的 Key 集合
const newKeys = new Set(newDeviceList.map(getDeviceKey));
let addedCount = 0;
let updatedCount = 0;
// 遍历新设备列表,新增或更新记录,并进行状态订阅
newDeviceList.forEach((device) => {
const key = getDeviceKey(device);
if (!currentDevices.has(key)) {
currentDevices.set(key, { agvId: device, lastSeen: 0, isOnline: false });
console.log(` 新增设备: ${device.manufacturer}/${device.serialNumber}`);
addedCount++;
} else {
// 更新 agvId 信息(如果有必要)
const record = currentDevices.get(key)!;
record.agvId = device;
currentDevices.set(key, record);
console.log(`🔄 更新设备: ${device.manufacturer}/${device.serialNumber}`);
updatedCount++;
}
// 根据设备列表订阅对应的状态更新(重试订阅,直到客户端启动)
subscribeWithRetry(device);
});
// 对于 currentDevices 中存在但不在最新列表中的设备,置为离线
let removedCount = 0;
currentDevices.forEach((device, key) => {
if (!newKeys.has(key)) {
device.isOnline = false;
currentDevices.set(key, device);
console.log(`⏸️ 设备离线: ${device.agvId.manufacturer}/${device.agvId.serialNumber}`);
removedCount++;
}
});
console.log(`✅ 设备列表更新完成: 新增 ${addedCount}, 更新 ${updatedCount}, 离线 ${removedCount}`);
console.log(`📊 当前管理设备总数: ${currentDevices.size}`);
// 通知主线程最新设备列表情况
self.postMessage({
type: "deviceListUpdated",
data: {
total: currentDevices.size,
added: addedCount,
updated: updatedCount,
removed: removedCount,
devices: Array.from(currentDevices.values()).map(d => ({
manufacturer: d.agvId.manufacturer,
serialNumber: d.agvId.serialNumber,
isOnline: d.isOnline
}))
}
});
}
if (message.type === "orderForwarded") {
// Check if client is started before processing orders
if (!clientStarted || !masterController) {
console.warn("❌ VDA client not started yet, ignoring order request");
return;
}
// console.log("收到 AGV 订单消息1", message);
// 解构出 agvId 串号和 order 对象
const { agvId: agvSerial, order: msg } = message.data as { agvId: string; order: any };
// Rebuild order payload as Headerless<Order>
const order: Headerless<Order> = {
orderId: msg.orderId,
orderUpdateId: msg.orderUpdateId || 0,
nodes: msg.nodes.map((node: any, idx: number) => ({
nodeId: node.nodeId,
sequenceId: idx * 2,
released: node.released ?? true,
nodePosition: node.nodePosition,
actions: node.actions || []
})),
edges: []
};
if (msg.nodes.length > 1) {
for (let i = 0; i < msg.nodes.length - 1; i++) {
order.edges.push({
edgeId: msg.edges?.[i]?.edgeId || `edge${i}to${i + 1}`,
sequenceId: i * 2 + 1,
startNodeId: msg.nodes[i].nodeId,
endNodeId: msg.nodes[i + 1].nodeId,
released: msg.edges?.[i]?.released ?? true,
actions: msg.edges?.[i]?.actions || []
});
}
}
let devId: any = undefined;
// console.log("检查设备", currentDevices, msg.agvId);
currentDevices.forEach((device, key) => {
// console.log("检查设备", device, key, device.agvId.serialNumber, agvSerial);
if (device.agvId.serialNumber === agvSerial) {
devId = device.agvId;
}
});
if (devId) {
// console.log("收到 AGV 订单消息2", devId);
try {
// console.log("---->",{ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, order);
await masterController!.assignOrder({ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, order, {
onOrderProcessed: (err, canceled, active, ctx) => {
if (err) {
console.error("Order 被拒绝", err);
} else if (canceled) {
console.log("Order 被取消", ctx.order);
} else if (active) {
console.log("Order 正在执行", ctx.order);
} else {
console.log("Order 完成", ctx.order);
}
},
onNodeTraversed: (node, nextEdge, nextNode, ctx) => {
console.log("节点遍历完成", node);
},
onEdgeTraversing: (edge, start, end, stateChanges, count, ctx) => {
console.log("开始路径", edge);
},
onEdgeTraversed: (edge, start, end, ctx) => {
console.log("路径遍历完成", edge);
},
onActionStateChanged: (actionState, error) => {
console.log("Action 状态变化", actionState, error || "");
}
});
} catch (err) {
console.error("assignOrder 异常", err);
}
} // end if(agvId)
} // end if(message.type === "orderForwarded")
// 2) 收到 InstantActions 转发
if (message.type === "instantActionsForwarded") {
// Check if client is started before processing instant actions
if (!clientStarted || !masterController) {
console.warn("❌ VDA client not started yet, ignoring instant actions request");
return;
}
// console.log("收到 AGV 即时动作消息", message);
const msg: any = message.data;
// const actions: InstantActions = msg.actions;
const { agvId, actions } = msg as {
agvId: string;
actions: Array<{
actionType: string; /* …其它可能的字段… */
actionParameters: Array<{ key: string; value: string }>;
actionDescription: string;
actionId: string;
blockingType: string;
}>;
};
// console.log("收到 AGV 即时动作消息", agvId, actions);
let devId: any = undefined;
currentDevices.forEach((device, key) => {
if (device.agvId.serialNumber === msg.agvId) {
devId = device.agvId;
}
});
if (devId) {
// console.log("收到 AGV 即时动作消息", msg);
try {
const headerless: Headerless<InstantActions> = {
actions: actions.map(a => ({
actionType: a.actionType, // 必填
actionId: a.actionId, // 必填
blockingType: a.blockingType === "HARD" ? BlockingType.Hard : BlockingType.None, // 必填,使用枚举
actionParameters: a.actionParameters || [], // 使用原始的actionParameters或空数组
actionDescription: "action parameters", // 可选
}))
};
// console.log("=====>",headerless);
await masterController!.initiateInstantActions({ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, headerless, {
onActionStateChanged: (actionState, withError, action, agvId, state) => console.log("Instant action state changed: %o %o %o", actionState, withError, action),
onActionError: (error, action, agvId, state) => console.log("Instant action error: %o %o %o", error, action, state),
});
} catch (err) {
console.error("initiateInstantActions 异常", err);
}
} // end if(agvId)
} // end if(message.type === "instantActionsForwarded")
// 处理发送订单请求
if (message.type === "sendOrder") {
// Check if client is started before processing orders
if (!clientStarted || !masterController) {
console.warn("❌ VDA client not started yet, ignoring send order request");
return;
}
try {
const order: Headerless<Order> = {
orderId: message.orderId || masterController!.createUuid(),
orderUpdateId: 0,
nodes: message.nodes.map((node: any, index: number) => ({
nodeId: node.nodeId,
sequenceId: index * 2,
released: true,
nodePosition: node.nodePosition,
actions: node.actions || [],
})),
edges: [],
};
if (message.nodes.length > 1) {
for (let i = 0; i < message.nodes.length - 1; i++) {
order.edges.push({
edgeId: `edge${i}to${i + 1}`,
sequenceId: i * 2 + 1,
startNodeId: message.nodes[i].nodeId,
endNodeId: message.nodes[i + 1].nodeId,
released: true,
actions: [],
});
}
}
// 使用预注册列表中的第一个设备发送订单
let devId: any = undefined;
// currentDevices.forEach((device, key) => {
// if ( device.agvId.serialNumber === 'ZKG-0') {
// devId = device.agvId;
// }
// });
if (devId) {
await masterController!.assignOrder({ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, order, {
onOrderProcessed: (withError, byCancelation, active, ctx) => {
console.log("Order processed", { withError, byCancelation, active });
self.postMessage({
type: "orderCompleted",
orderId: order.orderId,
withError,
byCancelation,
active,
});
},
onNodeTraversed: (node, nextEdge, nextNode, ctx) => {
console.log("Order node traversed:", node);
self.postMessage({
type: "nodeTraversed",
node,
nextEdge,
nextNode,
});
},
onEdgeTraversing: (
edge,
startNode,
endNode,
stateChanges,
invocationCount,
ctx
) => {
console.log("Order edge traversing:", edge);
self.postMessage({
type: "edgeTraversing",
edge,
startNode,
endNode,
});
},
onEdgeTraversed: (edge, startNode, endNode, ctx) => {
console.log("Order edge traversed:", edge);
self.postMessage({ type: "edgeTraversed", edge, startNode, endNode });
},
});
}
console.log("Order assigned successfully");
self.postMessage({ type: "orderSent", orderId: order.orderId });
} catch (error) {
console.error("Failed to send order:", error);
self.postMessage({ type: "error", error: (error as Error).message });
}
}
// 处理取消订单请求
if (message.type === "cancelOrder") {
// 此处添加取消订单逻辑……
}
// 处理主动请求设备列表的消息
if (message.type === "discoverDevices") {
const devicesList = Array.from(currentDevices.values())
.filter(d => d.isOnline)
.map(d => ({
agvId: d.agvId,
isOnline: d.isOnline,
x: d.state?.agvPosition?.x,
y: d.state?.agvPosition?.y,
theta: d.state?.agvPosition?.theta,
actionStatus: d.state?.actionStates,
lastNodeId: d.state?.lastNodeId,
lastNodeSequenceId: d.state?.lastNodeSequenceId,
nodeStates: d.state?.nodeStates,
edgeStates: d.state?.edgeStates,
driving: d.state?.driving,
errors: d.state?.errors?.map(err => ({
errorType: err.errorType,
errorLevel: err.errorLevel,
errorDescription: err.errorDescription,
errorReferences: err.errorReferences?.map((ref: any) => ({
referenceKey: ref.referenceKey,
referenceValue: ref.referenceValue
}))
})),
information: d.state?.information || []
}));
console.log("currentDevices", JSON.stringify(Array.from(currentDevices.values()), null, 2));
self.postMessage({ type: "devicesList", data: devicesList });
}
// 处理factsheet请求
if (message.type === "factsheetRequest") {
// Check if client is started before processing factsheet requests
if (!clientStarted || !masterController) {
console.warn("❌ VDA client not started yet, ignoring factsheet request");
return;
}
const { agvId } = message.data || {};
if (!agvId) {
console.warn("❌ No agvId provided for factsheet request");
return;
}
console.log(`📋 Processing factsheet request for AGV: ${agvId}`);
// Find the device in currentDevices
let targetDevice: any = undefined;
currentDevices.forEach((device, key) => {
if (device.agvId.serialNumber === agvId) {
targetDevice = device.agvId;
}
});
if (targetDevice) {
try {
// Request factsheet using instant action
const factsheetAction: Headerless<InstantActions> = {
actions: [{
actionType: "factsheetRequest",
actionId: `factsheet_${Date.now()}`,
blockingType: BlockingType.None,
actionParameters: [],
actionDescription: "Request device factsheet"
}]
};
await masterController!.initiateInstantActions(
{ manufacturer: manufacturerValue, serialNumber: targetDevice.serialNumber },
factsheetAction,
{
onActionStateChanged: (actionState, withError, action, agvId, state) => {
console.log("Factsheet action state changed:", actionState, withError ? "with error" : "success");
},
onActionError: (error, action, agvId, state) => {
console.error("Factsheet action error:", error);
},
}
);
console.log(`✅ Factsheet request sent for AGV: ${agvId}`);
} catch (err) {
console.error("❌ Failed to send factsheet request:", err);
}
} else {
console.warn(`⚠️ AGV ${agvId} not found in current devices`);
}
}
// 处理 shutdown 消息
if (message === "shutdown" || message.type === "shutdown") {
console.log("收到 shutdown 消息,退出 Worker");
// 此处可添加关闭逻辑
}
};
// 在 worker 退出时关闭 Master Controller
addEventListener("unload", () => {
console.log("Closing VDA 5050 Worker");
masterController?.stop().catch((err: Error) => console.log(err));
});

289
web_worker.ts Normal file
View File

@ -0,0 +1,289 @@
import { Hono } from '@hono/hono';
import { serveStatic } from '@hono/hono/serve-static';
import { html } from '@hono/hono/html';
import { Context } from '@hono/hono';
// Create Hono app
const app = new Hono();
// Store AGV positions
const agvPositions: Map<string, { x: number; y: number; theta: number }> = new Map();
let server: { shutdown: () => Promise<void> } | null = null;
let isRunning = false;
// Serve static files
app.use('/*', serveStatic({
root: './',
getContent: async (path, c) => {
try {
const file = await Deno.readFile(path);
return file;
} catch {
return null;
}
}
}));
// Main page with canvas
app.get('/', (c: Context) => {
return c.html(html`
<!DOCTYPE html>
<html>
<head>
<title>AGV Position Monitor</title>
<style>
body {
margin: 0;
padding: 20px;
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
canvas {
background-color: white;
border: 1px solid #ccc;
margin: 20px 0;
}
.controls {
margin: 20px 0;
}
button {
padding: 8px 16px;
margin-right: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>AGV Position Monitor</h1>
<div class="controls">
<button onclick="startUpdates()">Start Updates</button>
<button onclick="stopUpdates()">Stop Updates</button>
</div>
<canvas id="agvCanvas" width="800" height="800"></canvas>
<script>
const canvas = document.getElementById('agvCanvas');
const ctx = canvas.getContext('2d');
let updateInterval;
// Scale factor for visualization
const SCALE = 2; // Scale adjusted for 400x400 meter area
const AGV_SIZE = 8; // AGV representation size
const GRID_SIZE = 400; // 400x400 meter grid
const COLORS = ['#4CAF50', '#2196F3', '#FFC107', '#9C27B0', '#FF5722', '#607D8B'];
function getColor(id) {
// Simple hash function to get consistent color for each AGV
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}
return COLORS[Math.abs(hash) % COLORS.length];
}
function drawAGV(x, y, theta, id, color) {
ctx.save();
// Center the canvas and flip Y axis to match coordinate system
ctx.translate(canvas.width/2 + x * SCALE, canvas.height/2 - y * SCALE);
ctx.rotate(-theta); // Negative theta to match coordinate system
// Draw AGV body
ctx.fillStyle = color;
ctx.fillRect(-AGV_SIZE/2, -AGV_SIZE/2, AGV_SIZE, AGV_SIZE);
// Draw direction indicator
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(AGV_SIZE/2, 0);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
// Draw AGV name
ctx.save();
ctx.translate(canvas.width/2 + x * SCALE, canvas.height/2 - y * SCALE - AGV_SIZE);
ctx.fillStyle = color;
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(id, 0, 0);
ctx.restore();
}
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw grid
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
// Grid interval for labels (show every 20 units)
const LABEL_INTERVAL = 20;
// Vertical lines
for (let x = -GRID_SIZE/2; x <= GRID_SIZE/2; x++) {
const drawX = canvas.width/2 + x * SCALE;
ctx.beginPath();
ctx.moveTo(drawX, 0);
ctx.lineTo(drawX, canvas.height);
ctx.stroke();
// Add X axis labels (every LABEL_INTERVAL units)
if (x % LABEL_INTERVAL === 0) {
ctx.fillStyle = '#666';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(x.toString(), drawX, canvas.height/2 + 20);
}
}
// Horizontal lines
for (let y = -GRID_SIZE/2; y <= GRID_SIZE/2; y++) {
const drawY = canvas.height/2 + y * SCALE;
ctx.beginPath();
ctx.moveTo(0, drawY);
ctx.lineTo(canvas.width, drawY);
ctx.stroke();
// Add Y axis labels (every LABEL_INTERVAL units)
if (y % LABEL_INTERVAL === 0) {
ctx.fillStyle = '#666';
ctx.font = '12px Arial';
ctx.textAlign = 'right';
ctx.fillText((-y).toString(), canvas.width/2 - 10, drawY + 4);
}
}
// Draw axis lines
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
// X axis
ctx.beginPath();
ctx.moveTo(0, canvas.height/2);
ctx.lineTo(canvas.width, canvas.height/2);
ctx.stroke();
// Y axis
ctx.beginPath();
ctx.moveTo(canvas.width/2, 0);
ctx.lineTo(canvas.width/2, canvas.height);
ctx.stroke();
// Add axes labels
ctx.fillStyle = '#444';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.fillText('X', canvas.width - 15, canvas.height/2 - 10);
ctx.fillText('Y', canvas.width/2 + 15, 15);
}
function updatePositions() {
fetch('/positions')
.then(response => response.json())
.then(positions => {
clearCanvas();
positions.forEach(pos => {
const color = getColor(pos.id);
drawAGV(
pos.position.x,
pos.position.y,
pos.position.theta,
pos.id,
color
);
});
})
.catch(error => console.error('Error fetching positions:', error));
}
function startUpdates() {
if (!updateInterval) {
updateInterval = setInterval(updatePositions, 1000);
}
}
function stopUpdates() {
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = null;
}
}
// Start updates automatically
startUpdates();
</script>
</body>
</html>
`);
});
// API endpoint to get current positions
app.get('/positions', (c: Context) => {
// Convert Map to array of objects with id and position
const positions = Array.from(agvPositions.entries()).map(([id, pos]) => ({
id,
position: pos
}));
return c.json(positions);
});
// Handle messages from main thread
self.onmessage = (event) => {
const message = event.data;
if (message.type === 'positionUpdate') {
const { agvId, position } = message.data;
// console.log("agvId", agvId, "position", position);
agvPositions.set(`${agvId.manufacturer}/${agvId.serialNumber}`, position);
} else if (message.type === 'shutdown') {
stopServer();
}
};
// Start the server
export async function startServer(port: number = 3001) {
if (isRunning) {
console.log("Web服务器已在运行中");
return false;
}
try {
server = Deno.serve({ port }, app.fetch);
isRunning = true;
console.log(`Web服务器已启动监听端口 ${port}`);
return true;
} catch (error) {
console.error(`服务器启动失败: ${error}`);
return false;
}
}
// Stop the server
export async function stopServer() {
if (!isRunning || !server) {
console.log("Web服务器未在运行");
return false;
}
try {
await server.shutdown();
isRunning = false;
server = null;
console.log('Web服务器已关闭');
return true;
} catch (error) {
console.error(`服务器关闭失败: ${error}`);
return false;
}
}
// Start the server when the worker is initialized
startServer();

139
worker_event_helper.ts Normal file
View File

@ -0,0 +1,139 @@
// worker_event_helper.ts
// Worker 端的全局事件助手
export interface GlobalEvent {
type: string;
source: string;
target?: string;
data?: any;
timestamp?: number;
}
export class WorkerEventHelper {
private workerName: string;
private listeners: Map<string, Map<string, Function>> = new Map();
constructor(workerName: string) {
this.workerName = workerName;
this.setupMessageHandler();
console.log(`🔧 ${workerName} 事件助手已初始化`);
}
/**
*
*/
private setupMessageHandler() {
self.addEventListener("message", (event: MessageEvent) => {
const message = event.data;
if (message.type === "globalEvent") {
this.handleGlobalEvent(message.event);
} else if (message.type === "globalEventReceived") {
this.handleEventReceived(message.eventType, message.listenerId, message.event);
}
});
}
/**
*
*/
private handleGlobalEvent(event: GlobalEvent) {
console.log(`📨 ${this.workerName} 收到全局事件: ${event.type} (来源: ${event.source})`);
// 触发本地监听器
const eventListeners = this.listeners.get(event.type);
if (eventListeners) {
eventListeners.forEach((callback) => {
try {
callback(event);
} catch (error) {
console.error(`${this.workerName} 事件处理器出错:`, error);
}
});
}
}
/**
*
*/
private handleEventReceived(eventType: string, listenerId: string, event: GlobalEvent) {
const eventListeners = this.listeners.get(eventType);
if (eventListeners) {
const callback = eventListeners.get(listenerId);
if (callback) {
try {
callback(event);
} catch (error) {
console.error(`${this.workerName} 事件监听器出错:`, error);
}
}
}
}
/**
*
*/
dispatchEvent(type: string, data?: any, target?: string) {
const event: GlobalEvent = {
type,
source: this.workerName,
target,
data,
timestamp: Date.now()
};
console.log(`📤 ${this.workerName} 分发事件: ${type}${target ? ` -> ${target}` : ' (广播)'}`);
self.postMessage({
type: "dispatchGlobalEvent",
event
});
}
/**
*
*/
addEventListener(eventType: string, callback: (event: GlobalEvent) => void): string {
const listenerId = `${this.workerName}_${eventType}_${Date.now()}_${Math.random()}`;
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, new Map());
}
this.listeners.get(eventType)!.set(listenerId, callback);
// 注册到主线程的事件管理器
self.postMessage({
type: "addEventListener",
eventType,
listenerId
});
console.log(`👂 ${this.workerName} 添加事件监听器: ${eventType}`);
return listenerId;
}
/**
*
*/
removeEventListener(eventType: string, listenerId: string) {
const eventListeners = this.listeners.get(eventType);
if (eventListeners) {
eventListeners.delete(listenerId);
if (eventListeners.size === 0) {
this.listeners.delete(eventType);
}
}
}
/**
* Worker
*/
getWorkerName(): string {
return this.workerName;
}
}
// 创建便捷的全局函数
export function createWorkerEventHelper(workerName: string): WorkerEventHelper {
return new WorkerEventHelper(workerName);
}