From 37e997d4c6e74ce748ac4b332160aee928b15012 Mon Sep 17 00:00:00 2001 From: Tony Cao Date: Wed, 4 Jun 2025 19:15:02 +0800 Subject: [PATCH] vda5050 gmr api server project init --- .gitignore | 8 + agv_main.ts | 212 +++++ agv_manager.ts | 104 +++ agv_worker.ts | 932 ++++++++++++++++++++++ cli.ts | 188 +++++ config.json | 12 + debug_logger.ts | 283 +++++++ deno.json | 53 ++ device_manager.ts | 279 +++++++ device_protocol_config.ts | 190 +++++ device_simulator.ts | 1374 +++++++++++++++++++++++++++++++++ device_simulator_main.ts | 538 +++++++++++++ devices.json | 111 +++ event_manager.ts | 139 ++++ generate_devices.ts | 12 + import_map.json | 12 + mapping.json | 144 ++++ master_manager.ts | 309 ++++++++ modbus_manager.ts | 240 ++++++ proto/vda5050.proto | 519 +++++++++++++ simulator.ts | 796 +++++++++++++++++++ simulator_config.json | 23 + simulator_config.ts | 127 +++ simulator_main.ts | 1303 +++++++++++++++++++++++++++++++ test_events.ts | 72 ++ test_worker.ts | 55 ++ vda5050_transformer_worker.ts | 428 ++++++++++ vda_worker.ts | 837 ++++++++++++++++++++ web_worker.ts | 289 +++++++ worker_event_helper.ts | 139 ++++ 30 files changed, 9728 insertions(+) create mode 100644 .gitignore create mode 100644 agv_main.ts create mode 100644 agv_manager.ts create mode 100644 agv_worker.ts create mode 100644 cli.ts create mode 100644 config.json create mode 100644 debug_logger.ts create mode 100644 deno.json create mode 100644 device_manager.ts create mode 100644 device_protocol_config.ts create mode 100644 device_simulator.ts create mode 100644 device_simulator_main.ts create mode 100644 devices.json create mode 100644 event_manager.ts create mode 100644 generate_devices.ts create mode 100644 import_map.json create mode 100644 mapping.json create mode 100644 master_manager.ts create mode 100644 modbus_manager.ts create mode 100644 proto/vda5050.proto create mode 100644 simulator.ts create mode 100644 simulator_config.json create mode 100644 simulator_config.ts create mode 100644 simulator_main.ts create mode 100644 test_events.ts create mode 100644 test_worker.ts create mode 100644 vda5050_transformer_worker.ts create mode 100644 vda_worker.ts create mode 100644 web_worker.ts create mode 100644 worker_event_helper.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d36ce7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +deno.lock +*.exe +agv_api +simulator +release +device_registry.json +debug.log diff --git a/agv_main.ts b/agv_main.ts new file mode 100644 index 0000000..eabb0ff --- /dev/null +++ b/agv_main.ts @@ -0,0 +1,212 @@ +// main.ts + +/// + +// 导入 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+C(SIGINT)退出 +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 退出程序"); \ No newline at end of file diff --git a/agv_manager.ts b/agv_manager.ts new file mode 100644 index 0000000..07bd177 --- /dev/null +++ b/agv_manager.ts @@ -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; +} \ No newline at end of file diff --git a/agv_worker.ts b/agv_worker.ts new file mode 100644 index 0000000..fea9cf5 --- /dev/null +++ b/agv_worker.ts @@ -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 | null = null; +const agvMap = new Map(); + +// 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 = { + 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); + } + } + } +}); \ No newline at end of file diff --git a/cli.ts b/cli.ts new file mode 100644 index 0000000..ee89329 --- /dev/null +++ b/cli.ts @@ -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 "); + } + 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 "); + } + 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 "); + } + break; + + case "instant-remove": + if (args.length >= 3) { + // 仅从运行时移除,不修改配置文件 + await removeDeviceFromWorker(masterWorker, args[1], args[2]); + console.log("✅ 设备已从运行时移除(配置文件保持不变)"); + } else { + console.log("用法: device instant-remove "); + } + 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 - 添加新设备 + device remove - 删除设备(配置文件+重新加载) + device force-remove - 强制删除设备(配置文件+运行时) + device instant-remove - 即时删除设备(仅运行时) + device list - 列出所有设备 + device status - 查询运行时设备状态 + device reload - 重新加载设备配置 + device generate [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 - 取消指定的订单"); + console.log(" discover - 请求当前所有发现的设备信息"); + console.log(" setcmd - 发送自定义命令设置到 Worker"); + console.log(" requestKV - 请求从 KV 中获取设备数据,并发送给 agvWorker"); + console.log(" device add - 动态添加设备"); + console.log(" device remove - 删除设备(配置+重载)"); + console.log(" device force-remove - 强制删除设备"); + console.log(" device instant-remove - 即时删除设备"); + console.log(" device list - 列出所有配置的设备"); + console.log(" device reload - 重新加载设备配置"); + console.log(" device generate [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 , discover, setcmd , requestKV, device , exit"); + break; + } + } + } + })(); +} \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..c5a3265 --- /dev/null +++ b/config.json @@ -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" +} \ No newline at end of file diff --git a/debug_logger.ts b/debug_logger.ts new file mode 100644 index 0000000..1f4ec93 --- /dev/null +++ b/debug_logger.ts @@ -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 = {}) { + 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 = 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(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(label: string, fn: () => Promise): Promise { + 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]}`); + } +} + +// 调试工具已在上面导出 \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..40a9813 --- /dev/null +++ b/deno.json @@ -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"] + } + } +} \ No newline at end of file diff --git a/device_manager.ts b/device_manager.ts new file mode 100644 index 0000000..542d7a6 --- /dev/null +++ b/device_manager.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + this.devices = []; + await this.saveDevices(); + console.log("🗑️ 已清空所有设备"); + } + + /** + * 生成测试设备 + */ + async generateTestDevices(count: number, prefix = "TEST"): Promise { + 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 { + const manager = new DeviceManager(); + await manager.loadDevices(); + return await manager.addDevice(manufacturer, serialNumber); +} + +export async function removeDeviceFromConfig(manufacturer: string, serialNumber: string): Promise { + const manager = new DeviceManager(); + await manager.loadDevices(); + return await manager.removeDevice(manufacturer, serialNumber); +} + +export async function listConfigDevices(): Promise { + 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 "); + } + break; + + case "remove": + if (args.length >= 3) { + await manager.removeDevice(args[1], args[2]); + } else { + console.log("用法: deno run device_manager.ts remove "); + } + 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 "); + } + 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 [args] + +命令: + add - 添加设备 + remove - 删除设备 + force-remove - 强制删除设备 + list - 列出所有设备 + clear - 清空所有设备 + generate [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 + `); + } +} \ No newline at end of file diff --git a/device_protocol_config.ts b/device_protocol_config.ts new file mode 100644 index 0000000..c32a9f9 --- /dev/null +++ b/device_protocol_config.ts @@ -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 = { + "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 +}; \ No newline at end of file diff --git a/device_simulator.ts b/device_simulator.ts new file mode 100644 index 0000000..9404d3a --- /dev/null +++ b/device_simulator.ts @@ -0,0 +1,1374 @@ +// device_simulator.ts - 设备模拟器Worker +import { connect, MqttClient } from "npm:mqtt@5.10.1"; +import { ModbusManager } from "./modbus_manager.ts"; +import { getDefaultPollInterval, parseRegistersFromString, createModbusPollConfigFromRegisters } from "./device_protocol_config.ts"; +import { createModuleLogger, perfMonitor, logMemoryUsage } from "./debug_logger.ts"; + +interface DeviceInfo { + ip: string; + port: string; + slaveId: string; + deviceName: string; + protocolType: string; + brandName: string; + registers: string; +} + +interface DeviceWorkerMessage { + type: "init" | "close" | "reconnect" | "action"; + data?: any; +} + +interface DeviceMainMessage { + type: "ready" | "error" | "status" | "log" | "reset"; + data?: any; +} + +interface DeviceConfig { + deviceId: string; + deviceInfo: DeviceInfo; + vdaInterface: string; + mqtt: { + brokerUrl: string; + options: any; + }; + vehicle: { + manufacturer: string; + serialNumber: string; + vdaVersion: string; + }; + // 设备创建时的动作参数 + actionParameters?: Array<{ + key: string; + value: string; + }>; + modbus?: { + host: string; + port: number; + unitId: number; + poll: Array<{ + fn: "readHoldingRegisters" | "readInputRegisters" | "readCoils" | "readDiscreteInputs"; + start: number; + len: number; + mqttKey: string; + bind?: string; + }>; + pollInterval: number; + }; +} + +interface Position { + x: number; + y: number; + theta: number; + mapId: string; + mapDescription?: string; +} + +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; + localizationScore: number; + deviationRange: number; +} + +interface Velocity { + vx: number; + vy: number; + omega: number; +} + +interface BatteryState { + batteryCharge: number; + batteryVoltage: number; + batteryHealth: number; + charging: boolean; + reach: number; +} + +interface ErrorState { + errorType: string; + errorLevel: "WARNING" | "FATAL"; + errorDescription: string; + errorHint?: string; + errorReferences?: Array<{ referenceKey: string; referenceValue: string }>; +} + +interface SafetyState { + eStop: string; + fieldViolation: boolean; +} + + + +interface ActionState { + actionType: string; + actionId: string; + actionDescription?: string; + actionStatus: "WAITING" | "INITIALIZING" | "RUNNING" | "PAUSED" | "FINISHED" | "FAILED"; + resultDescription?: string; + actionParameters?: Array<{ key: string; value: string }>; + blockingType: "NONE" | "SOFT" | "HARD"; +} + +interface AgvState { + headerId: number; + timestamp: string; + version: string; + manufacturer: string; + serialNumber: string; + orderId: string; + orderUpdateId: number; + zoneSetId: string; + lastNodeId: string; + lastNodeSequenceId: number; + driving: boolean; + paused: boolean; + newBaseRequest: boolean; + distanceSinceLastNode: number; + operatingMode: "AUTOMATIC" | "SEMIAUTOMATIC" | "MANUAL" | "SERVICE" | "TEACHIN"; + nodeStates: NodeState[]; + edgeStates: EdgeState[]; + agvPosition: AgvPosition; + velocity: Velocity; + loads: any[]; + batteryState: BatteryState; + errors: ErrorState[]; + information: any[]; + safetyState: SafetyState; + actionStates: ActionState[]; + waitingForInteractionZoneRelease: boolean; + forkState?: any; +} + +interface Connection { + headerId: number; + timestamp: string; + version: string; + manufacturer: string; + serialNumber: string; + connectionState: "ONLINE" | "OFFLINE" | "CONNECTIONBROKEN"; +} + +class DeviceSimulator { + private client!: MqttClient; + private config!: DeviceConfig; + private state!: AgvState; + private connection!: Connection; + private stateInterval?: number; + private connectionInterval?: number; + private headerId = 1; + private isConnected = false; + private deviceActions: ActionState[] = []; + private modbus!: ModbusManager; + private modbusPollTimer?: number; + private logger = createModuleLogger("DEVICE_SIMULATOR"); + private enableLog = false; + private log(message: string) { + if(this.enableLog){ + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] [Device ${this.config.deviceId}] ${message}`; + console.log(logMessage); + + // 发送日志到主进程 + self.postMessage({ + type: "log", + data: { message: logMessage } + } as DeviceMainMessage); + } + + } + + private sendMessage(type: string, data?: any) { + self.postMessage({ type, data } as DeviceMainMessage); + } + + private getFunctionCodeDescription(fnCode: string): string { + const descriptions: Record = { + "1": "Read Coils (读线圈)", + "2": "Read Discrete Inputs (读离散输入)", + "3": "Read Holding Registers (读保持寄存器)", + "4": "Read Input Registers (读输入寄存器)", + "5": "Write Single Coil (写单个线圈)", + "6": "Write Single Register (写单个寄存器)", + "15": "Write Multiple Coils (写多个线圈)", + "16": "Write Multiple Registers (写多个寄存器)" + }; + return descriptions[fnCode] || `Unknown Function Code (未知功能码 ${fnCode})`; + } + + private getFunctionCodeFromModbusFunction(modbusFunction: string): string { + const functionMap: Record = { + "readCoils": "1", + "readDiscreteInputs": "2", + "readHoldingRegisters": "3", + "readInputRegisters": "4" + }; + return functionMap[modbusFunction] || "3"; // 默认为读保持寄存器 + } + + private updateRegisterInformation(registerName: string, bind: string, values: number[], address: number, count: number, modbusFunction: string, timestamp: string): void { + // 创建符合VDA 5050标准的信息项,将寄存器信息放到infoReferences中 + const infoType = `CUSTOM_REGISTER`; + const infoDescription = `REGISTER_${registerName.toUpperCase()}`; + const informationItem = { + infoType: infoType, + infoDescription: infoDescription, + infoLevel: "INFO", + infoReferences: [ + { referenceKey: "name", referenceValue: registerName }, + { referenceKey: "bind", referenceValue: bind }, + { referenceKey: "values", referenceValue: JSON.stringify(values) }, + { referenceKey: "address", referenceValue: address.toString() }, + { referenceKey: "count", referenceValue: count.toString() }, + { referenceKey: "function", referenceValue: modbusFunction }, + { referenceKey: "timestamp", referenceValue: timestamp } + ] + }; + + // 查找是否已存在相同寄存器的信息项 + const existingIndex = this.state.information.findIndex(info => + info.infoDescription === infoDescription + ); + + if (existingIndex >= 0) { + // 更新现有信息项 + this.state.information[existingIndex] = informationItem; + this.logger.trace("🔄 Updated register information", { + registerName, + infoType, + address, + newValues: values, + timestamp + }); + } else { + // 添加新信息项 + this.state.information.push(informationItem); + this.logger.trace("➕ Added new register information", { + registerName, + infoType, + address, + values, + totalInformation: this.state.information.length + }); + } + + // 可选:限制information数组的大小,避免无限增长 + const maxInformation = 100; // 最多保留100个信息项 + if (this.state.information.length > maxInformation) { + // 删除最旧的信息项(按时间戳排序) + this.state.information.sort((a, b) => { + const timeA = new Date(a.timeOfOccurrence).getTime(); + const timeB = new Date(b.timeOfOccurrence).getTime(); + return timeA - timeB; + }); + + const removedItems = this.state.information.splice(0, this.state.information.length - maxInformation); + this.logger.debug("🗑️ Removed old information items", { + removedCount: removedItems.length, + remainingCount: this.state.information.length + }); + } + } + + async initialize(config: DeviceConfig) { + this.config = config; + this.logger.info("🚀 Initializing device simulator", { + deviceId: config.deviceId, + deviceName: config.deviceInfo?.deviceName, + protocol: config.deviceInfo?.protocolType + }); + // console.log(config); + logMemoryUsage("DEVICE_SIMULATOR"); + + try { + await perfMonitor.measureAsync("device_initialization", async () => { + await this.connectMqtt(); + await this.setupModbus(); + this.initializeState(); + this.initializeConnection(); + this.startPeriodicReporting(); + }); + + this.logger.info("✅ Device simulator initialized successfully"); + this.log("Device simulator initialized successfully"); + this.sendMessage("ready"); + } catch (error) { + this.logger.error("❌ Failed to initialize device simulator", error); + this.log(`Failed to initialize: ${(error as Error).message}`); + + // 如果初始化失败,请求重启worker + if ((error as Error).message.includes("MQTT") || (error as Error).message.includes("timeout")) { + this.log(`❌ Initialization failed due to MQTT issue, requesting worker reset`); + this.sendMessage("reset", { reason: "init_failure", error: (error as Error).message }); + } else { + this.sendMessage("error", { error: (error as Error).message }); + } + } + } + + private async connectMqtt() { + this.log(`Connecting to MQTT broker: ${this.config.mqtt.brokerUrl}`); + + this.client = connect(this.config.mqtt.brokerUrl, this.config.mqtt.options); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const error = new Error("MQTT connection timeout"); + this.log(`❌ MQTT connection timeout, requesting worker reset`); + this.sendMessage("reset", { reason: "mqtt_timeout", error: error.message }); + reject(error); + }, 10000); + + this.client.on("connect", () => { + clearTimeout(timeout); + this.isConnected = true; + this.log("Connected to MQTT broker"); + this.setupMqttHandlers(); + resolve(); + }); + + this.client.on("error", (error: Error) => { + clearTimeout(timeout); + this.log(`❌ MQTT connection error: ${error.message}, requesting worker reset`); + this.sendMessage("reset", { reason: "mqtt_error", error: error.message }); + reject(error); + }); + }); + } + + private async setupModbus() { + this.logger.info("🔧 Setting up Modbus connection..."); + + // 从设备信息中动态创建Modbus配置 + + const host = this.config.deviceInfo.ip; + const port = parseInt(this.config.deviceInfo.port) || 502; + const unitId = parseInt(this.config.deviceInfo.slaveId) || 1; + + this.logger.debug("📋 Modbus connection parameters", { + host, + port, + unitId, + deviceName: this.config.deviceInfo.deviceName, + brandName: this.config.deviceInfo.brandName, + protocolType: this.config.deviceInfo.protocolType + }); + + this.log(`Creating Modbus config from device info: ${host}:${port}, unitId: ${unitId}`); + this.log(`Device: ${this.config.deviceInfo.deviceName} (${this.config.deviceInfo.brandName})`); + this.log(`Protocol: ${this.config.deviceInfo.protocolType}`); + + // 获取设备品牌特定的轮询间隔 + const pollInterval = getDefaultPollInterval(this.config.deviceInfo.brandName); + this.logger.debug("⏱️ Poll interval configuration", { + brandName: this.config.deviceInfo.brandName, + pollInterval: `${pollInterval}ms` + }); + this.log(`Using poll interval for ${this.config.deviceInfo.brandName || "通用"}: ${pollInterval}ms`); + + let pollConfig: Array<{ + fn: "readHoldingRegisters" | "readInputRegisters" | "readCoils" | "readDiscreteInputs"; + start: number; + len: number; + mqttKey: string; + bind?: string; + }> = []; + + // 检查是否有寄存器配置 + this.logger.trace("🔍 Checking for registers configuration in device info..."); + + // 首先检查 deviceInfo.registers + let registersConfigString: string | undefined; + + if (this.config.deviceInfo.registers) { + registersConfigString = this.config.deviceInfo.registers; + this.logger.info("✅ Found registers configuration in deviceInfo.registers"); + this.logger.trace("📝 Raw registers config from deviceInfo", { config: registersConfigString }); + this.log("✅ Found registers configuration in deviceInfo.registers"); + } else { + // 如果 deviceInfo.registers 不存在,则检查 actionParameters + const registersParam = this.config.actionParameters?.find(param => param.key === "registers"); + if (registersParam && registersParam.value) { + registersConfigString = registersParam.value; + this.logger.info("✅ Found registers configuration in action parameters"); + this.logger.trace("📝 Raw registers config from actionParameters", { config: registersConfigString }); + this.log("✅ Found registers configuration in action parameters"); + } + } + + if (registersConfigString) { + this.log(`📝 Raw registers config: ${registersConfigString}`); + + // 验证JSON格式 + try { + const parsedConfig = JSON.parse(registersConfigString); + this.logger.debug("✅ Registers configuration JSON validation passed", { + registersCount: Array.isArray(parsedConfig) ? parsedConfig.length : 'not_array', + structure: parsedConfig + }); + this.log("✅ Registers configuration is valid JSON"); + } catch (error) { + this.logger.error("❌ Invalid JSON in registers configuration", { + error: (error as Error).message, + rawConfig: registersConfigString + }); + this.log(`❌ Invalid JSON in registers configuration: ${(error as Error).message}`); + pollConfig = []; + return; + } + + // 解析寄存器配置 + const registers = perfMonitor.measure("register_parsing", () => + parseRegistersFromString(registersConfigString!) + ); + + this.logger.info("📊 Register parsing completed", { + totalRegisters: registers.length, + registers: registers.map(reg => ({ + name: reg.name, + fnCode: reg.fnCode, + address: reg.regAddress, + count: reg.regCount + })) + }); + + this.log(`📊 Parsed ${registers.length} register definitions:`); + + if (registers.length === 0) { + this.logger.warn("⚠️ No valid register definitions found after parsing"); + this.log("⚠️ No valid register definitions found after parsing"); + } + + registers.forEach((reg, index) => { + this.logger.trace(`Register definition [${index + 1}]`, { + name: reg.name, + fnCode: reg.fnCode, + fnDescription: this.getFunctionCodeDescription(reg.fnCode), + address: reg.regAddress, + count: reg.regCount + }); + + this.log(` [${index + 1}] ${reg.name}:`); + this.log(` - Function Code: ${reg.fnCode} (${this.getFunctionCodeDescription(reg.fnCode)})`); + this.log(` - Address: ${reg.regAddress}`); + this.log(` - Count: ${reg.regCount}`); + }); + + // 使用解析的寄存器创建轮询配置 + pollConfig = perfMonitor.measure("poll_config_creation", () => + createModbusPollConfigFromRegisters(registers) + ); + + this.logger.info("🔄 Polling configuration created", { + totalTasks: pollConfig.length, + tasks: pollConfig.map(config => ({ + function: config.fn, + startAddress: config.start, + length: config.len, + mqttKey: config.mqttKey + })) + }); + + this.log(`🔄 Created ${pollConfig.length} polling tasks from register definitions:`); + + pollConfig.forEach((config, index) => { + this.log(` [${index + 1}] ${config.fn}:`); + this.log(` - Start Address: ${config.start}`); + this.log(` - Length: ${config.len}`); + this.log(` - MQTT Key: ${config.mqttKey}`); + }); + + } else { + this.logger.warn("❌ No registers configuration found"); + this.log("❌ No registers configuration found"); + this.log("💡 Expected registers configuration in deviceInfo.registers or actionParameters"); + this.log("📝 Example: [{\"fnCode\":\"6\",\"name\":\"button1\",\"regCount\":\"1\",\"regAddress\":\"1\"}]"); + pollConfig = []; + } + + // 动态创建Modbus配置 + this.config.modbus = { + host: host, + port: port, + unitId: unitId, + poll: pollConfig, + pollInterval: pollInterval + }; + + this.modbus = new ModbusManager( + this.config.modbus.host, + this.config.modbus.port, + this.config.modbus.unitId, + (msg) => this.log(`[Modbus] ${msg}`) + ); + + try { + await this.modbus.init(); + } catch (error) { + const errorMessage = (error as Error).message; + this.log(`❌ Modbus initialization failed: ${errorMessage}`); + // Report initialization error without auto-reset + this.sendMessage("error", { error: `Modbus initialization error: ${errorMessage}` }); + throw error; + } + + // 只有在有轮询配置时才启动循环读取任务 + if (this.config.modbus.poll.length > 0) { + this.modbusPollTimer = setInterval( + () => { void this.pollModbus(); }, + this.config.modbus.pollInterval + ); + this.log(`Modbus setup completed - polling ${this.config.modbus.poll.length} register groups every ${this.config.modbus.pollInterval}ms`); + } else { + this.log(`Modbus setup completed - no polling configured, only write operations will be available`); + } + } + + private async pollModbus() { + try { + // 确保Modbus配置存在 + if (!this.config.modbus) { + this.logger.error("❌ Modbus configuration not available for polling"); + this.log("❌ Modbus configuration not available for polling"); + return; + } + + const pollStartTime = performance.now(); + this.logger.debug("🔄 Starting Modbus polling cycle", { + totalGroups: this.config.modbus.poll.length, + pollInterval: this.config.modbus.pollInterval + }); + + this.log(`🔄 Starting Modbus polling cycle - ${this.config.modbus.poll.length} register groups to read`); + + for (const [index, item] of this.config.modbus.poll.entries()) { + const itemStartTime = performance.now(); + + try { + this.logger.trace(`📖 Reading register group [${index + 1}/${this.config.modbus.poll.length}]`, { + function: item.fn, + startAddress: item.start, + length: item.len, + mqttKey: item.mqttKey + }); + + this.log(`📖 [${index + 1}/${this.config.modbus.poll.length}] Reading ${item.fn} - Address: ${item.start}, Length: ${item.len}`); + + const values = await perfMonitor.measureAsync(`modbus_read_${item.mqttKey}`, async () => + this.modbus.enqueueRead(item.fn, item.start, item.len) + ); + + const readDuration = performance.now() - itemStartTime; + this.logger.debug("✅ Modbus read successful", { + mqttKey: item.mqttKey, + values, + duration: `${readDuration.toFixed(2)}ms`, + valuesCount: Array.isArray(values) ? values.length : 1 + }); + + this.log(`✅ Read successful: ${JSON.stringify(values)}`); + + // 将寄存器数据存储到information数组中 + const registerName = item.mqttKey.split('/').pop() || `register_${item.start}`; + const bind = item.bind || registerName; + const registerValues = Array.isArray(values) ? values : [values]; + const timestamp = new Date().toISOString(); + + // 更新或添加到information数组 + this.updateRegisterInformation(registerName, bind, registerValues, item.start, item.len, item.fn, timestamp); + + this.logger.trace("📦 Register data stored in information", { + registerName, + address: item.start, + count: item.len, + values: registerValues, + function: item.fn, + totalInformation: this.state.information.length + }); + + this.log(`📦 Stored register data in information: ${registerName} = ${JSON.stringify(registerValues)}`); + } catch (error) { + const readDuration = performance.now() - itemStartTime; + const errorMessage = (error as Error).message; + + this.logger.error("❌ Modbus read error", { + mqttKey: item.mqttKey, + function: item.fn, + startAddress: item.start, + length: item.len, + error: errorMessage, + duration: `${readDuration.toFixed(2)}ms` + }); + + this.log(`❌ Modbus read error for ${item.mqttKey}: ${errorMessage}`); + this.log(`🔧 Failed operation: ${item.fn} at address ${item.start}, length ${item.len}`); + + // 检查是否为连接失效错误,如果是则请求重置worker + if (this.isConnectionError(errorMessage)) { + this.log(`❌ Modbus connection failure detected, requesting worker reset`); + this.sendMessage("reset", { + reason: "modbus_connection_failure", + error: `Modbus connection error: ${errorMessage}` + }); + return; // 停止当前轮询周期 + } + } + } + + const totalDuration = performance.now() - pollStartTime; + this.logger.debug("🏁 Modbus polling cycle completed", { + totalDuration: `${totalDuration.toFixed(2)}ms`, + averagePerGroup: `${(totalDuration / this.config.modbus.poll.length).toFixed(2)}ms`, + groupsProcessed: this.config.modbus.poll.length + }); + + this.log(`🏁 Modbus polling cycle completed`); + + // 记录内存使用情况(每10次轮询记录一次) + if (Math.random() < 0.1) { + logMemoryUsage("DEVICE_SIMULATOR"); + } + } catch (error) { + // 捕获整个轮询周期的未预期错误 + const errorMessage = (error as Error).message; + this.logger.error("❌ Unexpected error in Modbus polling cycle", { + error: errorMessage, + stack: (error as Error).stack + }); + + this.log(`❌ Unexpected error in Modbus polling cycle: ${errorMessage}`); + + // 检查是否为连接相关错误 + if (this.isConnectionError(errorMessage)) { + this.log(`❌ Polling cycle connection failure detected, requesting worker reset`); + this.sendMessage("reset", { + reason: "modbus_polling_failure", + error: `Modbus polling error: ${errorMessage}` + }); + } + } + } + + // 检查是否为连接相关错误的辅助方法 + private isConnectionError(errorMessage: string): boolean { + const connectionErrors = [ + "no connection", + "connection lost", + "connection closed", + "connection refused", + "timeout", + "timed out", + "ECONNRESET", + "ENOTCONN", + "ECONNREFUSED", + "ETIMEDOUT", + "EPIPE", + "ENETUNREACH", + "EHOSTUNREACH" + ]; + + return connectionErrors.some(keyword => + errorMessage.toLowerCase().includes(keyword.toLowerCase()) + ); + } + + private setupMqttHandlers() { + // 订阅instantActions主题 + const instantActionsTopic = `${this.config.vdaInterface}/${this.config.vehicle.vdaVersion}/${this.config.vehicle.manufacturer}/${this.config.vehicle.serialNumber}/instantActions`; + this.client.subscribe(instantActionsTopic, (err: Error | null) => { + if (err) { + this.log(`Failed to subscribe to ${instantActionsTopic}: ${err.message}`); + } else { + this.log(`Subscribed to ${instantActionsTopic}`); + } + }); + + this.client.on("message", (topic: string, message: Uint8Array) => { + try { + const data = JSON.parse(new TextDecoder().decode(message)); + this.log(`📥 Received message on ${topic}`); + + if (topic.endsWith("/instantActions")) { + this.handleInstantActions(data); + } + } catch (error) { + this.log(`Error parsing message: ${(error as Error).message}`); + } + }); + + this.client.on("disconnect", () => { + this.isConnected = false; + this.log("❌ Disconnected from MQTT broker, requesting worker reset"); + this.sendMessage("reset", { reason: "mqtt_disconnect", error: "MQTT broker disconnected" }); + }); + + this.client.on("error", (error: Error) => { + this.log(`❌ MQTT runtime error: ${error.message}, requesting worker reset`); + this.sendMessage("reset", { reason: "mqtt_runtime_error", error: error.message }); + }); + } + + private initializeState() { + const now = new Date().toISOString(); + + this.state = { + headerId: this.headerId++, + timestamp: now, + version: this.config.vehicle.vdaVersion, + manufacturer: this.config.vehicle.manufacturer, + serialNumber: this.config.vehicle.serialNumber, + orderId: "order_0", + orderUpdateId: 0, + zoneSetId: "zone_0", + lastNodeId: "node_0", + lastNodeSequenceId: 0, + driving: false, + paused: false, + newBaseRequest: false, + distanceSinceLastNode: 0, + operatingMode: "AUTOMATIC", + nodeStates: [], + edgeStates: [], + agvPosition: { + x: 0, + y: 0, + theta: 0, + mapId: "map_1", + mapDescription: "Device Map", + positionInitialized: true, + localizationScore: 1.0, + deviationRange: 0.1, + }, + velocity: { + vx: 0, + vy: 0, + omega: 0, + }, + loads: [], + batteryState: { + batteryCharge: 85.5, + batteryVoltage: 24.2, + batteryHealth: 95.0, + charging: false, + reach: 8500, + }, + errors: [], + information: [], + safetyState: { + eStop: "NONE", + fieldViolation: false, + }, + actionStates: [], + waitingForInteractionZoneRelease: false, + }; + } + + private initializeConnection() { + const now = new Date().toISOString(); + + this.connection = { + headerId: this.headerId++, + timestamp: now, + version: this.config.vehicle.vdaVersion, + manufacturer: this.config.vehicle.manufacturer, + serialNumber: this.config.vehicle.serialNumber, + connectionState: "ONLINE", + }; + } + + private startPeriodicReporting() { + // 每2秒发送状态 + this.stateInterval = setInterval(() => { + this.publishState(); + }, 2000); + + // 每1秒发送连接状态 + this.connectionInterval = setInterval(() => { + this.publishConnection(); + }, 1000); + + // 立即发送一次 + this.publishState(); + this.publishConnection(); + } + + private publishState() { + if (!this.isConnected) return; + + this.state.headerId = this.headerId++; + this.state.timestamp = new Date().toISOString(); + this.state.actionStates = [...this.deviceActions]; + + const topic = `${this.config.vdaInterface}/${this.config.vehicle.vdaVersion}/${this.config.vehicle.manufacturer}/${this.config.vehicle.serialNumber}/state`; + const message = JSON.stringify(this.state, null, 2); + + this.client.publish(topic, message, (err?: Error) => { + if (err) { + this.log(`Failed to publish state: ${err.message}`); + } else { + // this.log(`📤 Published state (headerId: ${this.state.headerId})`); + } + }); + } + + private publishConnection() { + if (!this.isConnected) return; + + this.connection.headerId = this.headerId++; + this.connection.timestamp = new Date().toISOString(); + + const topic = `${this.config.vdaInterface}/${this.config.vehicle.vdaVersion}/${this.config.vehicle.manufacturer}/${this.config.vehicle.serialNumber}/connection`; + const message = JSON.stringify(this.connection, null, 2); + + this.client.publish(topic, message, (err?: Error) => { + if (err) { + this.log(`Failed to publish connection: ${err.message}`); + } else { + // this.log(`📤 Published connection (headerId: ${this.connection.headerId})`); + } + }); + } + + private handleInstantActions(message: any) { + this.log(`Processing instantActions message`); + + try { + // 支持两种格式:标准VDA 5050的instantActions和实际使用的actions + const actions = message.instantActions || message.actions || []; + + if (!Array.isArray(actions)) { + this.log("❌ Invalid actions format - not an array"); + return; + } + + for (const action of actions) { + // 检查是否为Modbus写操作 + if (action.actionType === "deviceWrite") { + this.handleModbusWrite(action); + } else { + this.processDeviceAction(action); + } + } + } catch (error) { + this.log(`❌ Error processing instantActions: ${(error as Error).message}`); + } + } + + private async handleModbusWrite(action: any) { + this.log(`🔧 Processing Modbus write action: ${action.actionId}`); + console.log("收到 Modbus 写入动作消息", action); + + try { + // 提取设备信息 + const getParam = (key: string) => action.actionParameters?.find((p: any) => p.key === key)?.value; + + const protocolType = getParam("protocolType"); + const brandName = getParam("brandName"); + const ip = getParam("ip"); + const port = getParam("port"); + const deviceName = getParam("deviceName"); + const slaveId = getParam("slaveId"); + const registersParam = getParam("registers"); + + this.log(`📋 设备信息: ${deviceName} (${brandName}) - ${ip}:${port}, SlaveID: ${slaveId}, 协议: ${protocolType}`); + + if (!registersParam) { + this.log(`❌ Missing registers parameter in Modbus write action`); + return; + } + + // 解析寄存器数组 + let registers: any[]; + try { + registers = JSON.parse(registersParam); + if (!Array.isArray(registers)) { + throw new Error("Registers parameter is not an array"); + } + } catch (error) { + this.log(`❌ Failed to parse registers parameter: ${(error as Error).message}`); + return; + } + + this.log(`📝 解析到 ${registers.length} 个寄存器写入操作:`); + registers.forEach((reg, index) => { + this.log(` [${index + 1}] ${reg.name}: 功能码${reg.fnCode}, 地址${reg.regAddress}, 值${reg.regValue}`); + }); + + // 创建动作状态 + const actionState: ActionState = { + actionType: action.actionType, + actionId: action.actionId, + actionDescription: `Modbus write ${registers.length} registers to ${deviceName}`, + actionStatus: "RUNNING", + actionParameters: action.actionParameters || [], + blockingType: action.blockingType || "NONE", + }; + + this.deviceActions.push(actionState); + + // 执行所有寄存器写操作 + const writeResults: any[] = []; + let successCount = 0; + let failureCount = 0; + + for (const [index, register] of registers.entries()) { + try { + const address = Number(register.regAddress); + const value = Number(register.regValue); + const fnCode = register.fnCode; + const registerName = register.name; + + this.log(`🔧 [${index + 1}/${registers.length}] 写入寄存器 ${registerName}: 地址${address}, 值${value}, 功能码${fnCode}`); + + // 根据功能码确定写入类型和数据格式 + let functionType: "writeSingleRegister" | "writeMultipleRegisters"; + let payload: number | number[]; + + if (fnCode === "6") { + functionType = "writeSingleRegister"; + payload = value; // 单个寄存器写入,使用单个数值 + } else if (fnCode === "16" || fnCode === "10") { + functionType = "writeMultipleRegisters"; + payload = [value]; // 多个寄存器写入,需要数组格式 + } else { + throw new Error(`Unsupported function code: ${fnCode}`); + } + + this.log(`📡 执行 ${functionType}(地址${address}, 数据${JSON.stringify(payload)})`); + + // 执行Modbus写操作 + await this.modbus.enqueueWrite(functionType, address, payload); + + writeResults.push({ + registerName, + address, + value, + fnCode, + status: "success" + }); + successCount++; + + this.log(`✅ 寄存器 ${registerName} 写入成功`); + + } catch (error) { + const errorMsg = (error as Error).message; + this.log(`❌ 寄存器 ${register.name} 写入失败: ${errorMsg}`); + + // 检查是否为连接失效错误 + if (this.isConnectionError(errorMsg)) { + this.log(`❌ Modbus write connection failure detected, requesting worker reset`); + this.sendMessage("reset", { + reason: "modbus_write_connection_failure", + error: `Modbus write connection error: ${errorMsg}` + }); + return; // 停止当前写操作 + } + + writeResults.push({ + registerName: register.name, + address: register.regAddress, + value: register.regValue, + fnCode: register.fnCode, + status: "error", + error: errorMsg + }); + failureCount++; + } + } + + // 更新动作状态 + if (failureCount === 0) { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = `All ${successCount} registers written successfully`; + this.log(`✅ 所有 ${successCount} 个寄存器写入完成`); + } else if (successCount === 0) { + actionState.actionStatus = "FAILED"; + actionState.resultDescription = `All ${failureCount} registers failed to write`; + this.log(`❌ 所有 ${failureCount} 个寄存器写入失败`); + } else { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = `Partial success: ${successCount} succeeded, ${failureCount} failed`; + this.log(`⚠️ 部分成功: ${successCount} 个成功, ${failureCount} 个失败`); + } + + // 发布写操作结果的MQTT消息 + const topic = `${this.config.vdaInterface}/${this.config.vehicle.vdaVersion}/${this.config.vehicle.manufacturer}/${this.config.vehicle.serialNumber}/writeResult`; + const resultPayload = JSON.stringify({ + timestamp: new Date().toISOString(), + deviceId: this.config.deviceId, + actionId: action.actionId, + deviceInfo: { + deviceName, + brandName, + ip, + port, + slaveId, + protocolType + }, + writeResults, + summary: { + total: registers.length, + success: successCount, + failure: failureCount + }, + status: failureCount === 0 ? "success" : (successCount === 0 ? "error" : "partial") + }); + + this.client.publish(topic, resultPayload); + + } catch (error) { + this.log(`❌ Modbus write action failed: ${(error as Error).message}`); + + // 更新动作状态为失败 + const actionState = this.deviceActions.find(a => a.actionId === action.actionId); + if (actionState) { + actionState.actionStatus = "FAILED"; + actionState.resultDescription = `Modbus write action failed: ${(error as Error).message}`; + } + + // 发布写操作失败的MQTT消息 + const topic = `${this.config.vdaInterface}/${this.config.vehicle.vdaVersion}/${this.config.vehicle.manufacturer}/${this.config.vehicle.serialNumber}/writeResult`; + const resultPayload = JSON.stringify({ + timestamp: new Date().toISOString(), + deviceId: this.config.deviceId, + actionId: action.actionId, + status: "error", + error: (error as Error).message + }); + + this.client.publish(topic, resultPayload); + } + } + + private processDeviceAction(action: any) { + this.log(`🔧 Processing device action: ${action.actionType}`); + + // 创建动作状态 + const actionState: ActionState = { + actionType: action.actionType, + actionId: action.actionId, + actionDescription: action.actionDescription || `Device action: ${action.actionType}`, + actionStatus: "RUNNING", + actionParameters: action.actionParameters || [], + blockingType: action.blockingType || "NONE", + }; + + // 添加到设备动作列表 + this.deviceActions.push(actionState); + + // 模拟设备操作 + this.simulateDeviceOperation(actionState); + } + + private simulateDeviceOperation(actionState: ActionState) { + this.log(`🎯 Simulating device operation: ${actionState.actionType}`); + + // 根据动作类型执行不同的模拟操作 + switch (actionState.actionType) { + case "deviceSetup": + this.simulateDeviceSetup(actionState); + break; + case "deviceWrite": + this.simulateDeviceWrite(actionState); + break; + case "deviceRead": + this.simulateDeviceRead(actionState); + break; + case "deviceStop": + this.simulateDeviceStop(actionState); + break; + case "deviceDelete": + this.simulateDeviceDelete(actionState); + break; + default: + this.log(`⚠️ Unknown device action type: ${actionState.actionType}`); + actionState.actionStatus = "FAILED"; + actionState.resultDescription = `Unknown action type: ${actionState.actionType}`; + } + } + + private simulateDeviceSetup(actionState: ActionState) { + this.log(`📱 Setting up device with parameters:`); + + if (actionState.actionParameters) { + for (const param of actionState.actionParameters) { + this.log(` ${param.key}: ${param.value}`); + } + } + + // 模拟设备设置过程 + setTimeout(() => { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = "Device setup completed successfully"; + this.log(`✅ Device setup completed for action ${actionState.actionId}`); + }, 2000); + } + + private simulateDeviceWrite(actionState: ActionState) { + this.log(`✍️ Writing to device registers`); + + // 模拟写入操作 + setTimeout(() => { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = "Device write operation completed"; + this.log(`✅ Device write completed for action ${actionState.actionId}`); + }, 1000); + } + + private simulateDeviceRead(actionState: ActionState) { + this.log(`📖 Reading from device registers`); + + // 模拟读取操作 + setTimeout(() => { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = "Device read operation completed"; + this.log(`✅ Device read completed for action ${actionState.actionId}`); + }, 500); + } + + private simulateDeviceStop(actionState: ActionState) { + this.log(`🛑 Processing device stop operation`); + + if (actionState.actionParameters) { + this.log(`📱 Stopping device with parameters:`); + for (const param of actionState.actionParameters) { + this.log(` ${param.key}: ${param.value}`); + } + } + + // 模拟设备停止过程 + setTimeout(() => { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = "Device stop operation completed successfully"; + this.log(`✅ Device stop completed for action ${actionState.actionId}`); + + // 注意:实际的设备停止操作由主进程管理器处理 + // 这里只是模拟设备层面的停止确认 + }, 1000); + } + + private simulateDeviceDelete(actionState: ActionState) { + this.log(`🗑️ Processing device delete operation`); + + if (actionState.actionParameters) { + this.log(`📱 Deleting device with parameters:`); + for (const param of actionState.actionParameters) { + this.log(` ${param.key}: ${param.value}`); + } + } + + // 模拟设备删除过程 + setTimeout(async () => { + actionState.actionStatus = "FINISHED"; + actionState.resultDescription = "Device delete operation completed successfully"; + this.log(`✅ Device delete completed for action ${actionState.actionId}`); + + // 发送最后一次状态更新,包含删除完成的动作状态 + this.publishState(); + + // 等待一小段时间确保状态发送完成 + setTimeout(async () => { + this.log(`🔄 Device delete action completed, initiating worker shutdown...`); + + // 主动关闭设备模拟器 worker + await this.close(); + }, 500); + }, 1000); + } + + async handleAction(actionMessage: any) { + this.log(`Handling external action: ${actionMessage.actionType}`); + this.processDeviceAction(actionMessage); + } + + async reconnect() { + this.log("Reconnecting to MQTT broker..."); + + try { + if (this.client) { + await this.client.endAsync(); + } + await this.connectMqtt(); + this.log("Reconnected successfully"); + this.sendMessage("status", { status: "reconnected" }); + } catch (error) { + this.log(`Reconnection failed: ${(error as Error).message}`); + this.sendMessage("error", { error: (error as Error).message }); + } + } + + async close() { + this.log("Closing device simulator..."); + + // 清理定时器 + if (this.stateInterval) { + clearInterval(this.stateInterval); + } + if (this.connectionInterval) { + clearInterval(this.connectionInterval); + } + + // 清理Modbus定时器和连接 + if (this.modbusPollTimer) { + clearInterval(this.modbusPollTimer); + } + if (this.modbus) { + try { + await this.modbus.close(); + this.log("Modbus connection closed"); + } catch (error) { + this.log(`Error closing Modbus connection: ${(error as Error).message}`); + } + } + + // 关闭MQTT连接 + if (this.client) { + try { + await this.client.endAsync(); + this.log("MQTT connection closed"); + } catch (error) { + this.log(`Error closing MQTT connection: ${(error as Error).message}`); + } + } + + this.sendMessage("status", { status: "closed" }); + } +} + +// Worker消息处理 +const simulator = new DeviceSimulator(); + +// 全局错误处理器 +self.addEventListener('error', (event) => { + console.error('❌ Uncaught error in device simulator worker:', event.error); + + // 检查是否为连接相关错误 + const errorMessage = event.error?.message || event.message || 'Unknown error'; + const isConnError = [ + "no connection", "connection lost", "connection closed", "connection refused", + "timeout", "timed out", "ECONNRESET", "ENOTCONN", "ECONNREFUSED", + "ETIMEDOUT", "EPIPE", "ENETUNREACH", "EHOSTUNREACH" + ].some(keyword => errorMessage.toLowerCase().includes(keyword.toLowerCase())); + + if (isConnError) { + console.log('❌ Global error handler detected connection failure, requesting worker reset'); + self.postMessage({ + type: "reset", + data: { + reason: "uncaught_connection_error", + error: `Uncaught connection error: ${errorMessage}` + } + } as DeviceMainMessage); + } else { + self.postMessage({ + type: "error", + data: { error: errorMessage } + } as DeviceMainMessage); + } +}); + +// 全局Promise拒绝处理器 +self.addEventListener('unhandledrejection', (event) => { + console.error('❌ Unhandled promise rejection in device simulator worker:', event.reason); + + // 检查是否为连接相关错误 + const errorMessage = event.reason?.message || event.reason || 'Unknown promise rejection'; + const isConnError = [ + "no connection", "connection lost", "connection closed", "connection refused", + "timeout", "timed out", "ECONNRESET", "ENOTCONN", "ECONNREFUSED", + "ETIMEDOUT", "EPIPE", "ENETUNREACH", "EHOSTUNREACH" + ].some(keyword => String(errorMessage).toLowerCase().includes(keyword.toLowerCase())); + + if (isConnError) { + console.log('❌ Global promise rejection handler detected connection failure, requesting worker reset'); + self.postMessage({ + type: "reset", + data: { + reason: "uncaught_promise_rejection", + error: `Uncaught promise rejection: ${errorMessage}` + } + } as DeviceMainMessage); + } else { + self.postMessage({ + type: "error", + data: { error: String(errorMessage) } + } as DeviceMainMessage); + } + + // 阻止默认的错误处理 + event.preventDefault(); +}); + +self.onmessage = async (event: MessageEvent) => { + const { type, data } = event.data; + + try { + switch (type) { + case "init": + await simulator.initialize(data); + break; + + case "action": + await simulator.handleAction(data); + break; + + case "reconnect": + await simulator.reconnect(); + break; + + case "close": + await simulator.close(); + break; + + default: + console.warn(`Unknown message type: ${type}`); + } + } catch (error) { + console.error(`Error handling message ${type}:`, error); + + // 检查是否为连接相关错误 + const errorMessage = (error as Error).message; + const isConnError = [ + "no connection", "connection lost", "connection closed", "connection refused", + "timeout", "timed out", "ECONNRESET", "ENOTCONN", "ECONNREFUSED", + "ETIMEDOUT", "EPIPE", "ENETUNREACH", "EHOSTUNREACH" + ].some(keyword => errorMessage.toLowerCase().includes(keyword.toLowerCase())); + + if (isConnError) { + console.log(`❌ Message handler detected connection failure, requesting worker reset`); + self.postMessage({ + type: "reset", + data: { + reason: "message_handler_connection_error", + error: `Message handler connection error: ${errorMessage}` + } + } as DeviceMainMessage); + } else { + self.postMessage({ + type: "error", + data: { error: errorMessage } + } as DeviceMainMessage); + } + } +}; \ No newline at end of file diff --git a/device_simulator_main.ts b/device_simulator_main.ts new file mode 100644 index 0000000..4fbe834 --- /dev/null +++ b/device_simulator_main.ts @@ -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 = 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((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 = {}; + + 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 { + 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 { + 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) => { + 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) => { + 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 { + 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 { + 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) => { + 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 { + 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); + } +} \ No newline at end of file diff --git a/devices.json b/devices.json new file mode 100644 index 0000000..0d55994 --- /dev/null +++ b/devices.json @@ -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" + } + +] \ No newline at end of file diff --git a/event_manager.ts b/event_manager.ts new file mode 100644 index 0000000..68f4257 --- /dev/null +++ b/event_manager.ts @@ -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 = 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; + 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(); \ No newline at end of file diff --git a/generate_devices.ts b/generate_devices.ts new file mode 100644 index 0000000..9a7d594 --- /dev/null +++ b/generate_devices.ts @@ -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"); \ No newline at end of file diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..e2fbaf6 --- /dev/null +++ b/import_map.json @@ -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" + } +} \ No newline at end of file diff --git a/mapping.json b/mapping.json new file mode 100644 index 0000000..bb3cb8f --- /dev/null +++ b/mapping.json @@ -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" + } + } +] \ No newline at end of file diff --git a/master_manager.ts b/master_manager.ts new file mode 100644 index 0000000..218cfda --- /dev/null +++ b/master_manager.ts @@ -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; +} \ No newline at end of file diff --git a/modbus_manager.ts b/modbus_manager.ts new file mode 100644 index 0000000..cca52e7 --- /dev/null +++ b/modbus_manager.ts @@ -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 { + 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 { + return new Promise((resolve, reject) => { + this.readQueue.push({ fn, start, len, resolve, reject }); + }); + } + + enqueueWrite( + fn: WriteJob["fn"], + start: number, + payload: number | number[] + ): Promise { + 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"); + } +} \ No newline at end of file diff --git a/proto/vda5050.proto b/proto/vda5050.proto new file mode 100644 index 0000000..84e184d --- /dev/null +++ b/proto/vda5050.proto @@ -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; +} \ No newline at end of file diff --git a/simulator.ts b/simulator.ts new file mode 100644 index 0000000..00c8037 --- /dev/null +++ b/simulator.ts @@ -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>; +} + +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((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((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(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 = {}; + + 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) => { + 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 } + }); + } +}; \ No newline at end of file diff --git a/simulator_config.json b/simulator_config.json new file mode 100644 index 0000000..0784d0d --- /dev/null +++ b/simulator_config.json @@ -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 + } +} \ No newline at end of file diff --git a/simulator_config.ts b/simulator_config.ts new file mode 100644 index 0000000..0c94355 --- /dev/null +++ b/simulator_config.ts @@ -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 { + // 配置文件可能的路径列表 + 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, + }, + }; +} \ No newline at end of file diff --git a/simulator_main.ts b/simulator_main.ts new file mode 100644 index 0000000..d39cc59 --- /dev/null +++ b/simulator_main.ts @@ -0,0 +1,1303 @@ +// simulator_main.ts - 主进程管理器 +import { loadConfig, RawConfig } from "./simulator_config.ts"; +import { createModuleLogger, perfMonitor, logMemoryUsage, setDebugLevel, LogLevel } from "./debug_logger.ts"; + +interface WorkerMessage { + type: "init" | "close" | "reconnect"; + data?: any; +} + +interface MainMessage { + type: "ready" | "error" | "status" | "log" | "device_request"; + data?: any; +} + +interface DeviceWorkerMessage { + type: "init" | "close" | "reconnect" | "action"; + data?: any; +} + +interface DeviceMainMessage { + type: "ready" | "error" | "status" | "log" | "reset"; + data?: any; +} + +interface AgvWorker { + worker: Worker; + agvId: string; + status: "starting" | "running" | "error" | "stopped"; + config: any; +} + +interface DeviceWorker { + worker: Worker; + deviceId: string; + status: "starting" | "running" | "error" | "stopped"; + config: any; + deviceInfo: { + ip: string; + port: string; + slaveId: string; + deviceName: string; + protocolType: string; + brandName: string; + }; +} + +interface DeviceKV { + [deviceId: string]: { + status: string; + createdAt: string; + lastActivity: string; + deviceInfo: any; + // 添加寄存器配置信息 + registersConfig?: string; + actionParameters?: Array<{ + key: string; + value: string; + }>; + }; +} + +// 持久化设备信息接口 +interface PersistedDeviceInfo { + deviceId: string; + deviceInfo: { + ip: string; + port: string; + slaveId: string; + deviceName: string; + protocolType: string; + brandName: string; + }; + createdAt: string; + lastActivity: string; + // 添加寄存器配置信息 + registersConfig?: string; + actionParameters?: Array<{ + key: string; + value: string; + }>; +} + +interface PersistedDevices { + devices: PersistedDeviceInfo[]; + version: string; + lastUpdated: string; +} + +class DevicePersistence { + private filePath: string; + + constructor(filePath: string = "./device_registry.json") { + this.filePath = filePath; + } + + async saveDevices(devices: PersistedDeviceInfo[]): Promise { + const data: PersistedDevices = { + devices, + version: "1.0.0", + lastUpdated: new Date().toISOString() + }; + + try { + await Deno.writeTextFile(this.filePath, JSON.stringify(data, null, 2)); + console.log(`📁 Saved ${devices.length} devices to ${this.filePath}`); + } catch (error) { + console.error(`❌ Failed to save devices: ${(error as Error).message}`); + } + } + + async loadDevices(): Promise { + try { + const data = await Deno.readTextFile(this.filePath); + const parsed: PersistedDevices = JSON.parse(data); + console.log(`📁 Loaded ${parsed.devices.length} devices from ${this.filePath}`); + return parsed.devices || []; + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + console.log(`📁 No existing device registry found at ${this.filePath}`); + return []; + } + console.error(`❌ Failed to load devices: ${(error as Error).message}`); + return []; + } + } + + async deviceExists(deviceId: string): Promise { + const devices = await this.loadDevices(); + return devices.some(device => device.deviceId === deviceId); + } + + async addDevice(deviceInfo: PersistedDeviceInfo): Promise { + const devices = await this.loadDevices(); + + // 检查是否已存在,如果存在则更新 + const existingIndex = devices.findIndex(d => d.deviceId === deviceInfo.deviceId); + if (existingIndex >= 0) { + devices[existingIndex] = deviceInfo; + console.log(`📝 Updated existing device: ${deviceInfo.deviceId}`); + } else { + devices.push(deviceInfo); + console.log(`📝 Added new device: ${deviceInfo.deviceId}`); + } + + await this.saveDevices(devices); + } + + async removeDevice(deviceId: string): Promise { + const devices = await this.loadDevices(); + const filteredDevices = devices.filter(d => d.deviceId !== deviceId); + + if (filteredDevices.length < devices.length) { + await this.saveDevices(filteredDevices); + console.log(`📝 Removed device: ${deviceId}`); + } + } +} + +class SimulatorManager { + private workers: Map = new Map(); + private deviceWorkers: Map = new Map(); + private deviceKV: DeviceKV = {}; + private config!: RawConfig; + private devicePersistence: DevicePersistence; + private logger = createModuleLogger("SIMULATOR_MAIN"); + + constructor() { + this.devicePersistence = new DevicePersistence(); + this.logger.info("🎯 SimulatorManager initialized"); + } + + async initialize() { + this.config = await loadConfig(); + console.log("📋 Loaded configuration for", this.config.settings.robotCount, "robots"); + console.log("🔧 Device simulator management initialized"); + + // 恢复持久化的设备 + await this.restorePersistedDevices(); + } + + private async restorePersistedDevices() { + console.log("🔄 Restoring persisted devices..."); + + try { + const persistedDevices = await this.devicePersistence.loadDevices(); + + if (persistedDevices.length === 0) { + console.log("📱 No persisted devices found"); + return; + } + + console.log(`🚀 Starting restoration of ${persistedDevices.length} devices...`); + + // helper to restore a device with retries on failure + const restoreWithRetry = async (pd: PersistedDeviceInfo) => { + try { + console.log(`🔧 Restoring device: ${pd.deviceId} (${pd.deviceInfo.deviceName})`); + await this.createDeviceWorkerFromPersisted(pd); + this.deviceKV[pd.deviceId] = { + status: "running", + createdAt: pd.createdAt, + lastActivity: new Date().toISOString(), + deviceInfo: pd.deviceInfo, + registersConfig: pd.registersConfig, + actionParameters: pd.actionParameters + }; + console.log(`✅ Device ${pd.deviceId} restored successfully`); + } catch (error) { + console.error(`❌ Failed to restore device ${pd.deviceId}:`, (error as Error).message); + setTimeout(() => restoreWithRetry(pd), 5000); + } + }; + + for (const persistedDevice of persistedDevices) { + restoreWithRetry(persistedDevice); + } + + console.log(`🎉 Device restoration tasks scheduled. Active devices: ${this.getDeviceCount()}`); + } catch (error) { + console.error("❌ Failed to restore persisted devices:", (error as Error).message); + } + } + + private async createDeviceWorkerFromPersisted(persistedDevice: PersistedDeviceInfo): Promise { + const { deviceId, deviceInfo } = persistedDevice; + + // 创建Worker配置 + const brokerUrl = `mqtt://${this.config.mqttBroker.host}:${this.config.mqttBroker.port}`; + const workerConfig = { + deviceId, + deviceInfo, + vdaInterface: this.config.mqttBroker.vdaInterface, + mqtt: { + brokerUrl, + options: { + clean: true, + keepalive: 60, + } + }, + vehicle: { + manufacturer: this.config.vehicle.manufacturer, + serialNumber: deviceId, + vdaVersion: this.config.vehicle.vdaVersion, + } + }; + + await this.startDeviceWorker(deviceId, deviceInfo, workerConfig); + } + + async startAllWorkers() { + console.log(`🚀 Starting ${this.config.settings.robotCount} AGV workers...`); + + const brokerUrl = `mqtt://${this.config.mqttBroker.host}:${this.config.mqttBroker.port}`; + + for (let i = 0; i < this.config.settings.robotCount; i++) { + const agvId = `${this.config.vehicle.serialNumber}${i}`; + + const workerConfig = { + vdaInterface: this.config.mqttBroker.vdaInterface, + zoneSetId: this.config.settings.mapId, + mapId: this.config.settings.mapId, + vehicle: { + manufacturer: this.config.vehicle.manufacturer, + serialNumber: agvId, + vdaVersion: this.config.vehicle.vdaVersion, + }, + mqtt: { + brokerUrl, + options: { + clean: true, + keepalive: 60, + } + }, + settings: { + robotCount: this.config.settings.robotCount, + stateFrequency: this.config.settings.stateFrequency, + visualizationFrequency: this.config.settings.visualizationFrequency, + speed: this.config.settings.speed, + }, + }; + + await this.startWorker(agvId, workerConfig); + } + } + + private async startWorker(agvId: string, config: any): Promise { + if (this.workers.has(agvId)) { + console.log(`⚠️ Worker ${agvId} already exists, stopping it first`); + await this.stopWorker(agvId); + } + + console.log(`🔧 Creating worker for AGV ${agvId}`); + + const worker = new Worker(new URL("./simulator.ts", import.meta.url).href, { + type: "module", + }); + + const agvWorker: AgvWorker = { + worker, + agvId, + status: "starting", + config, + }; + + this.workers.set(agvId, agvWorker); + this.setupWorkerHandlers(agvWorker); + + // 发送初始化消息 + worker.postMessage({ type: "init", data: config } as WorkerMessage); + + // 等待worker准备就绪 + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + agvWorker.status = "error"; + reject(new Error(`Worker ${agvId} failed to start within timeout`)); + }, 15000); // 15秒超时 + + const originalHandler = worker.onmessage; + worker.onmessage = (event: MessageEvent) => { + if (event.data.type === "ready") { + clearTimeout(timeout); + agvWorker.status = "running"; + worker.onmessage = originalHandler; + console.log(`✅ Worker ${agvId} started successfully`); + resolve(); + } else if (event.data.type === "error") { + clearTimeout(timeout); + agvWorker.status = "error"; + worker.onmessage = originalHandler; + reject(new Error(`Worker ${agvId} failed to start: ${event.data.data?.error}`)); + } + }; + }); + } + + private setupWorkerHandlers(agvWorker: AgvWorker) { + const { worker, agvId } = agvWorker; + + worker.onmessage = (event: MessageEvent) => { + const { type, data } = event.data; + + switch (type) { + case "ready": + agvWorker.status = "running"; + console.log(`✅ AGV ${agvId} is ready`); + break; + + case "error": + agvWorker.status = "error"; + console.error(`❌ AGV ${agvId} error:`, data?.error); + break; + + case "status": + console.log(`📊 AGV ${agvId} status:`, data?.status); + if (data?.status === "closed") { + agvWorker.status = "stopped"; + } + break; + + case "log": + // 转发日志到控制台 + console.log(data?.message); + break; + + case "device_request": + // 处理设备模拟器请求 + this.handleDeviceRequest(agvId, data); + break; + + default: + console.warn(`Unknown message type from ${agvId}:`, type); + } + }; + + worker.onerror = (error) => { + agvWorker.status = "error"; + console.error(`❌ Worker ${agvId} error:`, error.message); + }; + + worker.onmessageerror = (error) => { + console.error(`❌ Message error from worker ${agvId}:`, error); + }; + } + + async stopWorker(agvId: string): Promise { + const agvWorker = this.workers.get(agvId); + if (!agvWorker) { + console.warn(`⚠️ Worker ${agvId} not found`); + return; + } + + console.log(`🛑 Stopping worker ${agvId}`); + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + agvWorker.worker.terminate(); + this.workers.delete(agvId); + console.log(`🔄 Force terminated worker ${agvId}`); + resolve(); + }, 5000); + + agvWorker.worker.onmessage = (event: MessageEvent) => { + if (event.data.type === "status" && event.data.data?.status === "closed") { + clearTimeout(timeout); + agvWorker.worker.terminate(); + this.workers.delete(agvId); + console.log(`✅ Worker ${agvId} stopped gracefully`); + resolve(); + } + }; + + agvWorker.worker.postMessage({ type: "close" } as WorkerMessage); + }); + } + + async stopAllWorkers(): Promise { + console.log("🛑 Stopping all workers..."); + const stopPromises = Array.from(this.workers.keys()).map(agvId => this.stopWorker(agvId)); + await Promise.all(stopPromises); + + // 同时停止所有设备模拟器 + await this.stopAllDeviceWorkers(); + + console.log("✅ All workers stopped"); + } + + async resetWorker(agvId: string): Promise { + const agvWorker = this.workers.get(agvId); + if (!agvWorker) { + console.error(`❌ Worker ${agvId} not found`); + return; + } + + console.log(`🔄 Resetting worker ${agvId} (restarting worker process)`); + const config = agvWorker.config; + await this.stopWorker(agvId); + await this.startWorker(agvId, config); + } + + async reconnectWorker(agvId: string): Promise { + const agvWorker = this.workers.get(agvId); + if (!agvWorker) { + console.error(`❌ Worker ${agvId} not found`); + return; + } + + console.log(`🔌 Reconnecting worker ${agvId}`); + agvWorker.worker.postMessage({ type: "reconnect" } as WorkerMessage); + } + + async restartWorker(agvId: string): Promise { + const agvWorker = this.workers.get(agvId); + if (!agvWorker) { + console.error(`❌ Worker ${agvId} not found`); + return; + } + + console.log(`🔄 Restarting worker ${agvId}`); + const config = agvWorker.config; + await this.stopWorker(agvId); + await this.startWorker(agvId, config); + } + + getWorkerStatus(agvId?: string) { + if (agvId) { + const worker = this.workers.get(agvId); + return worker ? { agvId, status: worker.status } : null; + } + + return Array.from(this.workers.entries()).map(([id, worker]) => ({ + agvId: id, + status: worker.status + })); + } + + getWorkerCount(): number { + return this.workers.size; + } + + // 交互式命令处理 + async handleCommand(command: string, args: string[] = []) { + const cmd = command.toLowerCase(); + + switch (cmd) { + case "status": + if (args[0]) { + const status = this.getWorkerStatus(args[0]); + if (status && !Array.isArray(status)) { + console.log(`📊 AGV ${args[0]}: ${status.status}`); + } else { + console.log(`❌ AGV ${args[0]} not found`); + } + } else { + const statuses = this.getWorkerStatus(); + console.log("📊 AGV Worker Status:"); + if (Array.isArray(statuses)) { + statuses.forEach((s: any) => console.log(` ${s.agvId}: ${s.status}`)); + } + + // 显示设备状态 + console.log(`📱 Device Workers: ${this.getDeviceCount()}`); + const deviceKV = this.getDeviceKV(); + for (const [deviceId, info] of Object.entries(deviceKV)) { + console.log(` ${deviceId}: ${info.status} (${info.deviceInfo.deviceName})`); + } + } + break; + + case "devices":{ + const deviceKV = this.getDeviceKV(); + console.log(`📱 Active Device Simulators (${this.getDeviceCount()}):`); + for (const [deviceId, info] of Object.entries(deviceKV)) { + console.log(` ${deviceId}:`); + console.log(` Name: ${info.deviceInfo.deviceName} (${info.deviceInfo.brandName})`); + console.log(` Address: ${info.deviceInfo.ip}:${info.deviceInfo.port}/${info.deviceInfo.slaveId}`); + console.log(` Protocol: ${info.deviceInfo.protocolType}`); + console.log(` Status: ${info.status}`); + console.log(` Created: ${info.createdAt}`); + console.log(` Last Activity: ${info.lastActivity}`); + + // 显示寄存器配置信息 + if (info.registersConfig) { + console.log(` 📝 Registers Configuration:`); + try { + const registers = JSON.parse(info.registersConfig); + console.log(` Total Registers: ${registers.length}`); + registers.forEach((reg: any, index: number) => { + console.log(` [${index + 1}] ${reg.name}:`); + console.log(` Function Code: ${reg.fnCode} (${this.getFunctionCodeDescription(reg.fnCode)})`); + console.log(` Address: ${reg.regAddress}`); + console.log(` Count: ${reg.regCount}`); + }); + } catch (error) { + console.log(` ❌ Invalid JSON format: ${info.registersConfig}`); + } + } else { + console.log(` 📝 Registers Configuration: None`); + } + + // 显示其他动作参数 + if (info.actionParameters && info.actionParameters.length > 0) { + console.log(` 🔧 Action Parameters:`); + info.actionParameters.forEach((param: any, index: number) => { + if (param.key !== "registers") { // registers已经单独显示了 + const displayValue = param.value.length > 50 ? + param.value.substring(0, 50) + '...' : + param.value; + console.log(` [${index + 1}] ${param.key}: ${displayValue}`); + } + }); + } + + console.log(); // 空行分隔 + } + break; + } + case "devicestop": + if (args[0]) { + await this.stopDeviceSimulator(args[0]); + } else { + console.log("Usage: devicestop "); + console.log("Available devices:"); + const devices = this.getDeviceKV(); + for (const deviceId of Object.keys(devices)) { + console.log(` ${deviceId}`); + } + } + break; + + case "persistent": + if (args[0] === "list") { + await this.listPersistedDevices(); + } else if (args[0] === "clear") { + await this.clearPersistedDevices(); + } else if (args[0] === "restore") { + await this.restorePersistedDevices(); + } else { + console.log("Usage: persistent "); + console.log(" list - Show all persisted devices"); + console.log(" clear - Clear all persisted devices"); + console.log(" restore - Restore persisted devices (force reload)"); + } + break; + + case "start": + await this.startAllWorkers(); + break; + + case "stop": + if (args[0]) { + await this.stopWorker(args[0]); + } else { + await this.stopAllWorkers(); + } + break; + + case "reset": + if (args[0]) { + await this.resetWorker(args[0]); + } else { + // 重置所有workers + console.log("🔄 Resetting all workers..."); + const workerIds = Array.from(this.workers.keys()); + for (const agvId of workerIds) { + await this.resetWorker(agvId); + } + } + break; + + case "reconnect": + if (args[0]) { + await this.reconnectWorker(args[0]); + } else { + console.log("Usage: reconnect "); + } + break; + + case "restart": + if (args[0]) { + await this.restartWorker(args[0]); + } else { + console.log("Usage: restart "); + } + break; + + case "count": + console.log(`📊 Active Workers: ${this.getWorkerCount()}`); + console.log(`📱 Active Devices: ${this.getDeviceCount()}`); + break; + + case "debug": + if (args[0] === "level") { + if (args[1]) { + const level = args[1].toUpperCase(); + const module = args[2]; + try { + setDebugLevel(level, module); + } catch (error) { + console.log(`❌ Invalid debug level: ${level}`); + console.log("Valid levels: ERROR, WARN, INFO, DEBUG, TRACE"); + } + } else { + console.log("Usage: debug level [module]"); + console.log("Levels: ERROR, WARN, INFO, DEBUG, TRACE"); + console.log("Modules: DEVICE_SIMULATOR, MODBUS, MQTT, REGISTER_CONFIG, DEVICE_MANAGER, SIMULATOR_MAIN"); + } + } else if (args[0] === "memory") { + logMemoryUsage("SIMULATOR_MAIN"); + } else { + console.log(` +🔧 Debug Commands: + debug level [module] - Set debug level (ERROR/WARN/INFO/DEBUG/TRACE) + debug memory - Show current memory usage + +Examples: + debug level DEBUG - Set global debug level + debug level TRACE DEVICE_SIMULATOR - Set module-specific level + debug memory - Show memory usage + `); + } + break; + + case "help": + console.log(` +📖 Available Commands: + AGV Management: + status [agvId] - Show AGV worker status + start - Start all AGV workers + stop [agvId] - Stop AGV worker(s) + reset [agvId] - Reset AGV worker(s) (stop and restart) + reconnect - Reconnect AGV worker MQTT + restart - Restart AGV worker process + count - Show worker and device counts + + Device Management: + devices - List all active device simulators + devicestop - Stop specific device simulator + persistent list - Show all persisted devices + persistent clear - Clear all persisted devices + persistent restore - Force restore persisted devices + + Debug Commands: + debug level [module] - Set debug level + debug memory - Show memory usage + + General: + help - Show this help + quit/exit - Stop all workers and exit + `); + break; + + case "quit": + case "exit": + await this.stopAllWorkers(); + Deno.exit(0); + break; + + default: + console.log(`❌ Unknown command: ${command}. Type 'help' for available commands.`); + } + } + + async startInteractiveMode() { + console.log(` +🎮 Interactive Mode Started +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("simulator> ")); + + // 读取用户输入 + 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); + } + } + } + + // 设备模拟器管理方法 + private async handleDeviceRequest(agvId: string, data: any): Promise { + const { action, deviceInfo, deviceId, originalAction } = data; + + console.log(`🔧 Handling device request from ${agvId}: ${action}`); + + switch (action) { + case "create": + await this.createDeviceSimulator(deviceInfo, originalAction); + break; + case "stop": + await this.stopDeviceSimulator(deviceId); + break; + case "delete": + await this.stopDeviceSimulator(deviceId); + break; + case "forward": + await this.forwardActionToDevice(deviceId, originalAction); + break; + default: + console.warn(`Unknown device action: ${action}`); + } + } + + private async createDeviceSimulator(deviceInfo: any, originalAction: any): Promise { + const deviceId = `${deviceInfo.ip}-${deviceInfo.port}-${deviceInfo.slaveId}`; + + console.log(`🆕 Processing device creation request: ${deviceId}`); + console.log(`📱 Device: ${deviceInfo.deviceName} (${deviceInfo.brandName})`); + console.log(`🔌 Protocol: ${deviceInfo.protocolType}`); + + // 检查设备模拟器是否已经在运行中 + if (this.deviceWorkers.has(deviceId)) { + console.log(`⚠️ Device ${deviceId} worker is already running, ignoring duplicate creation request`); + console.log(`📡 Forwarding action to existing device instead`); + await this.forwardActionToDevice(deviceId, originalAction); + return; + } + + // 检查是否在持久化存储中存在(可能是重启后还未恢复的设备) + const persistedExists = await this.devicePersistence.deviceExists(deviceId); + if (persistedExists) { + console.log(`📁 Device ${deviceId} exists in persistent storage but worker not running`); + console.log(`🚀 Starting worker for existing persisted device`); + } else { + console.log(`🆕 Creating new device: ${deviceId}`); + } + + // 创建Worker配置 + const brokerUrl = `mqtt://${this.config.mqttBroker.host}:${this.config.mqttBroker.port}`; + const workerConfig = { + deviceId, + deviceInfo, + vdaInterface: this.config.mqttBroker.vdaInterface, + mqtt: { + brokerUrl, + options: { + clean: true, + keepalive: 60, + } + }, + vehicle: { + manufacturer: this.config.vehicle.manufacturer, + serialNumber: deviceId, + vdaVersion: this.config.vehicle.vdaVersion, + } + }; + + // Attempt to start the device worker, but proceed regardless of success or failure + let startError: Error | null = null; + try { + await this.startDeviceWorker(deviceId, deviceInfo, workerConfig); + } catch (error) { + startError = error as Error; + console.error(`❌ Failed to start device worker for ${deviceId}:`, startError.message); + } + + // Record and persist the device in all cases + const now = new Date().toISOString(); + // 提取寄存器配置 + const registersParam = originalAction.actionParameters?.find((p: any) => p.key === "registers"); + const registersConfig = registersParam?.value; + + // 更新KV存储 + this.deviceKV[deviceId] = { + status: startError ? "error" : "running", + createdAt: now, + lastActivity: now, + deviceInfo, + registersConfig, + actionParameters: originalAction.actionParameters + }; + + // 保存到持久化存储 + const persistedDevice: PersistedDeviceInfo = { + deviceId, + deviceInfo, + createdAt: now, + lastActivity: now, + registersConfig, + actionParameters: originalAction.actionParameters + }; + await this.devicePersistence.addDevice(persistedDevice); + + // 等待设备模拟器启动完成后发送初始action + setTimeout(async () => { + await this.forwardActionToDevice(deviceId, originalAction); + }, 2000); + + // Provide a combined status message + if (startError) { + console.log(`⚠ Device simulator ${deviceId} added but worker start failed: ${startError.message}`); + } else { + console.log(`✅ Device simulator ${deviceId} created and persisted successfully`); + } + } + + private async startDeviceWorker(deviceId: string, deviceInfo: any, config: any): Promise { + console.log(`🔧 Starting device worker for ${deviceId}`); + + const worker = new Worker(new URL("./device_simulator.ts", import.meta.url).href, { + type: "module", + }); + + const deviceWorker: DeviceWorker = { + worker, + deviceId, + status: "starting", + config, + deviceInfo, + }; + + this.deviceWorkers.set(deviceId, deviceWorker); + this.setupDeviceWorkerHandlers(deviceWorker); + + // 发送初始化消息 + worker.postMessage({ type: "init", data: config } as DeviceWorkerMessage); + + // 等待worker准备就绪 + 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) => { + if (event.data.type === "ready") { + clearTimeout(timeout); + deviceWorker.status = "running"; + worker.onmessage = originalHandler; + console.log(`✅ Device worker ${deviceId} started successfully`); + 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) => { + 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"; + // 设备 worker 自我关闭时,需要完整清理所有数据 + this.handleDeviceWorkerClosure(deviceId); + } + 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); + } + + // 更新最后活动时间 + if (this.deviceKV[deviceId]) { + this.deviceKV[deviceId].lastActivity = new Date().toISOString(); + } + }; + + 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 { + 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"; + } + } + + private async forwardActionToDevice(deviceId: string, action: any): Promise { + const deviceWorker = this.deviceWorkers.get(deviceId); + if (!deviceWorker) { + console.warn(`⚠️ Device worker ${deviceId} not found, cannot forward action`); + return; + } + + console.log(`📡 Forwarding action to device ${deviceId}: ${action.actionType}`); + deviceWorker.worker.postMessage({ + type: "action", + data: action + } as DeviceWorkerMessage); + } + + // 处理设备 worker 自我关闭(如 deviceDelete 触发的关闭) + private async handleDeviceWorkerClosure(deviceId: string): Promise { + console.log(`🔄 Handling device worker closure for ${deviceId}`); + + // 清理内存引用 + this.cleanupDeviceReferences(deviceId); + + // 检查是否是 deviceDelete 操作导致的关闭,如果是,则清理持久化数据 + // 注意:这里我们需要判断是正常删除还是意外关闭 + // 由于 deviceDelete 会主动关闭 worker,我们检查持久化存储中是否仍然存在此设备 + try { + const deviceExists = await this.devicePersistence.deviceExists(deviceId); + if (deviceExists) { + console.log(`🗑️ Device ${deviceId} closure appears to be from deviceDelete, cleaning up persistent data...`); + await this.cleanupDeviceData(deviceId); + } else { + console.log(`ℹ️ Device ${deviceId} closure - no persistent data cleanup needed`); + } + } catch (error) { + console.error(`❌ Error checking device existence during closure: ${(error as Error).message}`); + } + + console.log(`✅ Device worker closure handling completed for ${deviceId}`); + } + + // 只停止设备worker,不删除持久化数据(用于应用退出时) + private async stopDeviceWorker(deviceId: string): Promise { + const deviceWorker = this.deviceWorkers.get(deviceId); + if (!deviceWorker) { + console.warn(`⚠️ Device worker ${deviceId} not found`); + return; + } + + console.log(`🛑 Stopping device worker ${deviceId} (preserving persistence)`); + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + deviceWorker.worker.terminate(); + this.deviceWorkers.delete(deviceId); + delete this.deviceKV[deviceId]; + console.log(`🔄 Force terminated device worker ${deviceId}`); + resolve(); + }, 5000); + + deviceWorker.worker.onmessage = (event: MessageEvent) => { + if (event.data.type === "status" && event.data.data?.status === "closed") { + clearTimeout(timeout); + deviceWorker.worker.terminate(); + this.deviceWorkers.delete(deviceId); + delete this.deviceKV[deviceId]; + console.log(`✅ Device worker ${deviceId} stopped gracefully`); + resolve(); + } + }; + + deviceWorker.worker.postMessage({ type: "close" } as DeviceWorkerMessage); + }); + } + + // 停止设备模拟器并删除持久化数据(用于明确的设备删除) + private async stopDeviceSimulator(deviceId: string): Promise { + const deviceWorker = this.deviceWorkers.get(deviceId); + if (!deviceWorker) { + console.warn(`⚠️ Device worker ${deviceId} not found`); + // 即使worker不存在,也要尝试清理持久化数据 + await this.cleanupDeviceData(deviceId); + return; + } + + console.log(`🛑 Stopping device simulator ${deviceId} (removing all data)`); + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + console.log(`⚠️ Timeout waiting for ${deviceId} to close gracefully, forcing termination`); + deviceWorker.worker.terminate(); + this.cleanupDeviceReferences(deviceId); + console.log(`🔄 Force terminated device worker ${deviceId}`); + resolve(); + }, 5000); + + deviceWorker.worker.onmessage = (event: MessageEvent) => { + if (event.data.type === "status" && event.data.data?.status === "closed") { + clearTimeout(timeout); + deviceWorker.worker.terminate(); + this.cleanupDeviceReferences(deviceId); + console.log(`✅ Device worker ${deviceId} stopped gracefully`); + resolve(); + } + }; + + deviceWorker.worker.postMessage({ type: "close" } as DeviceWorkerMessage); + }).then(async () => { + // 完整清理所有设备数据 + await this.cleanupDeviceData(deviceId); + }); + } + + // 清理内存中的设备引用 + private cleanupDeviceReferences(deviceId: string): void { + console.log(`🧹 Cleaning up device references for ${deviceId}`); + + // 清理设备Worker映射 + if (this.deviceWorkers.has(deviceId)) { + this.deviceWorkers.delete(deviceId); + console.log(` ✅ Removed device worker reference`); + } + + // 清理设备KV存储 + if (this.deviceKV[deviceId]) { + delete this.deviceKV[deviceId]; + console.log(` ✅ Removed device KV data`); + } + } + + // 清理持久化的设备数据 + private async cleanupDeviceData(deviceId: string): Promise { + console.log(`🗑️ Cleaning up persistent data for ${deviceId}`); + + try { + // 从持久化存储中移除设备 + await this.devicePersistence.removeDevice(deviceId); + console.log(` ✅ Device ${deviceId} removed from persistent storage`); + + // 验证删除是否成功 + const stillExists = await this.devicePersistence.deviceExists(deviceId); + if (stillExists) { + console.warn(` ⚠️ Device ${deviceId} still exists in persistent storage after deletion attempt`); + } else { + console.log(` ✅ Verified: Device ${deviceId} successfully removed from persistent storage`); + } + + } catch (error) { + console.error(` ❌ Failed to remove device ${deviceId} from persistent storage:`, (error as Error).message); + + // 尝试强制清理 + console.log(` 🔄 Attempting force cleanup for ${deviceId}`); + try { + const devices = await this.devicePersistence.loadDevices(); + const filteredDevices = devices.filter(d => d.deviceId !== deviceId); + await this.devicePersistence.saveDevices(filteredDevices); + console.log(` ✅ Force cleanup completed for ${deviceId}`); + } catch (forceError) { + console.error(` ❌ Force cleanup failed:`, (forceError as Error).message); + } + } + + console.log(`🎯 Device ${deviceId} cleanup completed`); + } + + private async stopAllDeviceWorkers(): Promise { + console.log("🛑 Stopping all device workers..."); + // 使用新的stopDeviceWorker方法,只停止worker但保留持久化数据 + const stopPromises = Array.from(this.deviceWorkers.keys()).map(deviceId => this.stopDeviceWorker(deviceId)); + await Promise.all(stopPromises); + console.log("✅ All device workers stopped"); + } + + getDeviceKV(): DeviceKV { + return { ...this.deviceKV }; + } + + getDeviceCount(): number { + return this.deviceWorkers.size; + } + + private getFunctionCodeDescription(fnCode: string): string { + const descriptions: Record = { + "1": "Read Coils (读线圈)", + "2": "Read Discrete Inputs (读离散输入)", + "3": "Read Holding Registers (读保持寄存器)", + "4": "Read Input Registers (读输入寄存器)", + "5": "Write Single Coil (写单个线圈)", + "6": "Write Single Register (写单个寄存器)", + "15": "Write Multiple Coils (写多个线圈)", + "16": "Write Multiple Registers (写多个寄存器)" + }; + return descriptions[fnCode] || `Unknown Function Code (未知功能码 ${fnCode})`; + } + + private async listPersistedDevices() { + const devices = await this.devicePersistence.loadDevices(); + console.log(`📁 Persisted Devices (${devices.length}):`); + devices.forEach((device, index) => { + console.log(`${index + 1}. ${device.deviceId}:`); + console.log(` Name: ${device.deviceInfo.deviceName} (${device.deviceInfo.brandName})`); + console.log(` Address: ${device.deviceInfo.ip}:${device.deviceInfo.port}/${device.deviceInfo.slaveId}`); + console.log(` Protocol: ${device.deviceInfo.protocolType}`); + console.log(` Created: ${device.createdAt}`); + console.log(` Last Activity: ${device.lastActivity}`); + + // 显示寄存器配置信息 + if (device.registersConfig) { + console.log(` 📝 Registers Configuration:`); + try { + const registers = JSON.parse(device.registersConfig); + console.log(` Total Registers: ${registers.length}`); + registers.forEach((reg: any, regIndex: number) => { + console.log(` [${regIndex + 1}] ${reg.name}:`); + console.log(` Function Code: ${reg.fnCode} (${this.getFunctionCodeDescription(reg.fnCode)})`); + console.log(` Address: ${reg.regAddress}`); + console.log(` Count: ${reg.regCount}`); + }); + } catch (error) { + console.log(` ❌ Invalid JSON format: ${device.registersConfig}`); + } + } else { + console.log(` 📝 Registers Configuration: None`); + } + + console.log(); // 空行分隔 + }); + } + + private async clearPersistedDevices() { + await this.devicePersistence.saveDevices([]); + console.log("📁 All persisted devices cleared"); + } +} + +// 主程序入口 +if (import.meta.main) { + const manager = new SimulatorManager(); + + // 重试配置 + const RETRY_CONFIG = { + maxRetries: 10, // 最大重试次数 + initialDelay: 1000, // 初始延迟 1秒 + maxDelay: 30000, // 最大延迟 30秒 + backoffMultiplier: 2 // 指数退避倍数 + }; + + let retryCount = 0; + let currentDelay = RETRY_CONFIG.initialDelay; + + // 延迟函数 + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + // 启动函数 + const startSimulator = async (): Promise => { + try { + console.log(`🚀 Starting simulator (attempt ${retryCount + 1}/${RETRY_CONFIG.maxRetries + 1})...`); + + await manager.initialize(); + await manager.startAllWorkers(); + + console.log(` +🎯 All ${manager.getWorkerCount()} AGV workers started successfully! +Starting interactive mode... + `); + + // 重置重试计数器 + retryCount = 0; + currentDelay = RETRY_CONFIG.initialDelay; + + return; // 成功启动,退出重试循环 + + } catch (error) { + const errorMessage = (error as Error).message; + console.error(`❌ Startup failed (attempt ${retryCount + 1}):`, errorMessage); + + retryCount++; + + if (retryCount > RETRY_CONFIG.maxRetries) { + console.error(`💥 Maximum retry attempts (${RETRY_CONFIG.maxRetries}) exceeded. Giving up.`); + console.error("🔧 Please check your configuration and network connectivity."); + Deno.exit(1); + } + + console.log(`⏳ Waiting ${currentDelay / 1000}s before retry ${retryCount + 1}...`); + await delay(currentDelay); + + // 指数退避,但不超过最大延迟 + currentDelay = Math.min( + currentDelay * RETRY_CONFIG.backoffMultiplier, + RETRY_CONFIG.maxDelay + ); + + // 递归重试 + return startSimulator(); + } + }; + + // 启动模拟器 + await startSimulator(); + + // 处理进程信号 + Deno.addSignalListener("SIGINT", async () => { + console.log("\n⚠️ Received SIGINT, shutting down..."); + await manager.stopAllWorkers(); + Deno.exit(0); + }); + + // SIGTERM只在非Windows系统支持 + if (Deno.build.os !== "windows") { + Deno.addSignalListener("SIGTERM", async () => { + console.log("\n⚠️ Received SIGTERM, shutting down..."); + await manager.stopAllWorkers(); + Deno.exit(0); + }); + } + + // 启动交互模式 + await manager.startInteractiveMode(); +} \ No newline at end of file diff --git a/test_events.ts b/test_events.ts new file mode 100644 index 0000000..ba1814c --- /dev/null +++ b/test_events.ts @@ -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 退出"); \ No newline at end of file diff --git a/test_worker.ts b/test_worker.ts new file mode 100644 index 0000000..f4c1298 --- /dev/null +++ b/test_worker.ts @@ -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 事件系统已初始化"); \ No newline at end of file diff --git a/vda5050_transformer_worker.ts b/vda5050_transformer_worker.ts new file mode 100644 index 0000000..f31105b --- /dev/null +++ b/vda5050_transformer_worker.ts @@ -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; + }>; + 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; + } + | { + type: "array"; + op?: "array"; + source: string; + mapping: Record; + } + | { + op: "toString"; + source: string; + } + | { + // 通用对象映射(没有 type 字段) + mapping: Record; + 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): 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; 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; + }>; +} = { + 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) => { + 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" }); + } +}; \ No newline at end of file diff --git a/vda_worker.ts b/vda_worker.ts new file mode 100644 index 0000000..6309f25 --- /dev/null +++ b/vda_worker.ts @@ -0,0 +1,837 @@ +/// + +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 = 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 + const order: Headerless = { + 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 = { + 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 = { + 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 = { + 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)); +}); \ No newline at end of file diff --git a/web_worker.ts b/web_worker.ts new file mode 100644 index 0000000..866ee2e --- /dev/null +++ b/web_worker.ts @@ -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 = new Map(); +let server: { shutdown: () => Promise } | 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` + + + + AGV Position Monitor + + + +

AGV Position Monitor

+
+ + +
+ + + + + `); +}); + +// 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(); \ No newline at end of file diff --git a/worker_event_helper.ts b/worker_event_helper.ts new file mode 100644 index 0000000..1872a61 --- /dev/null +++ b/worker_event_helper.ts @@ -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> = 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); +} \ No newline at end of file