837 lines
30 KiB
TypeScript
837 lines
30 KiB
TypeScript
![]() |
/// <reference lib="deno.worker" />
|
|||
|
|
|||
|
import {
|
|||
|
MasterController,
|
|||
|
AgvId,
|
|||
|
ClientOptions,
|
|||
|
Topic,
|
|||
|
Order,
|
|||
|
State,
|
|||
|
Headerless,
|
|||
|
InstantActions,
|
|||
|
BlockingType,
|
|||
|
Connection,
|
|||
|
Factsheet,
|
|||
|
} from "vda-5050-lib";
|
|||
|
import { v4 as uuidv4 } from "npm:uuid";
|
|||
|
import { createWorkerEventHelper } from "./worker_event_helper.ts";
|
|||
|
|
|||
|
// 创建事件助手
|
|||
|
const eventHelper = createWorkerEventHelper("vdaWorker");
|
|||
|
|
|||
|
console.log("VDA 5050 Worker initialized");
|
|||
|
|
|||
|
// 预注册设备列表(后续主程序可动态更新)
|
|||
|
const preRegisteredDevices: AgvId[] = [
|
|||
|
];
|
|||
|
|
|||
|
// 用于保存所有设备状态及动态更新设备列表
|
|||
|
const currentDevices: Map<
|
|||
|
string,
|
|||
|
{ agvId: AgvId; lastSeen: number; isOnline: boolean; state?: State }
|
|||
|
> = new Map();
|
|||
|
|
|||
|
// 内部方法:生成设备唯一 key
|
|||
|
function getDeviceKey(agvId: AgvId) {
|
|||
|
return `${agvId.manufacturer}-${agvId.serialNumber}`;
|
|||
|
}
|
|||
|
|
|||
|
// 先将预注册设备保存到 currentDevices(初始状态为离线)
|
|||
|
preRegisteredDevices.forEach((device) => {
|
|||
|
const key = getDeviceKey(device);
|
|||
|
currentDevices.set(key, {
|
|||
|
agvId: device,
|
|||
|
lastSeen: 0,
|
|||
|
isOnline: false,
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
// 用于保存已经订阅状态的设备,避免重复订阅
|
|||
|
const subscribedDevices: Set<string> = new Set();
|
|||
|
|
|||
|
// 全局保存 MasterController 实例
|
|||
|
let masterController: MasterController | null = null;
|
|||
|
|
|||
|
// 从初始化参数读取的接口名和制造商
|
|||
|
let interfaceNameValue = "oagv";
|
|||
|
let manufacturerValue = "gateway";
|
|||
|
// 标记 Master Controller 是否启动完成
|
|||
|
let clientStarted = false;
|
|||
|
|
|||
|
/**
|
|||
|
* 当收到 init 消息后,从启动参数中提取 MQTT broker 地址,从而构造客户端选项并启动控制器
|
|||
|
*/
|
|||
|
function initializeControllerWithOptions(brokerUrl: string, iValue: string) {
|
|||
|
// Validate input parameters
|
|||
|
if (!brokerUrl || !iValue) {
|
|||
|
console.error("❌ Invalid initialization parameters:");
|
|||
|
console.error("brokerUrl:", brokerUrl);
|
|||
|
console.error("interfaceName:", iValue);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Reset previous state
|
|||
|
clientStarted = false;
|
|||
|
if (masterController) {
|
|||
|
try {
|
|||
|
masterController.stop().catch(() => {});
|
|||
|
} catch (e) {
|
|||
|
// Ignore stop errors
|
|||
|
}
|
|||
|
masterController = null;
|
|||
|
}
|
|||
|
|
|||
|
const clientOptions: ClientOptions = {
|
|||
|
interfaceName: iValue,
|
|||
|
transport: {
|
|||
|
// 使用启动参数传入的 MQTT server 地址
|
|||
|
brokerUrl: brokerUrl,
|
|||
|
heartbeat:5,
|
|||
|
reconnectPeriod:1000,
|
|||
|
connectTimeout:5000,
|
|||
|
},
|
|||
|
vdaVersion: "2.0.0",
|
|||
|
};
|
|||
|
|
|||
|
console.log(`🚀 正在初始化VDA 5050 Master Controller...`);
|
|||
|
console.log(`📡 MQTT Broker: ${brokerUrl}`);
|
|||
|
console.log(`🏷️ Interface: ${iValue}`);
|
|||
|
console.log(`🏭 Manufacturer: ${manufacturerValue}`);
|
|||
|
|
|||
|
// Test MQTT broker connectivity
|
|||
|
console.log(`🔍 Testing MQTT broker connectivity...`);
|
|||
|
try {
|
|||
|
const url = new URL(brokerUrl);
|
|||
|
console.log(`📍 Broker host: ${url.hostname}, port: ${url.port || 1883}`);
|
|||
|
} catch (urlError) {
|
|||
|
console.error("❌ Invalid broker URL format:", urlError);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
masterController = new MasterController(clientOptions, {});
|
|||
|
console.log("📦 MasterController instance created successfully");
|
|||
|
} catch (error) {
|
|||
|
console.error("❌ Failed to create MasterController instance:", error);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Add timeout to prevent hanging
|
|||
|
Promise.race([
|
|||
|
masterController.start(),
|
|||
|
new Promise((_, reject) =>
|
|||
|
setTimeout(() => reject(new Error("Master controller start timeout after 30 seconds")), 30000)
|
|||
|
)
|
|||
|
])
|
|||
|
.then(() => {
|
|||
|
clientStarted = true;
|
|||
|
console.log("✅ VDA 5050 master controller started successfully");
|
|||
|
self.postMessage({ type: "started" });
|
|||
|
|
|||
|
// 跟踪所有AGV连接状态
|
|||
|
masterController!.trackAgvs((trackAgvId, connectionState, timestamp) => {
|
|||
|
const key = getDeviceKey(trackAgvId);
|
|||
|
// 如果设备不在设备列表中,则不予上线
|
|||
|
// console.log("->", key, connectionState);
|
|||
|
// if (!currentDevices.has(key)) {
|
|||
|
// console.warn(
|
|||
|
// `收到未知设备 ${trackAgvId.manufacturer}/${trackAgvId.serialNumber} 的状态消息,忽略上线。`
|
|||
|
// );
|
|||
|
// return;
|
|||
|
// }
|
|||
|
const ts = Number(timestamp);
|
|||
|
const lastSeen = isNaN(ts) ? Date.now() : ts;
|
|||
|
// 自动添加新设备到currentDevices,无需预先配置
|
|||
|
let record = currentDevices.get(key);
|
|||
|
if (!record) {
|
|||
|
// 动态添加新发现的设备
|
|||
|
record = { agvId: trackAgvId, lastSeen: 0, isOnline: false };
|
|||
|
currentDevices.set(key, record);
|
|||
|
console.log(`🆕 自动添加新设备: ${trackAgvId.manufacturer}/${trackAgvId.serialNumber}`);
|
|||
|
}
|
|||
|
const wasOffline = !record.isOnline;
|
|||
|
// 无条件置为 ONLINE 并更新最后更新时间
|
|||
|
currentDevices.set(key, { ...record, lastSeen, isOnline: true });
|
|||
|
// 发送上线状态更新信息
|
|||
|
self.postMessage({
|
|||
|
type: "connectionState",
|
|||
|
data: { agvId: trackAgvId, state: "ONLINE", timestamp: lastSeen },
|
|||
|
});
|
|||
|
if (wasOffline) {
|
|||
|
// console.log(
|
|||
|
// `设备 ${trackAgvId.manufacturer}/${trackAgvId.serialNumber} 新上线`
|
|||
|
// );
|
|||
|
// 额外通知一次"新上线"
|
|||
|
self.postMessage({
|
|||
|
type: "connectionState",
|
|||
|
data: { agvId: trackAgvId, state: "ONLINE", timestamp: lastSeen },
|
|||
|
});
|
|||
|
}
|
|||
|
// 若设备首次出现,则订阅其状态更新
|
|||
|
if (!subscribedDevices.has(key)) {
|
|||
|
subscribedDevices.add(key);
|
|||
|
try {
|
|||
|
// 使用 trackAgvId 本身作为 AgvId 订阅
|
|||
|
masterController!.subscribe(
|
|||
|
Topic.State,
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: trackAgvId.serialNumber },
|
|||
|
(state: State) => {
|
|||
|
const subKey = getDeviceKey(trackAgvId);
|
|||
|
// 自动添加新设备,无需预先配置
|
|||
|
let existing = currentDevices.get(subKey);
|
|||
|
if (!existing) {
|
|||
|
existing = { agvId: trackAgvId, lastSeen: 0, isOnline: false };
|
|||
|
currentDevices.set(subKey, existing);
|
|||
|
console.log(`🆕 状态订阅中自动添加新设备: ${trackAgvId.manufacturer}/${trackAgvId.serialNumber}`);
|
|||
|
}
|
|||
|
const wasOfflineInSub = !existing.isOnline;
|
|||
|
currentDevices.set(subKey, {
|
|||
|
...existing,
|
|||
|
lastSeen: Date.now(),
|
|||
|
state,
|
|||
|
isOnline: true,
|
|||
|
});
|
|||
|
if (wasOfflineInSub) {
|
|||
|
// console.log(
|
|||
|
// `设备 ${trackAgvId.manufacturer}/${trackAgvId.serialNumber} 新上线(订阅)`
|
|||
|
// );
|
|||
|
self.postMessage({
|
|||
|
type: "connectionState",
|
|||
|
data: {
|
|||
|
agvId: trackAgvId,
|
|||
|
state: "ONLINE",
|
|||
|
timestamp: Date.now(),
|
|||
|
},
|
|||
|
});
|
|||
|
}
|
|||
|
self.postMessage({
|
|||
|
type: "stateUpdate",
|
|||
|
data: {
|
|||
|
agvId: trackAgvId,
|
|||
|
state: state,
|
|||
|
timestamp: Date.now(),
|
|||
|
},
|
|||
|
});
|
|||
|
}
|
|||
|
);
|
|||
|
// Subscribe to Factsheet topic
|
|||
|
masterController!.subscribe(
|
|||
|
Topic.Factsheet,
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: trackAgvId.serialNumber },
|
|||
|
(factsheet: Factsheet) => {
|
|||
|
// console.log("收到 factsheet 消息", factsheet);
|
|||
|
self.postMessage({
|
|||
|
type: "factsheet",
|
|||
|
data: { agvId: trackAgvId, factsheet, timestamp: Date.now() },
|
|||
|
});
|
|||
|
}
|
|||
|
);
|
|||
|
// Subscribe to Connection topic
|
|||
|
masterController!.subscribe(
|
|||
|
Topic.Connection,
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: trackAgvId.serialNumber },
|
|||
|
(connection: Connection) => {
|
|||
|
self.postMessage({
|
|||
|
type: "deviceDiscovered",
|
|||
|
data: { agvId: trackAgvId, connection, timestamp: Date.now() },
|
|||
|
});
|
|||
|
}
|
|||
|
);
|
|||
|
} catch (error) {
|
|||
|
console.error(`Failed to subscribe to tracked device ${trackAgvId.manufacturer}/${trackAgvId.serialNumber}:`, error);
|
|||
|
// 移除已添加的订阅标记,以便重试
|
|||
|
subscribedDevices.delete(key);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// 定时检测设备状态,超时则标记为离线
|
|||
|
const stateUpdateCycle = 5000;
|
|||
|
const offlineThreshold = stateUpdateCycle * 3;
|
|||
|
setInterval(() => {
|
|||
|
const now = Date.now();
|
|||
|
currentDevices.forEach((device, key) => {
|
|||
|
// console.log(
|
|||
|
// `设备 ${device.agvId.manufacturer}/${device.agvId.serialNumber} - lastSeen: ${device.lastSeen}, isOnline: ${device.isOnline}`
|
|||
|
// );
|
|||
|
if (now - device.lastSeen > offlineThreshold && device.isOnline) {
|
|||
|
device.isOnline = false;
|
|||
|
currentDevices.set(key, device);
|
|||
|
// console.log(
|
|||
|
// `设备 ${device.agvId.manufacturer}/${device.agvId.serialNumber} 超过 ${offlineThreshold} 毫秒未更新,标记为下线`
|
|||
|
// );
|
|||
|
}
|
|||
|
self.postMessage({
|
|||
|
type: "connectionState",
|
|||
|
data: { agvId: device.agvId, state: device.isOnline ? "ONLINE" : "OFFLINE", timestamp: now },
|
|||
|
});
|
|||
|
});
|
|||
|
}, stateUpdateCycle);
|
|||
|
})
|
|||
|
.catch((error) => {
|
|||
|
console.error("❌ Failed to start VDA 5050 master controller:");
|
|||
|
console.error("Error details:", error);
|
|||
|
console.error("Error message:", error?.message || "Unknown error");
|
|||
|
console.error("Error stack:", error?.stack || "No stack trace");
|
|||
|
|
|||
|
// Reset client state
|
|||
|
clientStarted = false;
|
|||
|
masterController = null;
|
|||
|
|
|||
|
// Schedule retry after delay
|
|||
|
console.log("🔄 Scheduling retry in 10 seconds...");
|
|||
|
setTimeout(() => {
|
|||
|
console.log("🔄 Retrying VDA 5050 master controller initialization...");
|
|||
|
initializeControllerWithOptions(brokerUrl, iValue);
|
|||
|
eventHelper.dispatchEvent("reconnect-all", {
|
|||
|
reason: "grpc-stream-failed",
|
|||
|
retryCount: 5,
|
|||
|
timestamp: Date.now()
|
|||
|
});
|
|||
|
self.postMessage({ type: "reconnect-all" });
|
|||
|
}, 10000);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 辅助函数:
|
|||
|
* 当客户端尚未启动时,延迟一段时间后重试订阅
|
|||
|
*/
|
|||
|
function subscribeWithRetry(device: AgvId, retryCount = 0) {
|
|||
|
const key = getDeviceKey(device);
|
|||
|
// console.log("订阅设备->", key);
|
|||
|
// 自动添加新设备,无需预先配置
|
|||
|
if (!currentDevices.has(key)) {
|
|||
|
currentDevices.set(key, { agvId: device, lastSeen: 0, isOnline: false });
|
|||
|
console.log(`🆕 订阅时自动添加新设备: ${device.manufacturer}/${device.serialNumber}`);
|
|||
|
}
|
|||
|
if (!clientStarted) {
|
|||
|
if (retryCount < 50000) {
|
|||
|
// console.warn("Client not started, retry subscribing after delay...", device);
|
|||
|
setTimeout(() => {
|
|||
|
subscribeWithRetry(device, retryCount + 1);
|
|||
|
}, 3000);
|
|||
|
} else {
|
|||
|
console.error("订阅失败:超过最大重试次数", device);
|
|||
|
}
|
|||
|
return;
|
|||
|
}
|
|||
|
if (!subscribedDevices.has(key)) {
|
|||
|
subscribedDevices.add(key);
|
|||
|
try {
|
|||
|
// 双重检查客户端状态
|
|||
|
if (!clientStarted || !masterController) {
|
|||
|
subscribedDevices.delete(key);
|
|||
|
setTimeout(() => {
|
|||
|
subscribeWithRetry(device, retryCount + 1);
|
|||
|
}, 3000);
|
|||
|
return;
|
|||
|
}
|
|||
|
// Subscribe to State topic
|
|||
|
masterController!.subscribe(
|
|||
|
Topic.State,
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: device.serialNumber },
|
|||
|
(state: State) => {
|
|||
|
const subKey = getDeviceKey(device);
|
|||
|
// 自动添加新设备,无需预先配置
|
|||
|
let existing = currentDevices.get(subKey);
|
|||
|
if (!existing) {
|
|||
|
existing = { agvId: device, lastSeen: 0, isOnline: false };
|
|||
|
currentDevices.set(subKey, existing);
|
|||
|
console.log(`🆕 状态更新中自动添加新设备: ${device.manufacturer}/${device.serialNumber}`);
|
|||
|
}
|
|||
|
const wasOfflineInSub = !existing.isOnline;
|
|||
|
currentDevices.set(subKey, {
|
|||
|
...existing,
|
|||
|
lastSeen: Date.now(),
|
|||
|
state,
|
|||
|
isOnline: true,
|
|||
|
});
|
|||
|
if (wasOfflineInSub) {
|
|||
|
self.postMessage({
|
|||
|
type: "connectionState",
|
|||
|
data: { agvId: device, state: "ONLINE", timestamp: Date.now() },
|
|||
|
});
|
|||
|
}
|
|||
|
self.postMessage({
|
|||
|
type: "stateUpdate",
|
|||
|
data: { agvId: device, state, timestamp: Date.now() },
|
|||
|
});
|
|||
|
}
|
|||
|
);
|
|||
|
// Subscribe to Factsheet topic
|
|||
|
masterController!.subscribe(
|
|||
|
Topic.Factsheet,
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: device.serialNumber },
|
|||
|
(factsheet: Factsheet) => {
|
|||
|
// console.log("收到 factsheet 消息", factsheet);
|
|||
|
self.postMessage({
|
|||
|
type: "factsheet",
|
|||
|
data: { agvId: device, factsheet, timestamp: Date.now() },
|
|||
|
});
|
|||
|
}
|
|||
|
);
|
|||
|
// Subscribe to Connection topic
|
|||
|
masterController!.subscribe(
|
|||
|
Topic.Connection,
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: device.serialNumber },
|
|||
|
(connection: Connection) => {
|
|||
|
self.postMessage({
|
|||
|
type: "deviceDiscovered",
|
|||
|
data: { agvId: device, connection, timestamp: Date.now() },
|
|||
|
});
|
|||
|
}
|
|||
|
);
|
|||
|
} catch (error) {
|
|||
|
console.error(`Failed to subscribe to device ${device.manufacturer}/${device.serialNumber}:`, error);
|
|||
|
// 移除已添加的订阅标记,以便重试
|
|||
|
subscribedDevices.delete(key);
|
|||
|
// 如果客户端未启动,重新调度重试
|
|||
|
if (!clientStarted) {
|
|||
|
setTimeout(() => {
|
|||
|
subscribeWithRetry(device, 0);
|
|||
|
}, 5000);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 处理来自主线程的消息
|
|||
|
self.onmessage = async (event) => {
|
|||
|
const message = event.data;
|
|||
|
// console.log("Received message from main thread:", message);
|
|||
|
|
|||
|
// 如果收到 init 消息,从启动参数中传入 MQTT server 地址
|
|||
|
if (message.type === "init") {
|
|||
|
// 从主线程传入初始化参数
|
|||
|
// data: {
|
|||
|
// brokerUrl: config.mqtt.brokerUrl,
|
|||
|
// interfaceName: config.interfaceName,
|
|||
|
// manufacturer: config.manufacturer,
|
|||
|
// instanceId: config.instanceId
|
|||
|
// },
|
|||
|
console.log("event", message);
|
|||
|
const { brokerUrl, interfaceName, manufacturer, instanceId } = message.data;
|
|||
|
console.log(`init params → brokerUrl: ${brokerUrl}, interfaceName: ${interfaceName}, manufacturer: ${manufacturer}, instanceId: ${instanceId}`);
|
|||
|
manufacturerValue = instanceId;
|
|||
|
interfaceNameValue = interfaceName;
|
|||
|
initializeControllerWithOptions(brokerUrl, interfaceNameValue);
|
|||
|
}
|
|||
|
|
|||
|
// 处理单个设备移除
|
|||
|
if (message.type === "removeDevice") {
|
|||
|
const { manufacturer, serialNumber } = message.data;
|
|||
|
const deviceKey = getDeviceKey({ manufacturer, serialNumber });
|
|||
|
|
|||
|
if (currentDevices.has(deviceKey)) {
|
|||
|
// 移除设备
|
|||
|
currentDevices.delete(deviceKey);
|
|||
|
subscribedDevices.delete(deviceKey);
|
|||
|
|
|||
|
console.log(`🗑️ 已移除设备: ${manufacturer}/${serialNumber}`);
|
|||
|
|
|||
|
// 通知主线程设备已移除
|
|||
|
self.postMessage({
|
|||
|
type: "deviceRemoved",
|
|||
|
data: {
|
|||
|
manufacturer,
|
|||
|
serialNumber,
|
|||
|
deviceKey,
|
|||
|
remainingDevices: currentDevices.size
|
|||
|
}
|
|||
|
});
|
|||
|
} else {
|
|||
|
console.log(`⚠️ 设备 ${manufacturer}/${serialNumber} 不存在,无法移除`);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 动态更新设备列表(主程序从配置文件发送更新消息)
|
|||
|
if (message.type === "updateDeviceList") {
|
|||
|
const newDeviceList: AgvId[] = message.data;
|
|||
|
console.log(`🔄 VDA Worker 收到设备列表更新,包含 ${newDeviceList.length} 个设备`);
|
|||
|
|
|||
|
// 构造新设备的 Key 集合
|
|||
|
const newKeys = new Set(newDeviceList.map(getDeviceKey));
|
|||
|
let addedCount = 0;
|
|||
|
let updatedCount = 0;
|
|||
|
|
|||
|
// 遍历新设备列表,新增或更新记录,并进行状态订阅
|
|||
|
newDeviceList.forEach((device) => {
|
|||
|
const key = getDeviceKey(device);
|
|||
|
if (!currentDevices.has(key)) {
|
|||
|
currentDevices.set(key, { agvId: device, lastSeen: 0, isOnline: false });
|
|||
|
console.log(`➕ 新增设备: ${device.manufacturer}/${device.serialNumber}`);
|
|||
|
addedCount++;
|
|||
|
} else {
|
|||
|
// 更新 agvId 信息(如果有必要)
|
|||
|
const record = currentDevices.get(key)!;
|
|||
|
record.agvId = device;
|
|||
|
currentDevices.set(key, record);
|
|||
|
console.log(`🔄 更新设备: ${device.manufacturer}/${device.serialNumber}`);
|
|||
|
updatedCount++;
|
|||
|
}
|
|||
|
// 根据设备列表订阅对应的状态更新(重试订阅,直到客户端启动)
|
|||
|
subscribeWithRetry(device);
|
|||
|
});
|
|||
|
|
|||
|
// 对于 currentDevices 中存在但不在最新列表中的设备,置为离线
|
|||
|
let removedCount = 0;
|
|||
|
currentDevices.forEach((device, key) => {
|
|||
|
if (!newKeys.has(key)) {
|
|||
|
device.isOnline = false;
|
|||
|
currentDevices.set(key, device);
|
|||
|
console.log(`⏸️ 设备离线: ${device.agvId.manufacturer}/${device.agvId.serialNumber}`);
|
|||
|
removedCount++;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
console.log(`✅ 设备列表更新完成: 新增 ${addedCount}, 更新 ${updatedCount}, 离线 ${removedCount}`);
|
|||
|
console.log(`📊 当前管理设备总数: ${currentDevices.size}`);
|
|||
|
|
|||
|
// 通知主线程最新设备列表情况
|
|||
|
self.postMessage({
|
|||
|
type: "deviceListUpdated",
|
|||
|
data: {
|
|||
|
total: currentDevices.size,
|
|||
|
added: addedCount,
|
|||
|
updated: updatedCount,
|
|||
|
removed: removedCount,
|
|||
|
devices: Array.from(currentDevices.values()).map(d => ({
|
|||
|
manufacturer: d.agvId.manufacturer,
|
|||
|
serialNumber: d.agvId.serialNumber,
|
|||
|
isOnline: d.isOnline
|
|||
|
}))
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
if (message.type === "orderForwarded") {
|
|||
|
// Check if client is started before processing orders
|
|||
|
if (!clientStarted || !masterController) {
|
|||
|
console.warn("❌ VDA client not started yet, ignoring order request");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// console.log("收到 AGV 订单消息1", message);
|
|||
|
// 解构出 agvId 串号和 order 对象
|
|||
|
const { agvId: agvSerial, order: msg } = message.data as { agvId: string; order: any };
|
|||
|
// Rebuild order payload as Headerless<Order>
|
|||
|
const order: Headerless<Order> = {
|
|||
|
orderId: msg.orderId,
|
|||
|
orderUpdateId: msg.orderUpdateId || 0,
|
|||
|
nodes: msg.nodes.map((node: any, idx: number) => ({
|
|||
|
nodeId: node.nodeId,
|
|||
|
sequenceId: idx * 2,
|
|||
|
released: node.released ?? true,
|
|||
|
nodePosition: node.nodePosition,
|
|||
|
actions: node.actions || []
|
|||
|
})),
|
|||
|
edges: []
|
|||
|
};
|
|||
|
if (msg.nodes.length > 1) {
|
|||
|
for (let i = 0; i < msg.nodes.length - 1; i++) {
|
|||
|
order.edges.push({
|
|||
|
edgeId: msg.edges?.[i]?.edgeId || `edge${i}to${i + 1}`,
|
|||
|
sequenceId: i * 2 + 1,
|
|||
|
startNodeId: msg.nodes[i].nodeId,
|
|||
|
endNodeId: msg.nodes[i + 1].nodeId,
|
|||
|
released: msg.edges?.[i]?.released ?? true,
|
|||
|
actions: msg.edges?.[i]?.actions || []
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
let devId: any = undefined;
|
|||
|
// console.log("检查设备", currentDevices, msg.agvId);
|
|||
|
currentDevices.forEach((device, key) => {
|
|||
|
// console.log("检查设备", device, key, device.agvId.serialNumber, agvSerial);
|
|||
|
if (device.agvId.serialNumber === agvSerial) {
|
|||
|
devId = device.agvId;
|
|||
|
}
|
|||
|
});
|
|||
|
if (devId) {
|
|||
|
// console.log("收到 AGV 订单消息2", devId);
|
|||
|
try {
|
|||
|
// console.log("---->",{ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, order);
|
|||
|
await masterController!.assignOrder({ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, order, {
|
|||
|
onOrderProcessed: (err, canceled, active, ctx) => {
|
|||
|
if (err) {
|
|||
|
console.error("Order 被拒绝", err);
|
|||
|
} else if (canceled) {
|
|||
|
console.log("Order 被取消", ctx.order);
|
|||
|
} else if (active) {
|
|||
|
console.log("Order 正在执行", ctx.order);
|
|||
|
} else {
|
|||
|
console.log("Order 完成", ctx.order);
|
|||
|
}
|
|||
|
},
|
|||
|
onNodeTraversed: (node, nextEdge, nextNode, ctx) => {
|
|||
|
console.log("节点遍历完成", node);
|
|||
|
},
|
|||
|
onEdgeTraversing: (edge, start, end, stateChanges, count, ctx) => {
|
|||
|
console.log("开始路径", edge);
|
|||
|
},
|
|||
|
onEdgeTraversed: (edge, start, end, ctx) => {
|
|||
|
console.log("路径遍历完成", edge);
|
|||
|
},
|
|||
|
onActionStateChanged: (actionState, error) => {
|
|||
|
console.log("Action 状态变化", actionState, error || "");
|
|||
|
}
|
|||
|
});
|
|||
|
} catch (err) {
|
|||
|
console.error("assignOrder 异常", err);
|
|||
|
}
|
|||
|
} // end if(agvId)
|
|||
|
} // end if(message.type === "orderForwarded")
|
|||
|
// 2) 收到 InstantActions 转发
|
|||
|
if (message.type === "instantActionsForwarded") {
|
|||
|
// Check if client is started before processing instant actions
|
|||
|
if (!clientStarted || !masterController) {
|
|||
|
console.warn("❌ VDA client not started yet, ignoring instant actions request");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// console.log("收到 AGV 即时动作消息", message);
|
|||
|
const msg: any = message.data;
|
|||
|
// const actions: InstantActions = msg.actions;
|
|||
|
const { agvId, actions } = msg as {
|
|||
|
agvId: string;
|
|||
|
actions: Array<{
|
|||
|
actionType: string; /* …其它可能的字段… */
|
|||
|
actionParameters: Array<{ key: string; value: string }>;
|
|||
|
actionDescription: string;
|
|||
|
actionId: string;
|
|||
|
blockingType: string;
|
|||
|
}>;
|
|||
|
};
|
|||
|
// console.log("收到 AGV 即时动作消息", agvId, actions);
|
|||
|
|
|||
|
let devId: any = undefined;
|
|||
|
currentDevices.forEach((device, key) => {
|
|||
|
if (device.agvId.serialNumber === msg.agvId) {
|
|||
|
devId = device.agvId;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (devId) {
|
|||
|
// console.log("收到 AGV 即时动作消息", msg);
|
|||
|
try {
|
|||
|
const headerless: Headerless<InstantActions> = {
|
|||
|
actions: actions.map(a => ({
|
|||
|
actionType: a.actionType, // 必填
|
|||
|
actionId: a.actionId, // 必填
|
|||
|
blockingType: a.blockingType === "HARD" ? BlockingType.Hard : BlockingType.None, // 必填,使用枚举
|
|||
|
actionParameters: a.actionParameters || [], // 使用原始的actionParameters或空数组
|
|||
|
actionDescription: "action parameters", // 可选
|
|||
|
}))
|
|||
|
};
|
|||
|
// console.log("=====>",headerless);
|
|||
|
await masterController!.initiateInstantActions({ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, headerless, {
|
|||
|
onActionStateChanged: (actionState, withError, action, agvId, state) => console.log("Instant action state changed: %o %o %o", actionState, withError, action),
|
|||
|
onActionError: (error, action, agvId, state) => console.log("Instant action error: %o %o %o", error, action, state),
|
|||
|
});
|
|||
|
} catch (err) {
|
|||
|
console.error("initiateInstantActions 异常", err);
|
|||
|
}
|
|||
|
} // end if(agvId)
|
|||
|
} // end if(message.type === "instantActionsForwarded")
|
|||
|
|
|||
|
// 处理发送订单请求
|
|||
|
if (message.type === "sendOrder") {
|
|||
|
// Check if client is started before processing orders
|
|||
|
if (!clientStarted || !masterController) {
|
|||
|
console.warn("❌ VDA client not started yet, ignoring send order request");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
const order: Headerless<Order> = {
|
|||
|
orderId: message.orderId || masterController!.createUuid(),
|
|||
|
orderUpdateId: 0,
|
|||
|
nodes: message.nodes.map((node: any, index: number) => ({
|
|||
|
nodeId: node.nodeId,
|
|||
|
sequenceId: index * 2,
|
|||
|
released: true,
|
|||
|
nodePosition: node.nodePosition,
|
|||
|
actions: node.actions || [],
|
|||
|
})),
|
|||
|
edges: [],
|
|||
|
};
|
|||
|
|
|||
|
if (message.nodes.length > 1) {
|
|||
|
for (let i = 0; i < message.nodes.length - 1; i++) {
|
|||
|
order.edges.push({
|
|||
|
edgeId: `edge${i}to${i + 1}`,
|
|||
|
sequenceId: i * 2 + 1,
|
|||
|
startNodeId: message.nodes[i].nodeId,
|
|||
|
endNodeId: message.nodes[i + 1].nodeId,
|
|||
|
released: true,
|
|||
|
actions: [],
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 使用预注册列表中的第一个设备发送订单
|
|||
|
let devId: any = undefined;
|
|||
|
// currentDevices.forEach((device, key) => {
|
|||
|
// if ( device.agvId.serialNumber === 'ZKG-0') {
|
|||
|
// devId = device.agvId;
|
|||
|
// }
|
|||
|
// });
|
|||
|
if (devId) {
|
|||
|
await masterController!.assignOrder({ manufacturer: manufacturerValue, serialNumber: devId.serialNumber }, order, {
|
|||
|
onOrderProcessed: (withError, byCancelation, active, ctx) => {
|
|||
|
console.log("Order processed", { withError, byCancelation, active });
|
|||
|
self.postMessage({
|
|||
|
type: "orderCompleted",
|
|||
|
orderId: order.orderId,
|
|||
|
withError,
|
|||
|
byCancelation,
|
|||
|
active,
|
|||
|
});
|
|||
|
},
|
|||
|
onNodeTraversed: (node, nextEdge, nextNode, ctx) => {
|
|||
|
console.log("Order node traversed:", node);
|
|||
|
self.postMessage({
|
|||
|
type: "nodeTraversed",
|
|||
|
node,
|
|||
|
nextEdge,
|
|||
|
nextNode,
|
|||
|
});
|
|||
|
},
|
|||
|
onEdgeTraversing: (
|
|||
|
edge,
|
|||
|
startNode,
|
|||
|
endNode,
|
|||
|
stateChanges,
|
|||
|
invocationCount,
|
|||
|
ctx
|
|||
|
) => {
|
|||
|
console.log("Order edge traversing:", edge);
|
|||
|
self.postMessage({
|
|||
|
type: "edgeTraversing",
|
|||
|
edge,
|
|||
|
startNode,
|
|||
|
endNode,
|
|||
|
});
|
|||
|
},
|
|||
|
onEdgeTraversed: (edge, startNode, endNode, ctx) => {
|
|||
|
console.log("Order edge traversed:", edge);
|
|||
|
self.postMessage({ type: "edgeTraversed", edge, startNode, endNode });
|
|||
|
},
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
console.log("Order assigned successfully");
|
|||
|
self.postMessage({ type: "orderSent", orderId: order.orderId });
|
|||
|
} catch (error) {
|
|||
|
console.error("Failed to send order:", error);
|
|||
|
self.postMessage({ type: "error", error: (error as Error).message });
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 处理取消订单请求
|
|||
|
if (message.type === "cancelOrder") {
|
|||
|
// 此处添加取消订单逻辑……
|
|||
|
}
|
|||
|
|
|||
|
// 处理主动请求设备列表的消息
|
|||
|
if (message.type === "discoverDevices") {
|
|||
|
const devicesList = Array.from(currentDevices.values())
|
|||
|
.filter(d => d.isOnline)
|
|||
|
.map(d => ({
|
|||
|
agvId: d.agvId,
|
|||
|
isOnline: d.isOnline,
|
|||
|
x: d.state?.agvPosition?.x,
|
|||
|
y: d.state?.agvPosition?.y,
|
|||
|
theta: d.state?.agvPosition?.theta,
|
|||
|
actionStatus: d.state?.actionStates,
|
|||
|
lastNodeId: d.state?.lastNodeId,
|
|||
|
lastNodeSequenceId: d.state?.lastNodeSequenceId,
|
|||
|
nodeStates: d.state?.nodeStates,
|
|||
|
edgeStates: d.state?.edgeStates,
|
|||
|
driving: d.state?.driving,
|
|||
|
errors: d.state?.errors?.map(err => ({
|
|||
|
errorType: err.errorType,
|
|||
|
errorLevel: err.errorLevel,
|
|||
|
errorDescription: err.errorDescription,
|
|||
|
errorReferences: err.errorReferences?.map((ref: any) => ({
|
|||
|
referenceKey: ref.referenceKey,
|
|||
|
referenceValue: ref.referenceValue
|
|||
|
}))
|
|||
|
})),
|
|||
|
information: d.state?.information || []
|
|||
|
}));
|
|||
|
console.log("currentDevices", JSON.stringify(Array.from(currentDevices.values()), null, 2));
|
|||
|
self.postMessage({ type: "devicesList", data: devicesList });
|
|||
|
}
|
|||
|
|
|||
|
// 处理factsheet请求
|
|||
|
if (message.type === "factsheetRequest") {
|
|||
|
// Check if client is started before processing factsheet requests
|
|||
|
if (!clientStarted || !masterController) {
|
|||
|
console.warn("❌ VDA client not started yet, ignoring factsheet request");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
const { agvId } = message.data || {};
|
|||
|
if (!agvId) {
|
|||
|
console.warn("❌ No agvId provided for factsheet request");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
console.log(`📋 Processing factsheet request for AGV: ${agvId}`);
|
|||
|
|
|||
|
// Find the device in currentDevices
|
|||
|
let targetDevice: any = undefined;
|
|||
|
currentDevices.forEach((device, key) => {
|
|||
|
if (device.agvId.serialNumber === agvId) {
|
|||
|
targetDevice = device.agvId;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (targetDevice) {
|
|||
|
try {
|
|||
|
// Request factsheet using instant action
|
|||
|
const factsheetAction: Headerless<InstantActions> = {
|
|||
|
actions: [{
|
|||
|
actionType: "factsheetRequest",
|
|||
|
actionId: `factsheet_${Date.now()}`,
|
|||
|
blockingType: BlockingType.None,
|
|||
|
actionParameters: [],
|
|||
|
actionDescription: "Request device factsheet"
|
|||
|
}]
|
|||
|
};
|
|||
|
|
|||
|
await masterController!.initiateInstantActions(
|
|||
|
{ manufacturer: manufacturerValue, serialNumber: targetDevice.serialNumber },
|
|||
|
factsheetAction,
|
|||
|
{
|
|||
|
onActionStateChanged: (actionState, withError, action, agvId, state) => {
|
|||
|
console.log("Factsheet action state changed:", actionState, withError ? "with error" : "success");
|
|||
|
},
|
|||
|
onActionError: (error, action, agvId, state) => {
|
|||
|
console.error("Factsheet action error:", error);
|
|||
|
},
|
|||
|
}
|
|||
|
);
|
|||
|
|
|||
|
console.log(`✅ Factsheet request sent for AGV: ${agvId}`);
|
|||
|
} catch (err) {
|
|||
|
console.error("❌ Failed to send factsheet request:", err);
|
|||
|
}
|
|||
|
} else {
|
|||
|
console.warn(`⚠️ AGV ${agvId} not found in current devices`);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 处理 shutdown 消息
|
|||
|
if (message === "shutdown" || message.type === "shutdown") {
|
|||
|
console.log("收到 shutdown 消息,退出 Worker");
|
|||
|
// 此处可添加关闭逻辑
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// 在 worker 退出时关闭 Master Controller
|
|||
|
addEventListener("unload", () => {
|
|||
|
console.log("Closing VDA 5050 Worker");
|
|||
|
masterController?.stop().catch((err: Error) => console.log(err));
|
|||
|
});
|