工业仿真(simulation)-- 工人池(7)

90 阅读8分钟

本篇介绍仿真引擎里面的工人池实体类

工人池和其他的实体类有所不同,工人池不需要继承 BaseStation 基类

一下是加工站,发生器,缓存区,都需要继承 BaseStation 基类

由于工人池不参与生产,所以不需要继承

从下面这张图可以看出,除工人池外,其他的设备都有输入和输出,对应产品的流入和流出,唯独工人池没有

但工人池需要维护一个设备关联表

设计思路

在整个仿真过程中,我们的工人池只需要做三件事情

  1. 仿真开始时,遍历自己的关联设备表,查看哪些设备需要工人协助,然后派发工人过去协助

  2. 在仿真过程中,如果有设备出现了不良率,如果是自己的关联的设备,就派发工人过去处理不良产品,处理逻辑是直接丢弃不良产品,然后将设备转为空闲状态,然后工人返回到工人池

  3. 在仿真过程中,如果有设备出现了故障,如果是自己的关联的设备,就派发工厂过去,解决故障的逻辑是,让设备重新加工一次,此时的加工,一定不会出现故障,然后工人返回到工人池

那么针对上面的这个逻辑,我们就需要写两个类,工人池实体类,工人实体类

代码开发

在之前的工业仿真文章里面我提到过调度器 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,等待设定的维修时间后,重启该设备,然后返程

关于仿真引擎里面的工人池,他的执行逻辑到这里已讲解完毕,若有不理解的地方可以在评论区留言