932 lines
28 KiB
TypeScript
932 lines
28 KiB
TypeScript
// agv_worker.ts
|
||
|
||
// 捕获未处理的 promise,避免 worker 因 grpc 异常退出
|
||
globalThis.addEventListener("unhandledrejection", (event) => {
|
||
console.error("Unhandled promise rejection:", event.reason);
|
||
event.preventDefault();
|
||
});
|
||
|
||
// Deno + npm 包
|
||
import * as grpc from "npm:@grpc/grpc-js@1.12.1";
|
||
import * as protoLoader from "npm:@grpc/proto-loader";
|
||
import { delay } from "https://deno.land/std/async/delay.ts";
|
||
import { createWorkerEventHelper } from "./worker_event_helper.ts";
|
||
|
||
// 创建事件助手
|
||
const eventHelper = createWorkerEventHelper("agvWorker");
|
||
|
||
// Proto 加载
|
||
const PROTO_PATH = "./proto/vda5050.proto";
|
||
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
||
keepCase: true,
|
||
longs: String,
|
||
enums: String,
|
||
defaults: true,
|
||
oneofs: true,
|
||
});
|
||
const proto = grpc.loadPackageDefinition(packageDefinition) as any;
|
||
const vda5050 = proto.vda5050 as any;
|
||
|
||
// 运行时参数 & 设备列表
|
||
let SERVER_URL: string;
|
||
let devices: Device[] = [];
|
||
|
||
// 全局 gRPC 流 & AGV 实例映射
|
||
let stream: grpc.ClientDuplexStream<any, any> | null = null;
|
||
const agvMap = new Map<string, AgvSimulator>();
|
||
|
||
// gRPC 连接状态标记
|
||
let isGrpcConnected = false;
|
||
|
||
// 全局自增 headerId
|
||
let globalHeaderId = 1;
|
||
|
||
// 设备接口
|
||
interface Device {
|
||
serialNumber: string;
|
||
manufacturer: string;
|
||
}
|
||
|
||
// 新增 AGV 模拟器状态类型定义
|
||
interface Position {
|
||
x: number;
|
||
y: number;
|
||
theta: number;
|
||
}
|
||
|
||
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;
|
||
deviationRange: number;
|
||
localizationScore: number;
|
||
}
|
||
|
||
interface Velocity {
|
||
vx: number;
|
||
vy: number;
|
||
omega: number;
|
||
}
|
||
|
||
interface BatteryState {
|
||
batteryCharge: number;
|
||
charging: boolean;
|
||
batteryVoltage?: number;
|
||
batteryHealth?: number;
|
||
reach?: number;
|
||
}
|
||
|
||
interface ErrorState {
|
||
errorType: string;
|
||
errorDescription: string;
|
||
errorLevel: string;
|
||
}
|
||
|
||
interface SafetyState {
|
||
eStop: boolean;
|
||
fieldViolation: boolean;
|
||
}
|
||
|
||
interface ActionState {
|
||
actionId: string;
|
||
actionType: string;
|
||
actionDescription: string;
|
||
actionStatus: string;
|
||
resultDescription: string;
|
||
}
|
||
|
||
interface AgvState {
|
||
headerId: number;
|
||
timestamp: string;
|
||
version: string;
|
||
manufacturer: string;
|
||
serialNumber: string;
|
||
orderId: string;
|
||
orderUpdateId: number;
|
||
lastNodeId: string;
|
||
lastNodeSequenceId: number;
|
||
driving: boolean;
|
||
waitingForInteractionZoneRelease: boolean;
|
||
paused: boolean;
|
||
forkState?: string;
|
||
newBaseRequest: boolean;
|
||
distanceSinceLastNode: number;
|
||
operatingMode: string;
|
||
nodeStates: NodeState[];
|
||
edgeStates: EdgeState[];
|
||
agvPosition: AgvPosition;
|
||
velocity: Velocity;
|
||
loads: any[];
|
||
batteryState: BatteryState;
|
||
errors: ErrorState[];
|
||
information: any[];
|
||
safetyState: SafetyState;
|
||
actionStates: ActionState[];
|
||
}
|
||
|
||
// 建立/重建 gRPC 流
|
||
function initGrpcStream() {
|
||
try {
|
||
if (stream) {
|
||
console.log("关闭旧 gRPC 流");
|
||
try {
|
||
// 移除所有事件监听,避免它们触发重复 reconnectAll
|
||
stream.removeAllListeners();
|
||
stream.end();
|
||
} catch (_) { /* ignore */ }
|
||
stream = null;
|
||
}
|
||
|
||
const client = new vda5050.AgvManagement(
|
||
SERVER_URL,
|
||
grpc.credentials.createInsecure(),
|
||
);
|
||
stream = client.CommunicationChannel();
|
||
console.log("gRPC 流已建立");
|
||
|
||
isGrpcConnected = true;
|
||
|
||
if (!stream) {
|
||
console.error("无法创建 gRPC 流");
|
||
return false;
|
||
}
|
||
|
||
stream.on("data", (msg: any) => {
|
||
if(msg) {
|
||
const sim = agvMap.get(msg.targetAgvId);
|
||
if (!sim) {
|
||
console.warn("未知 AGV,丢弃消息:", msg.agvId);
|
||
return;
|
||
}else{
|
||
if (msg.order) {
|
||
// console.log(`${msg.targetAgvId} << Order`, msg);
|
||
sim.handleOrder(msg.order);
|
||
} else if (msg.instantActions) {
|
||
const acts = msg.instantActions.instantActions || [];
|
||
// console.log(`${msg.targetAgvId} << InstantActions`, msg);
|
||
sim.handleInstantActions(acts);
|
||
}
|
||
}
|
||
}else{
|
||
console.warn("AGV 消息为空,丢弃消息:", msg.agvId);
|
||
return;
|
||
}
|
||
});
|
||
|
||
stream.on("error", async (err: any) => {
|
||
console.error("gRPC 流错误,全体重连");
|
||
isGrpcConnected = false;
|
||
stream = null; // 清空引用,防止重复使用失效的流
|
||
|
||
// 发送 gRPC 连接丢失事件
|
||
eventHelper.dispatchEvent("grpc-connection-lost", {
|
||
error: err.message || "unknown error",
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
await reconnectAll();
|
||
});
|
||
|
||
// stream.on("end", async () => {
|
||
// console.warn("gRPC 流结束,全体重连");
|
||
// isGrpcConnected = false;
|
||
// stream = null; // 清空引用,防止重复使用失效的流
|
||
|
||
// // 发送 gRPC 连接结束事件
|
||
// eventHelper.dispatchEvent("grpc-connection-ended", {
|
||
// timestamp: Date.now()
|
||
// });
|
||
|
||
// await reconnectAll();
|
||
// });
|
||
|
||
return true;
|
||
} catch (err) {
|
||
console.error("gRPC 流初始化失败:", err);
|
||
stream = null;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 安全发送消息到流
|
||
function sendToStream(msg: any): boolean {
|
||
if (!stream || !isGrpcConnected) {
|
||
console.warn("无法发送消息,gRPC 流未初始化");
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
stream.write(msg);
|
||
return true;
|
||
} catch (err) {
|
||
console.error("发送消息失败:", err);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 重连:停所有定时器,延迟后重建流,重发 Connection 并重启状态上报
|
||
async function reconnectAll() {
|
||
// 停止所有状态更新
|
||
agvMap.forEach((s) => s.stopStatusUpdates());
|
||
|
||
// 等待一段时间再重连
|
||
await delay(500);
|
||
|
||
// 尝试重建流
|
||
let retryCount = 0;
|
||
const maxRetries = 5;
|
||
|
||
while (retryCount < maxRetries) {
|
||
console.log(`正在尝试重建 gRPC 流 等待次数 (${retryCount + 1}/${maxRetries})...`);
|
||
// if (initGrpcStream()) {
|
||
// console.log("gRPC 流重建成功");
|
||
// break;
|
||
// }
|
||
|
||
retryCount++;
|
||
await delay(100 * retryCount); // 递增等待时间
|
||
}
|
||
|
||
if (!stream) {
|
||
console.error(`无法重建 gRPC 流,已达到最大重试等待次数 ${maxRetries}`);
|
||
// 使用全局事件系统发送重连请求
|
||
eventHelper.dispatchEvent("reconnect-all", {
|
||
reason: "grpc-stream-failed",
|
||
retryCount: maxRetries,
|
||
timestamp: Date.now()
|
||
});
|
||
self.postMessage({ type: "reconnect-all"});
|
||
await delay(10000);
|
||
return;
|
||
}
|
||
|
||
// 成功重建流,重新发送所有 Connection 并重启状态更新
|
||
for (const sim of agvMap.values()) {
|
||
// sim.sendConnection("OFFLINE"); // 主动发送 ONLINE 状态
|
||
sim.resetLastOnlineStatus();
|
||
sim.startStatusUpdates();
|
||
await delay(200);
|
||
}
|
||
}
|
||
|
||
// AGV 模拟器
|
||
class AgvSimulator {
|
||
private batteryLevel: number;
|
||
private position = { x: 0, y: 0, theta: 0 };
|
||
private orderId = "";
|
||
private orderUpdateId = 0;
|
||
private state!: AgvState;
|
||
private updateIntervalId: number | null = null;
|
||
private lastOperatingMode = "NONE";
|
||
private lastOnlineStatus = "OFFLINE";
|
||
private agvId: string;
|
||
private manufacturer: string;
|
||
private serialNumber: string;
|
||
|
||
|
||
constructor(
|
||
agvId: string,
|
||
manufacturer: string,
|
||
serialNumber: string,
|
||
) {
|
||
this.agvId = agvId;
|
||
this.manufacturer = manufacturer;
|
||
this.serialNumber = serialNumber;
|
||
// 随机初始化
|
||
this.batteryLevel = 75 + Math.random() * 25;
|
||
this.position = {
|
||
x: Math.random() * 100,
|
||
y: Math.random() * 100,
|
||
theta: Math.random() * Math.PI * 2,
|
||
};
|
||
this.initState();
|
||
}
|
||
|
||
// 构造初始 State
|
||
private initState() {
|
||
this.state = <AgvState>{
|
||
headerId: globalHeaderId++,
|
||
timestamp: new Date().toISOString(),
|
||
version: "2.0.0",
|
||
manufacturer: this.manufacturer,
|
||
serialNumber: this.serialNumber,
|
||
orderId: this.orderId,
|
||
orderUpdateId: this.orderUpdateId,
|
||
lastNodeId: "",
|
||
lastNodeSequenceId: 0,
|
||
driving: false,
|
||
waitingForInteractionZoneRelease: false,
|
||
paused: false,
|
||
forkState: undefined,
|
||
newBaseRequest: false,
|
||
distanceSinceLastNode: 0,
|
||
operatingMode: "NONE", // 初始离线
|
||
nodeStates: [],
|
||
edgeStates: [],
|
||
agvPosition: {
|
||
x: this.position.x,
|
||
y: this.position.y,
|
||
theta: this.position.theta,
|
||
mapId: "warehouse",
|
||
mapDescription: "",
|
||
positionInitialized: true,
|
||
deviationRange: 0,
|
||
localizationScore: 0.95,
|
||
},
|
||
velocity: { vx: 0, vy: 0, omega: 0 },
|
||
loads: [],
|
||
actionStates: [],
|
||
batteryState: {
|
||
batteryCharge: this.batteryLevel,
|
||
batteryVoltage: 24.5,
|
||
batteryHealth: 100,
|
||
charging: false,
|
||
reach: 0,
|
||
},
|
||
errors: [],
|
||
information: [],
|
||
safetyState: { eStop: false, fieldViolation: false },
|
||
};
|
||
}
|
||
|
||
// 上行 Connection,默认为 ONLINE
|
||
public sendConnection(state: "ONLINE" | "OFFLINE" = "OFFLINE") {
|
||
const msg = {
|
||
agvId: this.agvId,
|
||
connection: {
|
||
headerId: globalHeaderId++,
|
||
timestamp: new Date().toISOString(),
|
||
version: "2.0.0",
|
||
manufacturer: this.manufacturer,
|
||
serialNumber: this.serialNumber,
|
||
connectionState: state,
|
||
},
|
||
};
|
||
// console.log("发送 Connection 消息", msg);
|
||
if (sendToStream(msg)) {
|
||
// console.log(`${this.agvId} >> Connection(${state})`);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
public resetLastOnlineStatus() {
|
||
this.lastOnlineStatus = "OFFLINE";
|
||
}
|
||
// 定时上报 State
|
||
public startStatusUpdates() {
|
||
this.stopStatusUpdates();
|
||
this.updateIntervalId = setInterval(async () => {
|
||
// 检查在线/离线状态
|
||
this.checkOnlineStatus();
|
||
// console.log(`${this.agvId}:${this.state.operatingMode}:${this.lastOnlineStatus} 状态更新>>>>`);
|
||
if (this.lastOnlineStatus === "ONLINE") {
|
||
// console.log(`${this.agvId} 状态更新>>>>`);
|
||
this.updateAndSendState().catch(err => {
|
||
console.error(`${this.agvId} 状态更新失败:`, err);
|
||
});
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
private async updateAndSendState() {
|
||
// 1) 请求 KV,添加随机ID
|
||
const requestId = Math.random().toString(36).substring(2, 15);
|
||
self.postMessage({
|
||
type: "requestKVData",
|
||
requestId,
|
||
key: `device:${this.manufacturer}/${this.serialNumber}`,
|
||
});
|
||
}
|
||
|
||
sendStateToGrpc(data: any) {
|
||
// console.log("发送状态到 gRPC 服务器",this.agvId, this.state.serialNumber, data.agvId.serialNumber, data.state.serialNumber);
|
||
// console.log("更新状态并发送-->",this.agvId, this.state.serialNumber);
|
||
// 若请求失败则保持当前状态
|
||
if (data === null) return;
|
||
// console.log(`${this.agvId}: KV 数据获取成功`, data);
|
||
|
||
// 发送设备状态更新事件
|
||
// eventHelper.dispatchEvent("device-status-update", {
|
||
// agvId: this.agvId,
|
||
// batteryLevel: this.batteryLevel,
|
||
// operatingMode: data.state?.operatingMode || this.state.operatingMode,
|
||
// position: this.position,
|
||
// timestamp: Date.now()
|
||
// });
|
||
|
||
// 更新 headerId 和时间戳
|
||
this.state.headerId = globalHeaderId++;
|
||
this.state.timestamp = new Date(data.lastSeen).toISOString();
|
||
|
||
// 根据KV数据更新状态
|
||
if (data.state) {
|
||
// 更新操作模式
|
||
if (data.state.operatingMode) {
|
||
this.state.operatingMode = data.state.operatingMode;
|
||
this.lastOperatingMode = data.state.operatingMode;
|
||
}
|
||
|
||
// 更新电池状态
|
||
if (data.state.batteryState) {
|
||
const kvBattery = data.state.batteryState;
|
||
this.batteryLevel = kvBattery.batteryCharge !== undefined ?
|
||
kvBattery.batteryCharge : this.batteryLevel;
|
||
|
||
// 更新完整的电池状态
|
||
this.state.batteryState = {
|
||
batteryCharge: this.batteryLevel,
|
||
batteryVoltage: kvBattery.batteryVoltage || this.state.batteryState.batteryVoltage,
|
||
batteryHealth: kvBattery.batteryHealth || this.state.batteryState.batteryHealth,
|
||
charging: kvBattery.charging !== undefined ? kvBattery.charging : this.state.batteryState.charging,
|
||
reach: kvBattery.reach !== undefined ? kvBattery.reach : this.state.batteryState.reach
|
||
};
|
||
} else if (this.state.driving) {
|
||
// 如果KV中没有电池数据但AGV在移动,模拟电量消耗
|
||
this.batteryLevel = Math.max(0, this.batteryLevel - 0.05);
|
||
this.state.batteryState.batteryCharge = this.batteryLevel;
|
||
}
|
||
|
||
// 更新位置信息
|
||
if (data.state.agvPosition) {
|
||
const kvPos = data.state.agvPosition;
|
||
// 使用KV中的位置数据
|
||
this.position = {
|
||
x: kvPos.x !== undefined ? kvPos.x : this.position.x,
|
||
y: kvPos.y !== undefined ? kvPos.y : this.position.y,
|
||
theta: kvPos.theta !== undefined ? kvPos.theta : this.position.theta
|
||
};
|
||
|
||
// 更新完整的位置状态
|
||
this.state.agvPosition = {
|
||
x: this.position.x,
|
||
y: this.position.y,
|
||
theta: this.position.theta,
|
||
mapId: kvPos.mapId || this.state.agvPosition.mapId,
|
||
mapDescription: kvPos.mapDescription || this.state.agvPosition.mapDescription,
|
||
positionInitialized: kvPos.positionInitialized !== undefined ?
|
||
kvPos.positionInitialized : this.state.agvPosition.positionInitialized,
|
||
deviationRange: kvPos.deviationRange !== undefined ?
|
||
kvPos.deviationRange : this.state.agvPosition.deviationRange,
|
||
localizationScore: kvPos.localizationScore !== undefined ?
|
||
kvPos.localizationScore : this.state.agvPosition.localizationScore
|
||
};
|
||
} else if (this.state.driving) {
|
||
// 如果KV中没有位置数据但AGV在移动,模拟位置变化
|
||
this.position.x += (Math.random() - 0.5) * 0.5;
|
||
this.position.y += (Math.random() - 0.5) * 0.5;
|
||
this.position.theta += (Math.random() - 0.5) * 0.1;
|
||
|
||
this.state.agvPosition.x = this.position.x;
|
||
this.state.agvPosition.y = this.position.y;
|
||
this.state.agvPosition.theta = this.position.theta;
|
||
}
|
||
|
||
// 更新其他状态字段
|
||
if (data.state.orderId !== undefined) {
|
||
this.state.orderId = data.state.orderId;
|
||
}
|
||
if (data.state.orderUpdateId !== undefined) {
|
||
this.state.orderUpdateId = data.state.orderUpdateId;
|
||
}
|
||
if (data.state.lastNodeId !== undefined) {
|
||
this.state.lastNodeId = data.state.lastNodeId;
|
||
}
|
||
if (data.state.lastNodeSequenceId !== undefined) {
|
||
this.state.lastNodeSequenceId = data.state.lastNodeSequenceId;
|
||
}
|
||
if (data.state.driving !== undefined) {
|
||
this.state.driving = data.state.driving;
|
||
}
|
||
if (data.state.waitingForInteractionZoneRelease !== undefined) {
|
||
this.state.waitingForInteractionZoneRelease = data.state.waitingForInteractionZoneRelease;
|
||
}
|
||
if (data.state.paused !== undefined) {
|
||
this.state.paused = data.state.paused;
|
||
}
|
||
if (data.state.nodeStates) {
|
||
this.state.nodeStates = data.state.nodeStates;
|
||
}
|
||
if (data.state.edgeStates) {
|
||
this.state.edgeStates = data.state.edgeStates;
|
||
}
|
||
if (data.state.actionStates) {
|
||
this.state.actionStates = data.state.actionStates;
|
||
}
|
||
if (data.state.errors) {
|
||
this.state.errors = data.state.errors;
|
||
}
|
||
if (data.state.safetyState) {
|
||
this.state.safetyState = data.state.safetyState;
|
||
}
|
||
if (data.state.serialNumber) {
|
||
this.state.serialNumber = data.state.serialNumber;
|
||
}
|
||
if (data.state.manufacturer) {
|
||
this.state.manufacturer = data.state.manufacturer;
|
||
}
|
||
if (data.state.velocity) {
|
||
this.state.velocity = data.state.velocity;
|
||
}
|
||
if (data.state.loads) {
|
||
this.state.loads = data.state.loads;
|
||
}
|
||
if (data.state.information) {
|
||
this.state.information = data.state.information;
|
||
}
|
||
if (data.state.forkState) {
|
||
this.state.forkState = data.state.forkState;
|
||
}
|
||
if (data.state.safetyState) {
|
||
this.state.safetyState = data.state.safetyState;
|
||
}
|
||
if (data.state.actionStates) {
|
||
this.state.actionStates = data.state.actionStates;
|
||
}
|
||
if (data.state.headerId) {
|
||
this.state.headerId = data.state.headerId;
|
||
}
|
||
if (data.state.timestamp) {
|
||
this.state.timestamp = data.state.timestamp;
|
||
}
|
||
if (data.state.version) {
|
||
this.state.version = data.state.version;
|
||
}
|
||
if (data.state.operatingMode) {
|
||
this.state.operatingMode = data.state.operatingMode;
|
||
}
|
||
if (data.state.nodeStates) {
|
||
this.state.nodeStates = data.state.nodeStates;
|
||
}
|
||
if (data.state.edgeStates) {
|
||
this.state.edgeStates = data.state.edgeStates;
|
||
}
|
||
|
||
|
||
|
||
|
||
}
|
||
// 发送 State,按照 gRPC 定义的 AgvMessage 结构
|
||
const agvMessage = {
|
||
agvId: this.state.serialNumber,
|
||
state: this.state
|
||
// message_type 会由 oneof 自动处理,我们只需提供 state 字段
|
||
};
|
||
|
||
if (!sendToStream(agvMessage)) {
|
||
console.error(`${this.agvId}: 发送状态到 gRPC 服务器失败`);
|
||
}
|
||
}
|
||
|
||
public stopStatusUpdates() {
|
||
if (this.updateIntervalId != null) {
|
||
clearInterval(this.updateIntervalId);
|
||
this.updateIntervalId = null;
|
||
}
|
||
}
|
||
|
||
// 下行 Order 处理
|
||
public handleOrder(order: any) {
|
||
// 1) 更新内部状态
|
||
// console.log("=====>",order);
|
||
this.orderId = order.orderId;
|
||
this.orderUpdateId = order.orderUpdateId;
|
||
this.state.orderId = this.orderId;
|
||
this.state.orderUpdateId = this.orderUpdateId;
|
||
this.state.nodeStates = order.nodes.map((n: any, i: number) => ({
|
||
nodeId: n.nodeId,
|
||
sequenceId: n.sequenceId ?? i,
|
||
nodeDescription: n.nodeDescription ?? "",
|
||
nodePosition: n.nodePosition,
|
||
released: true,
|
||
}));
|
||
this.state.edgeStates = order.edges.map((e: any, i: number) => ({
|
||
edgeId: e.edgeId,
|
||
sequenceId: e.sequenceId ?? i,
|
||
edgeDescription: e.edgeDescription ?? "",
|
||
released: true,
|
||
trajectory: e.trajectory,
|
||
}));
|
||
this.state.driving = true;
|
||
|
||
// 2) 主动通知主线程收到新 Order
|
||
self.postMessage({
|
||
type: "orderForwarded",
|
||
agvId: this.agvId,
|
||
data: {
|
||
manufacturer: order.manufacturer,
|
||
serialNumber: order.serialNumber,
|
||
orderId: this.orderId,
|
||
orderUpdateId: this.orderUpdateId,
|
||
nodes: this.state.nodeStates,
|
||
edges: this.state.edgeStates,
|
||
},
|
||
});
|
||
}
|
||
|
||
// 下行 InstantActions 处理
|
||
public handleInstantActions(actions: any[]) {
|
||
// 1) 更新内部状态
|
||
// console.log(`${this.agvId} 收到即时动作`, actions);
|
||
actions.forEach((a) => {
|
||
switch (a.actionType) {
|
||
case "STOP":
|
||
this.state.driving = false;
|
||
break;
|
||
case "RESUME":
|
||
this.state.driving = true;
|
||
break;
|
||
case "CHARGE":
|
||
this.state.batteryState.charging = true;
|
||
break;
|
||
case "pick":
|
||
console.log("pick");
|
||
break;
|
||
case "drop":
|
||
console.log("drop");
|
||
break;
|
||
case "stopPause":
|
||
this.state.paused = true;
|
||
this.state.driving = false;
|
||
console.log("stopPause");
|
||
break;
|
||
case "startPose":
|
||
console.log("startPose");
|
||
this.state.driving = true;
|
||
break;
|
||
case "factsheetRequest":
|
||
console.log("factsheetRequest");
|
||
break;
|
||
case "instantActions":
|
||
console.log("instantActions");
|
||
break;
|
||
case "finePositioning":
|
||
console.log("finePositioning");
|
||
break;
|
||
case "startCharging":
|
||
console.log("startCharging");
|
||
break;
|
||
case "stopCharging":
|
||
console.log("stopCharging");
|
||
break;
|
||
case "initPosition":
|
||
console.log("initPosition");
|
||
break;
|
||
case "stateRequst":
|
||
console.log("stateRequst");
|
||
break;
|
||
case "logReport":
|
||
console.log("logReport");
|
||
break;
|
||
case "detectObject":
|
||
console.log("detectObject");
|
||
break;
|
||
case "waitForRtigger":
|
||
console.log("waitForRtigger");
|
||
break;
|
||
case "Standard":
|
||
console.log("Standard");
|
||
break;
|
||
case "switchMap":
|
||
console.log("switchMap");
|
||
break;
|
||
case "reloc":
|
||
console.log("reloc");
|
||
break;
|
||
case "turn":
|
||
console.log("turn");
|
||
break;
|
||
case "confirmLoc":
|
||
console.log("confirmLoc");
|
||
break;
|
||
case "cancelReloc":
|
||
console.log("cancelReloc");
|
||
break;
|
||
case "rotateAgv":
|
||
console.log("rotateAgv");
|
||
break;
|
||
case "rotateLoad":
|
||
console.log("rotateLoad");
|
||
break;
|
||
case "deviceSetup":
|
||
console.log("deviceSetup:", a);
|
||
break;
|
||
case "deviceWrite":
|
||
console.log("deviceWrite:", a);
|
||
break;
|
||
case "deviceDelete":
|
||
console.log("deviceDelete:", a);
|
||
break;
|
||
default:
|
||
console.warn(`${this.agvId} 未知即时动作`, a);
|
||
}
|
||
});
|
||
|
||
// 2) 主动通知主线程收到 InstantActions
|
||
self.postMessage({
|
||
type: "instantActionsForwarded",
|
||
agvId: this.agvId,
|
||
data: actions.map((a) => ({
|
||
actionType: a.actionType,
|
||
actionParameters: a.actionParameters,
|
||
actionDescription: a.actionDescription,
|
||
actionId: a.actionId,
|
||
blockingType: a.blockingType,
|
||
// …你需要的其它字段…
|
||
})),
|
||
});
|
||
}
|
||
|
||
sendOnlineStatusGrpc(data: any) {
|
||
// 若请求失败则保持当前状态
|
||
if (data === null) return;
|
||
// agvId: { manufacturer: "gateway", serialNumber: "ZKG-2" }
|
||
if (data.agvId.manufacturer === this.manufacturer
|
||
&& data.agvId.serialNumber === this.agvId
|
||
&& this.agvId === this.serialNumber) {
|
||
// console.log(`${this.agvId}: KV 数据请求成功`, data);
|
||
// 3) 计算是否在线
|
||
const lastSeen = data?.lastSeen
|
||
? Date.now() - new Date(data.lastSeen).getTime() < 3180 //如果上次 KV 中的 lastSeen 距今超过 3.18 秒,就算离线(无 connection)
|
||
: false;
|
||
const isOnline = lastSeen && data.state === "ONLINE";
|
||
const newMode = isOnline ? "ONLINE" : "OFFLINE";
|
||
|
||
// 只有状态变化时才发送 Connection
|
||
if (newMode !== this.lastOnlineStatus) {
|
||
this.lastOnlineStatus = newMode;
|
||
this.sendConnection(this.lastOnlineStatus as any);
|
||
}
|
||
}
|
||
}
|
||
// 检查在线/离线:从主线程取 KV,再比较时间,若状态变更则发 Connection
|
||
private async checkOnlineStatus() {
|
||
// console.log(`${this.agvId} 检查在线状态`);
|
||
try {
|
||
// 1) 请求 KV
|
||
const requestId = Math.random().toString(36).substring(2, 15);
|
||
self.postMessage({
|
||
type: "requestKVDataOnline",
|
||
requestId,
|
||
key: `device-online:${this.manufacturer}/${this.serialNumber}`,
|
||
});
|
||
} catch (err) {
|
||
console.error(`${this.agvId}: 检查在线状态失败:`, err);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 简易状态用于 UI
|
||
public getStatus() {
|
||
return {
|
||
agvId: this.agvId,
|
||
batteryCharge: this.batteryLevel,
|
||
timestamp: this.state.timestamp,
|
||
position: this.state.agvPosition,
|
||
driving: this.state.driving,
|
||
charging: this.state.batteryState.charging,
|
||
operatingMode: this.state.operatingMode,
|
||
};
|
||
}
|
||
}
|
||
|
||
// 主流程
|
||
async function main() {
|
||
console.log("agv_workermain");
|
||
|
||
// 设置事件监听器
|
||
eventHelper.addEventListener("test-event", (event) => {
|
||
console.log("🧪 AGV Worker 收到测试事件:", event);
|
||
});
|
||
|
||
eventHelper.addEventListener("grpc-reconnect", (event) => {
|
||
console.log("🔄 AGV Worker 收到 gRPC 重连指令:", event);
|
||
reconnectAll();
|
||
});
|
||
|
||
if (!SERVER_URL || devices.length === 0) {
|
||
console.error("缺少 SERVER_URL 或 无设备,无法启动");
|
||
return;
|
||
}
|
||
|
||
// 初始化 gRPC 流
|
||
if (!initGrpcStream()) {
|
||
console.error("gRPC 流初始化失败,无法启动");
|
||
return;
|
||
}
|
||
|
||
// 初始化所有 AGV 并发送上线消息
|
||
for (const dev of devices) {
|
||
const sim = new AgvSimulator(dev.serialNumber, dev.manufacturer, dev.serialNumber);
|
||
agvMap.set(dev.serialNumber, sim);
|
||
sim.sendConnection("OFFLINE"); // 发送 OFFLINE 状态
|
||
sim.startStatusUpdates();
|
||
await delay(200);
|
||
}
|
||
console.log("所有 AGV 已上线并开始状态上报");
|
||
|
||
// 发送 AGV Worker 启动完成事件
|
||
eventHelper.dispatchEvent("agv-worker-ready", {
|
||
deviceCount: devices.length,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
// 回主线程汇总状态
|
||
setInterval(() => {
|
||
const statuses = Array.from(agvMap.values()).map((s) => s.getStatus());
|
||
self.postMessage({ type: "devicesStatus", data: statuses });
|
||
}, 1000);
|
||
}
|
||
|
||
// worker 接收消息
|
||
self.addEventListener("message", (evt: MessageEvent) => {
|
||
const msg = evt.data;
|
||
if (msg.type === "init") {
|
||
SERVER_URL = msg.data.serverUrl;
|
||
self.postMessage({ type: "inited"});
|
||
console.log("设置 SERVER_URL =", SERVER_URL);
|
||
} else if (msg.type === "deviceList") {
|
||
devices = msg.data as Device[];
|
||
console.log("devices", devices[0]);
|
||
main().catch(err => console.error("主流程启动失败:", err));
|
||
} else if (msg.type === "shutdown") {
|
||
console.log("Shutdown:关闭 gRPC 流 & 停止所有 AGV");
|
||
if (stream) {
|
||
try { stream.end(); } catch (e) { /* 忽略关闭错误 */ }
|
||
}
|
||
agvMap.forEach((s) => s.stopStatusUpdates());
|
||
setTimeout(() => self.close(), 500);
|
||
} else if (msg.type === "factsheetResponse") {
|
||
|
||
// const agvMessage = {
|
||
// agvId: msg.data.agvId,
|
||
// state: msg.data.factsheet
|
||
// // message_type 会由 oneof 自动处理,我们只需提供 state 字段
|
||
// };
|
||
// message Factsheet {
|
||
// uint32 headerId = 1;
|
||
// string timestamp = 2;
|
||
// string version = 3;
|
||
// string manufacturer = 4;
|
||
// string serialNumber = 5;
|
||
// TypeSpecification typeSpecification = 6;
|
||
// PhysicalParameters physicalParameters = 7;
|
||
// ProtocolLimits protocolLimits = 8;
|
||
// ProtocolFeatures protocolFeatures = 9;
|
||
// AgvGeometry agvGeometry = 10;
|
||
// LoadSpecification loadSpecification = 11;
|
||
// VehicleConfig vehicleConfig = 12;
|
||
// }
|
||
const factsheetMsg = {
|
||
agvId: msg.data.agvId.serialNumber,
|
||
factsheet: {
|
||
headerId: globalHeaderId++,
|
||
timestamp: new Date().toISOString(),
|
||
version: "2.0.0",
|
||
manufacturer: msg.data.agvId.manufacturer,
|
||
serialNumber: msg.data.agvId.serialNumber,
|
||
typeSpecification: msg.data.factsheet.typeSpecification,
|
||
physicalParameters: msg.data.factsheet.physicalParameters,
|
||
},
|
||
};
|
||
// console.log("==>factsheetResponse", factsheetMsg);
|
||
if (!sendToStream(factsheetMsg)) {
|
||
console.error(`${msg.data.agvId}: 发送状态到 factsheet 的 gRPC 服务器失败`);
|
||
}
|
||
} else if (msg.type === "requestKVData") {
|
||
// console.log("收到 Worker requestKVData 消息,准备更新设备状态",msg.data.agvId);
|
||
if(msg && msg.data && msg.data.agvId && msg.data.agvId.serialNumber) {
|
||
const agv = agvMap.get(msg.data.agvId.serialNumber);
|
||
if (agv) {
|
||
agv.sendStateToGrpc(msg.data);
|
||
}
|
||
}
|
||
} else if (msg.type === "requestKVDataOnline") {
|
||
// console.log("收到 Worker requestKVDataOnline 消息,准备更新设备状态",msg.data.agvId);
|
||
if(msg && msg.data && msg.data.agvId && msg.data.agvId.serialNumber) {
|
||
const agv = agvMap.get(msg.data.agvId.serialNumber);
|
||
if (agv) {
|
||
agv.sendOnlineStatusGrpc(msg.data);
|
||
}
|
||
}
|
||
}
|
||
}); |