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