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.
265 lines
7.9 KiB
265 lines
7.9 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'
|
|
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<any> = JSON.parse(payload.toString())
|
|
this.amrMessageManager.handleMessage(topic, message)
|
|
} else if (topic.startsWith('/agv_robot/status')) {
|
|
const message: AmrMsg<any> = 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<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()
|
|
}
|
|
}
|
|
|