本篇介绍仿真引擎里面的工人池实体类
工人池和其他的实体类有所不同,工人池不需要继承 BaseStation 基类
一下是加工站,发生器,缓存区,都需要继承 BaseStation 基类
由于工人池不参与生产,所以不需要继承
从下面这张图可以看出,除工人池外,其他的设备都有输入和输出,对应产品的流入和流出,唯独工人池没有
但工人池需要维护一个设备关联表
设计思路
在整个仿真过程中,我们的工人池只需要做三件事情
仿真开始时,遍历自己的关联设备表,查看哪些设备需要工人协助,然后派发工人过去协助
在仿真过程中,如果有设备出现了不良率,如果是自己的关联的设备,就派发工人过去处理不良产品,处理逻辑是直接丢弃不良产品,然后将设备转为空闲状态,然后工人返回到工人池
在仿真过程中,如果有设备出现了故障,如果是自己的关联的设备,就派发工厂过去,解决故障的逻辑是,让设备重新加工一次,此时的加工,一定不会出现故障,然后工人返回到工人池
那么针对上面的这个逻辑,我们就需要写两个类,工人池实体类,工人实体类
代码开发
在之前的工业仿真文章里面我提到过调度器 Dispatcher
调度器的作用很重要,当设备A的状态发生改变时,会立即通知调度器进行处理,比如当设备从堵塞转为空闲时,就会通知调度器,然后调度器去通知A的上一站,要给A派发产品了
那此时我们就需要在调度器里面写一个工人池的属性数据
目的就是为了,当设备出现故障,不良等状态时,就给调度器发通知,然后调度器作为一个中介,再给工人池发通知,告知设备出现了异常,请赶紧处理
然后在我们仿真建模时(此时仿真还未开始),就需要先把所有的工人池注册到调度器里面
在调度器里面编写 dispatcher.registerWorkerPool(data) 方法
那接下来我们就先设计工人池实体类,首先是先确定工人池有哪些属性数据
工人池属性数据
class workerPool {
id: string
name: string
x: number
y: number
//总人数
workerNum: number
//空闲人数
freeWorkerNum: number
//工人速度
workerSpeed: number
//维修时间
maintenanceTime: number
// 设备映射表
deviceMap: { id: string; name: string }[]
//待办事项
todoList: { type: 'clean' | 'maintenance'; station: BaseStation }[] = []
}
工人属性数据
class Worker {
id: string
x: number
y: number
target: BaseStation //目标设备
workerPool: WorkerPool // 所属工人池
//任务类型
taskType: 'assist' | 'clean' | 'maintenance' | 'return'
speed: number //当前速度
maintenanceTime: number //维修时间
targetX: number = 0 //目标位置X
targetY: number = 0 //目标位置Y
}
工人协助
那接下来我们先实现第一个功能
仿真开始时,遍历自己的关联设备表,查看哪些设备需要工人协助,然后派发工人过去协助
在建模过程中,我们是已经把工人池注册到了调度器里面,然后再建模完成后,我们是需要立即启动工人池,让工人池去寻找有哪些设备需要工人协助
//开启工人池
const workerPoolList = dispatcher.workerPoolList
workerPoolList.forEach((workerPool) => {
workerPool.initManualStation()
})
那我们接着看 工人池里面的 initManualStation 方法
/**
* 遍历所有关联设备,然后判断哪些设备需要人协助
*/
initManualStation() {
this.deviceMap.forEach((device) => {
const nodes = Canvas.getNodes('id', device.id)
if (nodes.length === 0) return
const node = nodes[0]
if (node.simData.process?.personAssist && this.freeWorkerNum > 0) {
const targetNode = Array.from(dispatcher.idleStations).find((item) => item.id === node.id)
if (!targetNode) return
this.changeWorkerNum('-1')
//此设备需要工人协助
const worker = new Worker(
generateUUID(),
this.x,
this.y,
targetNode,
this,
'assist',
this.workerSpeed,
this.maintenanceTime
)
messageTransfer('workerPool', 'generate', {
targetId: this.id,
workerId: worker.id
})
//执行任务
worker.executeTask()
}
})
}
在这里代码里面,我们可以看到,是否需要工人协助,是根据personAssist属性来判断,如果需要协助,就会new 出一个工人,然后将工人执行任务的类型设置为assist,任务类型分为三种
'assist':协助
'clean':若设备发生了不良率,任务类型就为clean
'maintenance':若设备发生了故障,任务类型就为maintenance
然后让工人执行任务,那接下来我们来看工人的 executeTask 方法
//执行任务
executeTask() {
this.targetX = this.target.x
this.targetY = this.target.y
if (this.taskType === 'assist') {
//执行协助任务
this.moving()
} else if (this.taskType === 'clean') {
//执行清洁任务
this.moving()
} else if (this.taskType === 'maintenance') {
//执行维护任务
this.moving()
}
}
在这里三种任务类型的执行逻辑都是一样的,都是调用moving方法
工人的moving方法和AGV的moving方法类似
//移动
moving() {
if (simController.getStatus() !== 'running') return
// 计算当前位置和目标位置的距离
const dx = this.targetX - this.x
const dy = this.targetY - this.y
const distanceToTarget = Math.sqrt(dx * dx + dy * dy) // 直线距离
// 计算目标角度(弧度转换为度数)
const angleInRadians = Math.atan2(dy, dx) // 返回的是弧度
const angleInDegrees = (angleInRadians * 180) / Math.PI // 将弧度转换为度数
// 如果AGV已经到达目标位置,则停止移动
if (distanceToTarget === 0) {
this.arriveDestination()
return
}
// 计算在40ms(0.04s)内AGV能走的距离
const timeInterval = 0.04
const distancePerStep = this.speed * timeInterval * SimulationSpeed.getSpeed // 计算每50ms AGV可以移动的距离
// 如果目标距离小于AGV在50ms内能走的距离,就直接到目标位置
if (distanceToTarget <= distancePerStep) {
this.x = this.targetX
this.y = this.targetY
} else {
// 计算单位向量
const moveRatio = distancePerStep / distanceToTarget
this.x += dx * moveRatio
this.y += dy * moveRatio
}
messageTransfer('workerPool', 'transport', {
targetId: this.id,
x: this.x,
y: this.y,
angle: angleInDegrees
})
setTimeout(() => {
this.moving()
}, 40)
}
然后当工人到达目标点位后,就会调用arriveDestination方法
//到达目的地
arriveDestination() {
if (this.taskType === 'assist') {
//执行协助任务
messageTransfer('workerPool', 'recycle', {
targetId: this.id
})
messageTransfer('style', null, {
targetId: this.target.id,
style: {
icon: 'nodeMachineWithPeople'
}
})
}
}
此时工人就会修改设备的图标,这时我们就需要注意,协助生产类型的工人在到达设备后,是不会返回到工人池的
处理不良率
我们先看设备的完成加工方法
finishProcessing(product: Product): void {
if (Math.random() < this.defectRate) {
//产品发生了不良率
console.log(`[${currentTime}] ❌ ${this.name} 报废产品 ${product.id}`)
messageTransfer('style', null, {
targetId: this.id,
style: {
backgroundColor: '#FFDEB16B'
}
})
messageTransfer('product', 'finishProcessing', { targetId: this.id, productId: product.id })
this.setStatus('clearance')
return
}
messageTransfer('product', 'finishProcessing', { targetId: this.id, productId: product.id })
//产品加工完毕,尝试派发产品
this.setStatus('block')
}
在这个方法里面,如果随机数值小于我们设定的不良率,那么产品就发生了不良,此时设备的状态就会转变为clearance,表示需要清理
那接下来我们来看调度器是怎么处理这个状态的
/**
* 解决需要清理的设备,向工人池发送请求
*/
for (const alarm of this.clearStations) {
let workerPool = null as null | WorkerPool
for (const pool of this.workerPoolList) {
if (pool.isDeviceInMap(alarm.id)) {
workerPool = pool
break
}
}
if (workerPool) {
workerPool.receiveTask('clean', alarm)
}
}
从上面的代码我们可以看出
调度器会遍历所有的设备,然后再去遍历工人池,我们来看一下工人池暴露的isDeviceInMap方法
//判断某台设备是否在自己的映射表中
isDeviceInMap(deviceId: string) {
return this.deviceMap.some((device) => device.id === deviceId)
}
如果工人池检测出这个设备在自己的关联设备里面,就会执行接受任务,那我们继续看receiveTask方法
//任务接收器
receiveTask(type: 'clean' | 'maintenance', device: BaseStation) {
//如果该设备已经在todolist里面,则不重复添加
if (this.todoList.some((item) => item.station.id === device.id)) return
this.todoList.push({
type,
station: device
})
this.executeTask()
}
这个方法接受一个'clean' | 'maintenance'(clean表示清理,maintenance表示维修)类型的type参数,然后工人池拿到这个任务后,会将任务存入到todoList待办事项里面,然后再去执行executeTask方法
//执行任务
executeTask() {
if (this.todoList.length === 0) return
if (this.freeWorkerNum < 1) return
while (this.todoList.length > 0) {
//拿到todoList的第一个元素
const item = this.todoList.shift()
if (!item) break
dispatcher.clearNegativeStatus(item.station)
if (item.type === 'clean') {
console.log(`[${currentTime}] 🚿 ${this.name} 开始清理 ${item.station.name}`)
this.startCleanStation(item.station)
} else if (item.type === 'maintenance') {
console.log(
`[${currentTime}] 🔧 ${this.name} 开始维修 ${item.station.name} 预计需要时间 ${this.maintenanceTime} }`
)
this.startMaintenanceStation(item.station)
}
}
}
在执行任务方法里面,工人池会先清理指定设备的负面状态,为的就是防止设备持续不断的发出清理信号
然后再根据不同的类型在执行不同的方法,那接下来我们先看startCleanStation方法
/**
* 当有设备需要清洁时,比如产品发生了不良率
*/
startCleanStation(device: BaseStation) {
if (this.freeWorkerNum < 1) return
this.changeWorkerNum('-1')
const worker = new Worker(
generateUUID(),
this.x,
this.y,
device,
this,
'clean',
this.workerSpeed,
this.maintenanceTime
)
messageTransfer('workerPool', 'generate', {
targetId: this.id,
workerId: worker.id
})
//执行任务
worker.executeTask()
}
在这个方法里面我们new出的工人,他的任务类型就不再是assist,而是clean
此时工人再去执行任务 executeTask(),就和刚才协助生产一样,都是先移动到设备位置,那接下来我们来看看到达目的地后,工人会做出什么行为
//到达目的地
arriveDestination() {
if (this.taskType === 'assist') {
//执行协助任务
messageTransfer('workerPool', 'recycle', {
targetId: this.id
})
messageTransfer('style', null, {
targetId: this.target.id,
style: {
icon: 'nodeMachineWithPeople'
}
})
} else if (this.taskType === 'clean') {
//执行清洁任务
messageTransfer('style', null, {
targetId: this.target.id,
style: {
backgroundColor: '#ffffff9c'
}
})
this.target.eventWindow('delProduct', null)
this.target.eventWindow('status', 'idle')
this.returnWorkerPool()
}
}
工人在到达设备位置后,清除掉不良的产品,然后将设备状态转为空闲
再然后工人将任务类型修改为return,开始移动,返回工人池
//返回工人池
returnWorkerPool() {
//情节完毕,开始返程
this.taskType = 'return'
this.targetX = this.workerPool.x
this.targetY = this.workerPool.y
this.moving()
}
在工人到达工人池后
//到达目的地
arriveDestination() {
if (this.taskType === 'assist') {
//执行协助任务
messageTransfer('workerPool', 'recycle', {
targetId: this.id
})
messageTransfer('style', null, {
targetId: this.target.id,
style: {
icon: 'nodeMachineWithPeople'
}
})
} else if (this.taskType === 'clean') {
//执行清洁任务
messageTransfer('style', null, {
targetId: this.target.id,
style: {
backgroundColor: '#ffffff9c'
}
})
this.target.eventWindow('delProduct', null)
this.target.eventWindow('status', 'idle')
this.returnWorkerPool()
} else if (this.taskType === 'return') {
this.workerPool.returnHome(this.id)
}
}
工人池的returnHome方法会接收返程的工人
//有人员进行了返程
returnHome(workerId: string) {
messageTransfer('workerPool', 'recycle', {
targetId: workerId
})
this.changeWorkerNum('+1')
//查看是否有待办事项
this.executeTask()
}
工人池接收到返程的工人后,会将空闲工人数量+1,然后再去查看是否还有待办事项
处理故障
处理故障和处理不良率很相似,也是先通知调度器
/**
* 解决故障设备,向工人池发送请求
*/
for (const fault of this.faultStations) {
let workerPool = null as null | WorkerPool
for (const pool of this.workerPoolList) {
if (pool.isDeviceInMap(fault.id)) {
workerPool = pool
break
}
}
if (workerPool) {
workerPool.receiveTask('maintenance', fault)
}
}
然后找到对应的工人池,工人池再去执行任务,此时的任务类型就变成了maintenance
我们来看工人池的执行任务方法
当任务类型为maintenance时,就会调用startMaintenanceStation方法
/**
* 当有设备需要维修时,比如设备故障
*/
startMaintenanceStation(device: BaseStation) {
if (this.freeWorkerNum < 1) return
this.changeWorkerNum('-1')
const worker = new Worker(
generateUUID(),
this.x,
this.y,
device,
this,
'maintenance',
this.workerSpeed,
this.maintenanceTime
)
messageTransfer('workerPool', 'generate', {
targetId: this.id,
workerId: worker.id
})
//执行任务
worker.executeTask()
}
这是传给工人的任务类型就变成了maintenance
此时工人就会去执行任务,然后移动到指定设备的位置
//到达目的地
arriveDestination() {
if (this.taskType === 'assist') {
//执行协助任务
messageTransfer('workerPool', 'recycle', {
targetId: this.id
})
messageTransfer('style', null, {
targetId: this.target.id,
style: {
icon: 'nodeMachineWithPeople'
}
})
} else if (this.taskType === 'clean') {
//执行清洁任务
messageTransfer('style', null, {
targetId: this.target.id,
style: {
backgroundColor: '#ffffff9c'
}
})
this.target.eventWindow('delProduct', null)
this.target.eventWindow('status', 'idle')
this.returnWorkerPool()
} else if (this.taskType === 'maintenance') {
schedule(
this.maintenanceTime,
() => {
//执行维护任务
messageTransfer('style', null, {
targetId: this.target.id,
style: {
backgroundColor: '#ffffff9c'
}
})
this.target.eventWindow('rework', null)
//返程
this.returnWorkerPool()
},
'maintenance'
)
} else if (this.taskType === 'return') {
this.workerPool.returnHome(this.id)
}
}
从上面的代码可以看到,工人在达到设备后,调用schedule,等待设定的维修时间后,重启该设备,然后返程
关于仿真引擎里面的工人池,他的执行逻辑到这里已讲解完毕,若有不理解的地方可以在评论区留言