From b208c76161ee482d1f0953232a1233ba1b01fd7b Mon Sep 17 00:00:00 2001 From: luoyifan Date: Sat, 5 Jul 2025 00:25:09 +0800 Subject: [PATCH] BackendMessageReceiver / FrontendMessagePushService --- src/core/engine/Viewport.ts | 2 +- src/core/manager/BackendMessageReceiver.ts | 380 ++++++++++++++++++++++ src/core/manager/EnvManager.ts | 6 +- src/core/manager/LccMqttManager.ts | 362 --------------------- src/core/manager/WorldModel.ts | 4 +- src/core/script/LCCScript.ts | 12 +- src/editor/widgets/IWidgets.ts | 15 +- src/editor/widgets/logger/LoggerView.vue | 1 - src/editor/widgets/monitor/MonitorView.vue | 497 ++++++++++++++++++++--------- src/editor/widgets/task/TaskView.vue | 3 +- src/types/LCC.d.ts | 92 +++++- src/types/ModelTypes.ts | 1 + 12 files changed, 851 insertions(+), 524 deletions(-) create mode 100644 src/core/manager/BackendMessageReceiver.ts delete mode 100644 src/core/manager/LccMqttManager.ts diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts index 488b9a5..162c989 100644 --- a/src/core/engine/Viewport.ts +++ b/src/core/engine/Viewport.ts @@ -27,7 +27,7 @@ import { MapControls } from 'three/examples/jsm/controls/MapControls' import ModelManager from '@/core/script/ModelManager.ts' import RuntimeManager from '@/core/manager/RuntimeManager.ts' import EnvManager from '@/core/manager/EnvManager.ts' -import LccMqttManager from '@/core/manager/LccMqttManager.ts' +import BackendMessageReceiver from '@/core/manager/BackendMessageReceiver.ts' /** * 视窗对象 diff --git a/src/core/manager/BackendMessageReceiver.ts b/src/core/manager/BackendMessageReceiver.ts new file mode 100644 index 0000000..0e08e19 --- /dev/null +++ b/src/core/manager/BackendMessageReceiver.ts @@ -0,0 +1,380 @@ +import mqtt, { type IClientOptions, type IClientSubscribeOptions } from 'mqtt' +import { reactive } from 'vue' +import type Viewport from '@/core/engine/Viewport.ts' + +// 定义MQTT消息类型 +export interface MqttMessage { + topic: string + payload: Buffer | string +} + +// 定义MQTT连接状态 +enum ConnectionStatus { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + RECONNECTING = 'reconnecting', + ERROR = 'error' +} + +type ProcessFn = (message: MqttMessage, handler: BackendMessageHandler) => void + +interface HandlerNode { + processFn: ProcessFn + handler: BackendMessageHandler +} + +/** + * 服务端将变化数据推送给客户端的管理器类 + */ +export default class BackendMessageReceiver { + private projectUuid: string + private envId: number + private client: mqtt.MqttClient | null = null + private handlers: Map = new Map() + + // 状态管理 + public state = reactive({ + status: ConnectionStatus.DISCONNECTED, + isConnected: false, + lastError: '', + messageCount: 0, + subscribedTopics: [] as string[] + }) + + // 启动MQTT连接 + public async start(projectUuid: string, envId: number, config: MqttConfig): Promise { + // 如果已经连接,先断开 + if (this.client?.connected) { + await this.dispose() + } + + // 设置连接状态 + this.state.status = ConnectionStatus.CONNECTING + this.state.lastError = '' + + // 配置MQTT选项 + const options: IClientOptions = { + username: config.username, + password: config.password, + clientId: system.createUUID(), + clean: true, + connectTimeout: 4000, + reconnectPeriod: 3000, + keepalive: 60 + } + + return new Promise((resolve) => { + try { + this.client = mqtt.connect(config.websocket, options) + + // 连接成功 + this.client.on('connect', () => { + this.state.status = ConnectionStatus.CONNECTED + this.state.isConnected = true + console.log('MQTT connected') + resolve(true) + }) + + // 连接断开 + this.client.on('close', () => { + this.state.status = ConnectionStatus.DISCONNECTED + this.state.isConnected = false + console.log('MQTT disconnected') + }) + + // 重连中 + this.client.on('reconnect', () => { + this.state.status = ConnectionStatus.RECONNECTING + console.log('MQTT reconnecting...') + }) + + // 错误处理 + this.client.on('error', (error) => { + this.state.status = ConnectionStatus.ERROR + this.state.lastError = error.message + console.error('MQTT error:', error) + resolve(false) + }) + + // 消息处理 + this.client.on('message', (topic, payload) => { + this.state.messageCount++ + this.handleMessage(topic, payload) + }) + } catch (error) { + this.state.status = ConnectionStatus.ERROR + this.state.lastError = (error as Error).message + console.error('MQTT connection error:', error) + return false + } + }) + } + + // 停止MQTT连接 + public async dispose(): Promise { + return new Promise((resolve) => { + if (this.client) { + this.client.end(false, {}, () => { + this.client = null + this.state.isConnected = false + this.state.status = ConnectionStatus.DISCONNECTED + this.state.subscribedTopics = [] + console.log('MQTT stopped') + resolve() + }) + } else { + resolve() + } + }) + } + + public getTopicStringByType(type: BackendTopicType): [string, ProcessFn] { + // 根据类型生成订阅的 topic 字符串 + const projId = this.projectUuid + const envId = this.envId + switch (type) { + case 'ServerState': + return [`lcc/${projId}/${envId}/server`, this.handleServerState] + case 'ClientState': + return [`lcc/${projId}/${envId}/client`, this.handleClientState] + case 'TaskUpdate': + return [`lcc/${projId}/${envId}/task`, this.handleTaskMonitor] + case 'InvUpdate': + return [`lcc/${projId}/${envId}/inv/#`, this.handleInventoryMonitor] + case 'DeviceStatus': + return [`lcc/${projId}/${envId}/device/+/status`, this.handleDeviceStatus] + case 'DeviceAlive': + return [`lcc/${projId}/${envId}/device/+/alive`, this.handleDeviceAlive] + case 'Logs': + return [`lcc/${projId}/${envId}/log/#`, this.handleLogMonitor] + case 'Alarm': + return [`lcc/${projId}/${envId}/alarm`, this.handleAlarmMonitor] + case 'ScriptUpdate': + return [`lcc/${projId}/script`, this.handleScriptSystem] + } + + throw new Error(`Invalid topic for type ${type}`) + } + + // 订阅主题 + public subscribe(type: BackendTopicType, handler: BackendMessageHandler): void { + if (!this.client?.connected) { + throw new Error('Cannot subscribe - MQTT not connected') + } + + const [topic, processFn] = this.getTopicStringByType(type) + + // 添加消息处理器 + if (!this.handlers.has(topic)) { + this.handlers.set(topic, []) + } + this.handlers.get(topic)?.push({ processFn, handler }) + + // 如果尚未订阅该主题 + if (!this.state.subscribedTopics.includes(topic) && this.client) { + const options: IClientSubscribeOptions = { qos: 1 } + + this.client.subscribe(topic, options, (err) => { + if (err) { + console.error(`Failed to subscribe to ${topic}:`, err) + } else { + this.state.subscribedTopics = [...this.state.subscribedTopics, topic] + console.log(`Subscribed to ${topic}`) + } + }) + } + } + + // 取消订阅 + public unsubscribe(type: BackendTopicType, handler: BackendMessageHandler): void { + // if (!this.client?.connected) { + // throw new Error('Cannot unsubscribe - MQTT not connected') + // } + + const [topic, processFn] = this.getTopicStringByType(type) + if (!topic) { + throw new Error(`Invalid topic for type ${type}`) + } + + // 移除特定处理函数 + if (handler && this.handlers.has(topic)) { + const handlers = this.handlers.get(topic) || [] + const newHandlers = handlers.filter(h => (h.processFn !== processFn && h.handler !== handler)) + + if (newHandlers.length > 0) { + this.handlers.set(topic, newHandlers) + } else { + this.handlers.delete(topic) + } + } else { + this.handlers.delete(topic) + } + + // 如果没有处理器了,取消订阅 + if (!this.handlers.has(topic) && this.client) { + this.client.unsubscribe(topic, {}, (err) => { + if (err) { + console.error(`Failed to unsubscribe from ${topic}:`, err) + } else { + this.state.subscribedTopics = this.state.subscribedTopics.filter(t => t !== topic) + console.log(`Unsubscribed from ${topic}`) + } + }) + } + } + + // 处理消息分发 + private handleMessage(topic: string, payload: Buffer): void { + const message: MqttMessage = { topic, payload } + + // 1. 查找该主题的精确匹配处理器 + if (this.handlers.has(topic)) { + this.handlers.get(topic)?.forEach(node => { + + const { processFn, handler } = node + processFn(message, handler) + + }) + } + + // 2. 查找通配符匹配的处理器 + const wildcardTopics = Array.from(this.handlers.keys()).filter(t => t.includes('#') || t.includes('+')) + + for (const pattern of wildcardTopics) { + if (this.matchTopic(pattern, topic)) { + this.handlers.get(pattern)?.forEach(node => { + + const { processFn, handler } = node + processFn(message, handler) + + }) + } + } + } + + // 通配符匹配 + private matchTopic(pattern: string, topic: string): boolean { + const patternParts = pattern.split('/') + const topicParts = topic.split('/') + + for (let i = 0; i < patternParts.length; i++) { + const patternPart = patternParts[i] + + // 单级通配符 + if (patternPart === '+') { + if (i >= topicParts.length) return false + continue + } + + // 多级通配符 + if (patternPart === '#') { + return true + } + + // 精确匹配 + if (patternPart !== topicParts[i]) { + return false + } + } + + return patternParts.length === topicParts.length + } + + + // ==================== 消息处理器实现 ==================== + + private handleServerState(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('ServerState', message.topic, _.cloneDeep(data)) + + } catch (error) { + console.error('Error parsing server state:', error) + } + } + + private handleClientState(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('ClientState', message.topic, _.cloneDeep(data)) + + } catch (error) { + console.error('Error parsing client state:', error) + } + } + + private handleTaskMonitor(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('TaskUpdate', message.topic, _.cloneDeep(data)) + + } catch (error) { + console.error('Error parsing task data:', error) + } + } + + private handleInventoryMonitor(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('InvUpdate', message.topic, _.cloneDeep(data)) + + // 处理库存更新 + } catch (error) { + console.error('Error parsing inventory data:', error) + } + } + + private handleDeviceStatus(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('DeviceStatus', message.topic, _.cloneDeep(data)) + + // 处理设备状态更新 + } catch (error) { + console.error('Error parsing device status:', error) + } + } + + private handleDeviceAlive(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('DeviceAlive', message.topic, _.cloneDeep(data)) + + // 处理设备存活状态 + } catch (error) { + console.error('Error parsing device alive status:', error) + } + } + + private handleLogMonitor(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('Logs', message.topic, _.cloneDeep(data)) + + // 处理日志更新 + } catch (error) { + console.error('Error parsing log data:', error) + } + } + + private handleAlarmMonitor(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('Alarm', message.topic, _.cloneDeep(data)) + + } catch (error) { + console.error('Error parsing alarm data:', error) + } + } + + private handleScriptSystem(message: MqttMessage, handler: BackendMessageHandler) { + try { + const data = JSON.parse(message.payload.toString()) + handler('ScriptUpdate', message.topic, _.cloneDeep(data)) + + } catch (error) { + console.error('Error parsing script data:', error) + } + } +} diff --git a/src/core/manager/EnvManager.ts b/src/core/manager/EnvManager.ts index fbfa78b..f2dbd31 100644 --- a/src/core/manager/EnvManager.ts +++ b/src/core/manager/EnvManager.ts @@ -2,7 +2,6 @@ import type Viewport from '@/core/engine/Viewport.ts' import { worldModel } from '@/core/manager/WorldModel.ts' import mqtt, { type IConnackPacket, type IPublishPacket } from 'mqtt' import type { ErrorWithReasonCode } from 'mqtt/src/lib/shared.ts' -import type Cl23dObject from '@/modules/cl2/Cl23dObject.ts' import { Request } from '@ease-forge/shared' import AmrMessageManager from '@/core/manager/amr/AmrMessageManager' import { AmrMsg } from '@/core/manager/amr/AmrMessageDefine' @@ -63,7 +62,10 @@ export default class EnvManager { worldModel.state.runState.currentEnv = env try { await LCC.serverStart() - await worldModel.lccMqttManager.start(env.envConfig.frontendMqtt) + await worldModel.backendMessageReceiver.start( + worldModel.state.project_uuid, + worldModel.state.runState.currentEnvId, + env.envConfig.frontendMqtt) await LCC.loadInv() this.client = mqtt.connect(env.envConfig.mqtt.websocket, { diff --git a/src/core/manager/LccMqttManager.ts b/src/core/manager/LccMqttManager.ts deleted file mode 100644 index 91a9fdd..0000000 --- a/src/core/manager/LccMqttManager.ts +++ /dev/null @@ -1,362 +0,0 @@ -import mqtt, { type IClientOptions, type IClientSubscribeOptions } from 'mqtt' -import { reactive } from 'vue' -import type Viewport from '@/core/engine/Viewport.ts' - -// 定义MQTT消息类型 -export interface MqttMessage { - topic: string - payload: Buffer | string -} - -// 定义MQTT消息处理器 -export type MqttMessageHandler = (message: MqttMessage) => void - -// 定义MQTT连接状态 -enum ConnectionStatus { - DISCONNECTED = 'disconnected', - CONNECTING = 'connecting', - CONNECTED = 'connected', - RECONNECTING = 'reconnecting', - ERROR = 'error' -} - -/** - * 服务端将变化数据推送给客户端的管理器类 - */ -export default class LccMqttManager { - private client: mqtt.MqttClient | null = null - private handlers: Map = new Map() - - // 状态管理 - public state = reactive({ - status: ConnectionStatus.DISCONNECTED, - isConnected: false, - lastError: '', - messageCount: 0, - subscribedTopics: [] as string[] - }) - - // 启动MQTT连接 - public async start(config: MqttConfig): Promise { - // 如果已经连接,先断开 - if (this.client?.connected) { - await this.dispose() - } - - // 设置连接状态 - this.state.status = ConnectionStatus.CONNECTING - this.state.lastError = '' - - // 配置MQTT选项 - const options: IClientOptions = { - username: config.username, - password: config.password, - clientId: system.createUUID(), - clean: true, - connectTimeout: 4000, - reconnectPeriod: 3000, - keepalive: 60 - } - - return new Promise((resolve) => { - try { - this.client = mqtt.connect(config.websocket, options) - - // 连接成功 - this.client.on('connect', () => { - this.state.status = ConnectionStatus.CONNECTED - this.state.isConnected = true - console.log('MQTT connected') - resolve(true) - }) - - // 连接断开 - this.client.on('close', () => { - this.state.status = ConnectionStatus.DISCONNECTED - this.state.isConnected = false - console.log('MQTT disconnected') - }) - - // 重连中 - this.client.on('reconnect', () => { - this.state.status = ConnectionStatus.RECONNECTING - console.log('MQTT reconnecting...') - }) - - // 错误处理 - this.client.on('error', (error) => { - this.state.status = ConnectionStatus.ERROR - this.state.lastError = error.message - console.error('MQTT error:', error) - resolve(false) - }) - - // 消息处理 - this.client.on('message', (topic, payload) => { - this.state.messageCount++ - this.handleMessage(topic, payload) - }) - } catch (error) { - this.state.status = ConnectionStatus.ERROR - this.state.lastError = (error as Error).message - console.error('MQTT connection error:', error) - return false - } - }) - } - - // 停止MQTT连接 - public async dispose(): Promise { - return new Promise((resolve) => { - if (this.client) { - this.client.end(false, {}, () => { - this.client = null - this.state.isConnected = false - this.state.status = ConnectionStatus.DISCONNECTED - this.state.subscribedTopics = [] - console.log('MQTT stopped') - resolve() - }) - } else { - resolve() - } - }) - } - - // 订阅主题 - public subscribe(topic: string, handler: MqttMessageHandler): void { - if (!this.client?.connected) { - console.warn('Cannot subscribe - MQTT not connected') - return - } - - // 添加消息处理器 - if (!this.handlers.has(topic)) { - this.handlers.set(topic, []) - } - this.handlers.get(topic)?.push(handler) - - // 如果尚未订阅该主题 - if (!this.state.subscribedTopics.includes(topic)) { - const options: IClientSubscribeOptions = { qos: 1 } - - this.client.subscribe(topic, options, (err) => { - if (err) { - console.error(`Failed to subscribe to ${topic}:`, err) - } else { - this.state.subscribedTopics = [...this.state.subscribedTopics, topic] - console.log(`Subscribed to ${topic}`) - } - }) - } - } - - // 取消订阅 - public unsubscribe(topic: string, handler?: MqttMessageHandler): void { - if (!this.client?.connected) { - console.warn('Cannot unsubscribe - MQTT not connected') - return - } - - // 移除特定处理函数 - if (handler && this.handlers.has(topic)) { - const handlers = this.handlers.get(topic) || [] - const newHandlers = handlers.filter(h => h !== handler) - - if (newHandlers.length > 0) { - this.handlers.set(topic, newHandlers) - } else { - this.handlers.delete(topic) - } - } else { - this.handlers.delete(topic) - } - - // 如果没有处理器了,取消订阅 - if (!this.handlers.has(topic)) { - this.client.unsubscribe(topic, {}, (err) => { - if (err) { - console.error(`Failed to unsubscribe from ${topic}:`, err) - } else { - this.state.subscribedTopics = this.state.subscribedTopics.filter(t => t !== topic) - console.log(`Unsubscribed from ${topic}`) - } - }) - } - } - - // 处理消息分发 - private handleMessage(topic: string, payload: Buffer): void { - const message: MqttMessage = { topic, payload } - - // 1. 查找该主题的精确匹配处理器 - if (this.handlers.has(topic)) { - this.handlers.get(topic)?.forEach(handler => handler(message)) - } - - // 2. 查找通配符匹配的处理器 - const wildcardTopics = Array.from(this.handlers.keys()).filter(t => t.includes('#') || t.includes('+')) - - for (const pattern of wildcardTopics) { - if (this.matchTopic(pattern, topic)) { - this.handlers.get(pattern)?.forEach(handler => handler(message)) - } - } - } - - // 通配符匹配 - private matchTopic(pattern: string, topic: string): boolean { - const patternParts = pattern.split('/') - const topicParts = topic.split('/') - - for (let i = 0; i < patternParts.length; i++) { - const patternPart = patternParts[i] - - // 单级通配符 - if (patternPart === '+') { - if (i >= topicParts.length) return false - continue - } - - // 多级通配符 - if (patternPart === '#') { - return true - } - - // 精确匹配 - if (patternPart !== topicParts[i]) { - return false - } - } - - return patternParts.length === topicParts.length - } - - // 注册LCC监控处理器 - public registerLccHandlers(projId: string, envId: string) { - // 服务器状态监控 - this.subscribe(`lcc/${projId}/${envId}/server`, this.handleServerState) - - // 客户端状态监控 - this.subscribe(`lcc/${projId}/${envId}/client`, this.handleClientState) - - // 任务监控 - this.subscribe(`lcc/${projId}/${envId}/task`, this.handleTaskMonitor) - - // 库存监控 - this.subscribe(`lcc/${projId}/${envId}/inv/#`, this.handleInventoryMonitor) - - // 设备监控 - this.subscribe(`lcc/${projId}/${envId}/device/+/status`, this.handleDeviceStatus) - this.subscribe(`lcc/${projId}/${envId}/device/+/alive`, this.handleDeviceAlive) - - // 日志监控 - this.subscribe(`lcc/${projId}/${envId}/log/#`, this.handleLogMonitor) - - // 告警监控 - this.subscribe(`lcc/${projId}/${envId}/alarm`, this.handleAlarmMonitor) - - // 脚本系统 - this.subscribe(`lcc/${projId}/script`, this.handleScriptSystem) - } - - // ==================== 消息处理器实现 ==================== - - private handleServerState(message: MqttMessage) { - try { - const data = JSON.parse(message.payload.toString()) - console.log('Server state update:', data) - // 这里可以分发到Vue store或其他状态管理系统 - } catch (error) { - console.error('Error parsing server state:', error) - } - } - - private handleClientState(message: MqttMessage) { - try { - const data = JSON.parse(message.payload.toString()) - console.log('Client state update:', data) - // 处理客户端状态更新 - } catch (error) { - console.error('Error parsing client state:', error) - } - } - - private handleTaskMonitor(message: MqttMessage) { - try { - const taskData = JSON.parse(message.payload.toString()) - console.log('Task update:', taskData) - // 处理任务更新 - } catch (error) { - console.error('Error parsing task data:', error) - } - } - - private handleInventoryMonitor(message: MqttMessage) { - try { - const [projId, envId, inv, catalogCode] = message.topic.split('/') - const inventoryData = JSON.parse(message.payload.toString()) - - console.log(`Inventory update for ${catalogCode}:`, inventoryData) - // 处理库存更新 - } catch (error) { - console.error('Error parsing inventory data:', error) - } - } - - private handleDeviceStatus(message: MqttMessage) { - try { - const [, projId, envId, device, deviceId, status] = message.topic.split('/') - const deviceData = JSON.parse(message.payload.toString()) - - console.log(`Device status for ${deviceId}:`, deviceData) - // 处理设备状态更新 - } catch (error) { - console.error('Error parsing device status:', error) - } - } - - private handleDeviceAlive(message: MqttMessage) { - try { - const [, projId, envId, device, deviceId, alive] = message.topic.split('/') - const status = message.payload.toString() - - console.log(`Device ${deviceId} is ${status}`) - // 处理设备存活状态 - } catch (error) { - console.error('Error parsing device alive status:', error) - } - } - - private handleLogMonitor(message: MqttMessage) { - try { - const [, projId, envId, log, logType] = message.topic.split('/') - const logData = JSON.parse(message.payload.toString()) - - console.log(`Logs for ${logType}:`, logData) - // 处理日志更新 - } catch (error) { - console.error('Error parsing log data:', error) - } - } - - private handleAlarmMonitor(message: MqttMessage) { - try { - const alarmData = JSON.parse(message.payload.toString()) - console.log('Alarm update:', alarmData) - // 处理告警更新 - } catch (error) { - console.error('Error parsing alarm data:', error) - } - } - - private handleScriptSystem(message: MqttMessage) { - try { - const scriptData = JSON.parse(message.payload.toString()) - console.log('Script system update:', scriptData) - // 处理脚本系统更新 - } catch (error) { - console.error('Error parsing script data:', error) - } - } -} diff --git a/src/core/manager/WorldModel.ts b/src/core/manager/WorldModel.ts index 6e5430c..e201a27 100644 --- a/src/core/manager/WorldModel.ts +++ b/src/core/manager/WorldModel.ts @@ -5,7 +5,7 @@ import EventBus from '@/runtime/EventBus' import StateManager from '@/core/manager/StateManager.ts' import { getQueryParams, setQueryParam } from '@/utils/webutils.ts' import localforage from 'localforage' -import LccMqttManager from '@/core/manager/LccMqttManager.ts' +import BackendMessageReceiver from '@/core/manager/BackendMessageReceiver.ts' import EnvManager from '@/core/manager/EnvManager.ts' import RCSScript from '@/core/script/RCSScript.ts' import LCCScript from '@/core/script/LCCScript.ts' @@ -40,7 +40,7 @@ export interface WorldModelState { */ export default class WorldModel { currentStateManager: StateManager - lccMqttManager = new LccMqttManager() + backendMessageReceiver = new BackendMessageReceiver() envManager = new EnvManager() /** diff --git a/src/core/script/LCCScript.ts b/src/core/script/LCCScript.ts index f99204c..3be4c38 100644 --- a/src/core/script/LCCScript.ts +++ b/src/core/script/LCCScript.ts @@ -1,4 +1,4 @@ -import Viewport from '@/core/engine/Viewport.ts' +import _ from 'lodash' import { worldModel } from '@/core/manager/WorldModel.ts' import { Request } from '@ease-forge/shared' @@ -67,7 +67,7 @@ export default class LCCScript implements LCC { } // 从后台读取所有车 - async loadExecutor(): Promise> { + async loadExecutor(): Promise { const res = await Request.request.post('/api/workbench/LccController@loadExecutor', { projectUuid: worldModel.state.project_uuid, envId: worldModel.state.runState.currentEnvId @@ -112,4 +112,12 @@ export default class LCCScript implements LCC { return res.data } + + subscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler) { + worldModel.backendMessageReceiver.subscribe(topicType, eventHandler) + } + + unsubscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler) { + worldModel.backendMessageReceiver.unsubscribe(topicType, eventHandler) + } } diff --git a/src/editor/widgets/IWidgets.ts b/src/editor/widgets/IWidgets.ts index fa8a44e..7389b11 100644 --- a/src/editor/widgets/IWidgets.ts +++ b/src/editor/widgets/IWidgets.ts @@ -1,6 +1,7 @@ import { defineComponent } from 'vue' import { renderIcon } from '@/utils/webutils.js' import Viewport, { type ViewportState } from '@/core/engine/Viewport.ts' +import { worldModel } from '@/core/manager/WorldModel.ts' export type IWidgetData = { /** @@ -23,6 +24,18 @@ export default defineComponent({ computed: { state(): ViewportState { return this.viewport?.state + }, + errorDescription() { + if (!this.isActivated) { + return '组件未激活' + } + if (!worldModel.state.isOpened) { + return '地图未打开' + } + if (!worldModel.backendMessageReceiver.state.isConnected) { + return '后端连接异常' + } + return '' } }, emits: ['close'], @@ -37,4 +50,4 @@ export default defineComponent({ this.$emit('close') } } -}) \ No newline at end of file +}) diff --git a/src/editor/widgets/logger/LoggerView.vue b/src/editor/widgets/logger/LoggerView.vue index 05e5178..236b637 100644 --- a/src/editor/widgets/logger/LoggerView.vue +++ b/src/editor/widgets/logger/LoggerView.vue @@ -21,7 +21,6 @@ import YvSrcEditor from '@/components/YvSrcEditor.vue' export default { name: 'LoggerView', components: { YvSrcEditor }, - webSocketSubscribe: ['logs'], mixins: [IWidgets], data() { return { diff --git a/src/editor/widgets/monitor/MonitorView.vue b/src/editor/widgets/monitor/MonitorView.vue index 5f18a6f..a6aa7ee 100644 --- a/src/editor/widgets/monitor/MonitorView.vue +++ b/src/editor/widgets/monitor/MonitorView.vue @@ -11,26 +11,28 @@
-
+ + +
- 总数{{total}} + 总数{{ total }}
- 上线{{online}} + 上线{{ online }}
- 下线{{offline}} + 下线{{ offline }}
- 运行中{{running}} + 运行中{{ running }}
- 空闲中{{idle}} + 空闲中{{ idle }}
-
+
@@ -38,10 +40,16 @@
- - [运行] + + + + [{{ getDeviceModeDesc(deviceInfo) }}] 导航到 - +
@@ -49,87 +57,34 @@
- -
按照导航路径移动至目标点
+ + + +
{{ getDeviceTaskTypeDesc(deviceInfo) }}
- -
17日 23:00:00 -01日 23:00:00
-
-
- + + +
- Agv-5 当前目标点 412
-
-
-
-
-
-
-
-
-
- -
-
-
- - [运行] - 导航到 - - -
- -
55%
+ {{ deviceInfo.isBlocked ? '阻挡' : '' }} + {{ deviceInfo.taskStatus == 'IDLE' ? '空闲' : '' }} + {{ deviceInfo.taskStatus == 'PAUSED' ? '暂停' : '' }} + {{ deviceInfo.taskStatus == 'EXECUTING' ? '执行中' : '' }}
- -
按照导航路径移动至目标点
-
-
- -
17日 23:00:00 -01日 23:00:00
-
-
- + + +
- Agv-5 当前目标点 412
-
-
-
-
-
-
-
-
-
- -
-
-
- - [运行] - 导航到 - - -
- -
55%
-
-
-
- -
按照导航路径移动至目标点
-
-
- -
17日 23:00:00 -01日 23:00:00
-
-
- -
- Agv-5 当前目标点 412
+ {{ deviceInfo.id }} 当前姿态 {{ deviceInfo.logicX + '_' + deviceInfo.logicY + '(' + deviceInfo.direction + ')' }}
@@ -141,107 +96,336 @@ \ No newline at end of file + diff --git a/src/types/LCC.d.ts b/src/types/LCC.d.ts index cbb5476..48bce25 100644 --- a/src/types/LCC.d.ts +++ b/src/types/LCC.d.ts @@ -25,15 +25,105 @@ declare interface LCC { /** * 获取所有车,并放到 Model 上 */ - loadExecutor(): Promise> + loadExecutor(): Promise /** * 保存并同步当前项目所有脚本 * @param scriptList */ saveAndSyncScripts(scriptList: { name: string, content: string }[]): Promise> + + /** + * 订阅后端消息 + * @param topicType 消息主题类型 + * @param eventHandler 消息处理函数 + */ + subscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler); + + /** + * 取消订阅后端消息 + * @param topicType 消息主题类型 + * @param eventHandler 消息处理函数 + */ + unsubscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler); } +/** + * 后端消息主题 + */ +type BackendTopicType = 'ServerState' | 'ClientState' | 'TaskUpdate' | 'InvUpdate' | + 'DeviceStatus' | 'DeviceAlive' | 'Logs' | 'Alarm' | 'ScriptUpdate' + +type DeviceAliveFn = (type: BackendTopicType, topic: string, body: { + id: string + type: string + online: boolean +}) => void + +type DeviceStatusFn = (type: BackendTopicType, topic: string, body: { + // 设备 ID + id: string + // 设备类型 + type: string + // 设备x,y,z坐标 + x: number + y: number + z: number + // 设备逻辑坐标x,y + logicX: number + logicY: number + // 设备姿态(up/down/left/right) + direction: string + // 设备旋转角度 + orientation: number + // 电池电量 + soc: number + // 工作模式: + // AMR_FREE_MODE=空闲模式; + // AMR_INIT_MODE=初始化模式; + // AMR_TASK_MODE=任务模式; + // AMR_SINGLE_ACTION_MODE=单动作模式; + // AMR_MANUAL_MODE=手动模式; + // AMR_HANDSET_MODE=遥控器模式; + // AMR_CHARGE_MODE=充电模式; + // AMR_TASK_INTERRUPT_MODE=任务被中断模式; + // AMR_CUSTOMIZE_MODE=自定义模式; + mode: 'AMR_FREE_MODE' | + 'AMR_INIT_MODE' | + 'AMR_TASK_MODE' | + 'AMR_SINGLE_ACTION_MODE' | + 'AMR_MANUAL_MODE' | + 'AMR_HANDSET_MODE' | + 'AMR_CHARGE_MODE' | + 'AMR_TASK_INTERRUPT_MODE' | + 'AMR_CUSTOMIZE_MODE' + // 任务状态 ID: IDLE / PAUSED / EXECUTING + taskStatus: string + // 是否阻挡 + isBlocked: boolean + // 已完成任务数 + taskCompleted: number + // 总共任务数,通过 taskCompleted/taskTotalCount 可以计算出完成率 + taskTotalCount: number + // 业务任务ID + bizTaskId: string + // 业务任务类型: 移动任务 / 充电任务 / 搬运任务 / 装载任务 / 卸载任务 + bizTaskType: '' | 'MOVE' | 'CHARGE' | 'CARRY' | 'LOAD' | 'UNLOAD' + // 业务任务类型: 待调度 / 设备执行中 / 暂停执行 / 调度异常 / 规划异常 / 设备执行异常 + bizTaskStatus: 'WAITING_FOR_DISPATCH' | 'DEVICE_EXECUTING' | 'PAUSED' | 'DISPATCH_ERROR' | 'PLANNING_ERROR' | 'DEVICE_ERROR' + // 比如 WAY_1_1 Rack1/0/0/0 + bizTaskFrom: string + // 比如 WAY_1_1 Rack1/0/0/0 charge1 + bizTaskTo: string + // 搬运托盘号 + bizLpn: string +}) => void + +/** + * 后端消息回调 + */ +type BackendMessageHandler = (type: BackendTopicType, topic: string, message: any) => void + type ContainerT = 'pallet' | 'tote' | 'carton' | 'box' interface InvVo { diff --git a/src/types/ModelTypes.ts b/src/types/ModelTypes.ts index bdc967e..22e53db 100644 --- a/src/types/ModelTypes.ts +++ b/src/types/ModelTypes.ts @@ -74,3 +74,4 @@ export const MaterialQualityEnum = { Low: 2 } +