1303 lines
43 KiB
TypeScript
1303 lines
43 KiB
TypeScript
![]() |
// 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<void> {
|
|||
|
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<PersistedDeviceInfo[]> {
|
|||
|
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<boolean> {
|
|||
|
const devices = await this.loadDevices();
|
|||
|
return devices.some(device => device.deviceId === deviceId);
|
|||
|
}
|
|||
|
|
|||
|
async addDevice(deviceInfo: PersistedDeviceInfo): Promise<void> {
|
|||
|
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<void> {
|
|||
|
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<string, AgvWorker> = new Map();
|
|||
|
private deviceWorkers: Map<string, DeviceWorker> = 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<void> {
|
|||
|
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<void> {
|
|||
|
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<MainMessage>) => {
|
|||
|
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<MainMessage>) => {
|
|||
|
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<void> {
|
|||
|
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<MainMessage>) => {
|
|||
|
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<void> {
|
|||
|
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<void> {
|
|||
|
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<void> {
|
|||
|
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<void> {
|
|||
|
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 <deviceId>");
|
|||
|
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 <list|clear|restore>");
|
|||
|
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 <agvId>");
|
|||
|
}
|
|||
|
break;
|
|||
|
|
|||
|
case "restart":
|
|||
|
if (args[0]) {
|
|||
|
await this.restartWorker(args[0]);
|
|||
|
} else {
|
|||
|
console.log("Usage: restart <agvId>");
|
|||
|
}
|
|||
|
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 <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 <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 <agvId> - Reconnect AGV worker MQTT
|
|||
|
restart <agvId> - Restart AGV worker process
|
|||
|
count - Show worker and device counts
|
|||
|
|
|||
|
Device Management:
|
|||
|
devices - List all active device simulators
|
|||
|
devicestop <id> - 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 <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<void> {
|
|||
|
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<void> {
|
|||
|
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<void> {
|
|||
|
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<DeviceMainMessage>) => {
|
|||
|
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<DeviceMainMessage>) => {
|
|||
|
const { type, data } = event.data;
|
|||
|
|
|||
|
switch (type) {
|
|||
|
case "ready":
|
|||
|
deviceWorker.status = "running";
|
|||
|
console.log(`✅ Device ${deviceId} is ready`);
|
|||
|
break;
|
|||
|
|
|||
|
case "error":
|
|||
|
deviceWorker.status = "error";
|
|||
|
console.error(`❌ Device ${deviceId} error:`, data?.error);
|
|||
|
break;
|
|||
|
|
|||
|
case "status":
|
|||
|
console.log(`📊 Device ${deviceId} status:`, data?.status);
|
|||
|
if (data?.status === "closed") {
|
|||
|
deviceWorker.status = "stopped";
|
|||
|
// 设备 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<void> {
|
|||
|
const { deviceId } = deviceWorker;
|
|||
|
console.log(`🔄 Resetting device worker ${deviceId} due to: ${resetData?.reason}`);
|
|||
|
|
|||
|
try {
|
|||
|
// 终止当前worker
|
|||
|
deviceWorker.worker.terminate();
|
|||
|
|
|||
|
// 创建新的worker
|
|||
|
const newWorker = new Worker(new URL("./device_simulator.ts", import.meta.url).href, {
|
|||
|
type: "module",
|
|||
|
});
|
|||
|
|
|||
|
// 更新worker引用
|
|||
|
deviceWorker.worker = newWorker;
|
|||
|
deviceWorker.status = "starting";
|
|||
|
|
|||
|
// 重新设置消息处理
|
|||
|
this.setupDeviceWorkerHandlers(deviceWorker);
|
|||
|
|
|||
|
// 重新初始化
|
|||
|
newWorker.postMessage({ type: "init", data: deviceWorker.config } as DeviceWorkerMessage);
|
|||
|
|
|||
|
console.log(`✅ Device worker ${deviceId} reset completed`);
|
|||
|
} catch (error) {
|
|||
|
console.error(`❌ Failed to reset device worker ${deviceId}:`, (error as Error).message);
|
|||
|
deviceWorker.status = "error";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private async forwardActionToDevice(deviceId: string, action: any): Promise<void> {
|
|||
|
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<void> {
|
|||
|
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<void> {
|
|||
|
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<void>((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<DeviceMainMessage>) => {
|
|||
|
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<void> {
|
|||
|
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<void>((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<DeviceMainMessage>) => {
|
|||
|
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<void> {
|
|||
|
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<void> {
|
|||
|
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<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 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<void> => {
|
|||
|
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();
|
|||
|
}
|