// 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 } }); } };