You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

258 lines
7.4 KiB

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'
export default class EnvManager {
private amrMessageManager: AmrMessageManager = new AmrMessageManager()
public client: mqtt.MqttClient = null
readonly stopSubscribe: StopSubscribe[] = []
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<any> = JSON.parse(payload.toString())
this.amrMessageManager.handleMessage(topic, message)
} else {
}
}
onMqttError = (error: Error | ErrorWithReasonCode) => {
console.error('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 = worldModel.state.runState.currentEnv
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(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)
}
}
// 加载库存到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)
}
}
// 加载执行器到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(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, agvState.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
}
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<EnvInfo[]> {
// 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()
}
}