1374 lines
46 KiB
TypeScript
1374 lines
46 KiB
TypeScript
![]() |
// 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<string, string> = {
|
|||
|
"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<string, string> = {
|
|||
|
"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<void>((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<DeviceWorkerMessage>) => {
|
|||
|
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);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|