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 { Request } from '@ease-forge/shared' import AmrMessageManager from '@/core/manager/amr/AmrMessageManager' import { AmrMsg } from '@/core/manager/amr/AmrMessageDefine' import { getAgvItemNameById } from '@/core/ModelUtils.ts' export default class EnvManager { private amrMessageManager: AmrMessageManager = new AmrMessageManager() public client: mqtt.MqttClient = null readonly stopSubscribe: StopSubscribe[] = [] public env: EnvInfo = null onMqttConnect = (packet: IConnackPacket) => { console.log('Connected') } onMqttMessage = (topic: string, payload: Buffer, packet: IPublishPacket) => { // console.log(`[${topic}]-> ${payload.toString()}`) if (topic.startsWith('/wcs_server/')) { const message: AmrMsg = JSON.parse(payload.toString()) this.amrMessageManager.handleMessage(topic, message) } else if (topic.startsWith('/agv_robot/status')) { const message: AmrMsg = JSON.parse(payload.toString()) this.amrMessageManager.handleStatusMessage(topic, message) } } onMqttError = (error: Error | ErrorWithReasonCode) => { console.error('EnvManager (' + this.env.envConfig.mqtt.websocket + ') error:', error) } async connectEnv() { if (!worldModel.state.isOpened) { system.showErrorDialog('WorldModel is not opened, cannot start EnvManager.') return } if (!worldModel.state.runState.currentEnvId) { system.showErrorDialog('Current environment ID is not set, cannot start EnvManager.') return } if (worldModel.state.runState.isRunning) { system.showErrorDialog('EnvManager is already running, cannot start again.') return } if (!worldModel.state.runState.currentEnv) { system.showErrorDialog('Environment is not specified, cannot start EnvManager.') return } if (!worldModel.state.runState.currentEnv.envConfig.mqtt.websocket) { system.showErrorDialog('MQTT websocket URL is not set in the envConfig.mqtt.') return } await this.disconnectEnv() system.showLoading() worldModel.state.runState.isLoading = true const env = _.cloneDeep(worldModel.state.runState.currentEnv) this.env = env try { worldModel.backendMessageReceiver.setProjectEnv(worldModel.state.project_uuid, worldModel.state.runState.currentEnvId) this.client = mqtt.connect(env.envConfig.mqtt.websocket, { path: '/mqtt', clientId: system.createUUID(), clean: true, connectTimeout: 300, username: env.envConfig.mqtt.username, password: env.envConfig.mqtt.password, unixSocket: true, keepalive: 60 }) await this.loadExecutorToModel() this.stopSubscribe.push( worldModel.backendMessageReceiver.subscribe('InvUpdate', this.onInvUpdateMessage.bind(this)) ) this.stopSubscribe.push( worldModel.backendMessageReceiver.subscribe('ServerState', this.onServerUpdateMessage.bind(this)) ) this.stopSubscribe.push( worldModel.backendMessageReceiver.subscribe('DeviceStatus', this.onDeviceStatusMessage.bind(this)) ) await this.loadInvToModel() this.client.on('connect', this.onMqttConnect) this.client.on('message', this.onMqttMessage) this.client.on('error', this.onMqttError) worldModel.state.runState.isRunning = true } finally { system.clearLoading() worldModel.state.runState.isLoading = false } } /** * 处理设备状态消息 */ onDeviceStatusMessage(type, topic, data: AgvStatusVo) { const object3D = Model.find3D(getAgvItemNameById(data.id)) if (object3D) { object3D.agvStatusVo = data } } /** * 监听在服务器停机之后,客户端连接也必须停机 */ onServerUpdateMessage(type: BackendTopicType, topic: string, data: ServerStatusVo) { if (worldModel.state.runState.isRunning && worldModel.state.runState.currentEnvId === data.envId) { // 处理服务器状态更新 if (!data.isRunning) { // 如果服务器停止了,则断开连接 console.log(`Server stopped: ${data.envId}`) system.msg(`Server is stopped, client disconnect!`, 'warning') this.disconnectEnv().finally() } } } async onInvUpdateMessage(type: BackendTopicType, topic: string, body: InvUpdateVo) { console.log(`InvUpdate: ${type} ${topic}`, body) if (!window['Model']) { // 如果没有3D模型加载,则不处理库存更新 return } Model.deleteInv(body.lpn) if (body.after != null) { // 将托盘挪到目标位置 const after = body.after Model.createInv('pallet', body.lpn, after.rack, after.bay, after.level, after.cell, after.locCode) } } // 加载库存到3D视图上 async loadInvToModel() { if (!window['Model']) { return } const invRes = await LCC.queryInv({}) if (!invRes.success) { return } for (const row of invRes.data) { const bay = row.bay const cell = row.cell // : 0 const level = row.level // : 0 const loc_code = row.loc_code // : "rack1_0_0_0" const lpn = row.lpn // : "LPN1" const rack = row.rack // : "rack1" const container_type = row.container_type // : "pallet" Model.createInv(container_type, lpn, rack, bay, level, cell, loc_code) } } // 加载执行器到3D视图上 async loadExecutorToModel() { if (!window['Model']) { return } const deviceRes = await LCC.queryDeviceInfoList() if (!deviceRes.success) { return } const deviceList: AgvStatusVo[] = deviceRes.data || [] for (const agvState of deviceList) { const agvItem = Model.find(getAgvItemNameById(agvState.id)) if (!agvItem) { // 还没加载到地图 const item = JSON.parse(agvState.virtualExecutorPayload) const pos = Model.getPositionByLogicXY(agvState.logicX, agvState.logicY) if (pos) { item.tf[0] = [pos.x, pos.y, pos.z] } switch (_.toLower(agvState.direction)) { // right=0/left=180/up=90/down=-90 case 'right': item.tf[1][1] = 0 // 右侧 break case 'left': item.tf[1][1] = 180 // 左侧 break case 'down': item.tf[1][1] = -90 // 下方 break case 'up': item.tf[1][1] = 90 // 上方 break } item.id = getAgvItemNameById(agvState.id) item.dt.vehicleId = agvState.id Model.createExecutor(item) } } } async disconnectEnv() { system.showLoading() try { worldModel.state.runState.isRunning = false for (const stopFn of this.stopSubscribe) { stopFn() } this.stopSubscribe.length = 0 if (this.client) { this.client.removeAllListeners() this.client.end() this.client = null } if (window['viewport']) { const viewport = window['viewport'] as Viewport viewport?.runtimeManager?.clear() } } finally { system.clearLoading() worldModel.state.runState.isLoading = false } } /** * 创建运行环境 * @param worldId 世界ID */ static async createEnv(worldId: string) { throw new Error('Method not implemented.') } /** * 获取所有运行环境 */ static async getAllEnv(worldId: string): Promise { // system.invokeServer('') if (!worldId) { return Promise.reject('World ID is not provided.') } const res = await Request.request.post('/api/workbench/EnvController@getAllEnv', { worldId: worldId }) return res.data } /** * 卸载资源 */ dispose(): void { this.disconnectEnv().finally() } }