工业仿真(simulation)-- AGV(6)

87 阅读8分钟

一:简介

AGV(自动导引运输车)是现代工业物流中不可或缺的核心装备之一,其重要性主要体现在以下几个方面:

  • 核心价值:AGV是连接生产、仓储、配送等各个环节的“自动化血脉”。它无需人工驾驶,可以24/7不间断工作,是实现“黑灯工厂”和全自动化物流的关键组成部分。
  • 智能调度:通过与上位管理系统(如WMS、ERP、MES)集成,AGV可以接收指令,智能规划最优路径,实现物料、半成品、成品的自动、精准、高效流转。

AGV的重要性可以概括为:它是工业4.0和智能制造的基石之一,通过将“物”的流动自动化、数字化和智能化,从根本上提升了现代工业物流的效率、柔性和可靠性,是企业降本增效、提升核心竞争力的关键工具。

二:仿真软件中的应用

在仿真软件中AGV可以有多种外观,例如搬运机器人,叉车,货车,飞机。甚至可以把人看作AGV的一种。

在正常情况下,如果两台设备相连接后,如果不指定搬运设备,那么线的箭头就是橙色

只有当我们指定了搬运设备,箭头就会变成蓝色

此时加工站的下一站就不再是加工站2,而是AGV,当加工站派发产品时,会将产品派发给AGV上面

并且在AGV内部会维护一张设备连接表,当我们收到加工站的产品时,会从这张表里面去找,应该将产品送到哪台设备上

当我们的AGV有了起点和终点后,我们就可以去拿货或者是送货,此时就需要判断我们的AGV是否绑定了交通

  • 如果绑定了交通,那么AGV就必须按照道路规划的路线来走
  • 如果没有绑定,那么就可以按照直线的方式过去

那接下来我们来看在代码里面如何实现AGV功能

三:AGV代码实现

设计思路

AGV和其他节点一致,也是继承了BaseStation类,总结来说,流程如下

主线流程

  1. 判断是否可以接收产品,此时不管能不能接收新产品,都需要将接收任务暂存起来

  2. 如果可以接收新产品,则移动到设备位置进行接收

  3. 移动到设备位置后,再次调用设备派发,拿到新产品

  4. 拿到新产品后,我们需要再次查看是否还有装载产品的任务,如果有,就再次执行2,3步骤

  5. 在送货前,还需要做一些前置处理,需要先判断有哪些设备处于空闲状态,如果有,就随机挑选一台,如果没有,就间隔一秒钟后,再次执行第5步骤

  6. 根据AGV是否绑定了交通,我们给AGV规划一条路线,此时将AGV的业务逻辑改为送货,然后开始移动

  7. 产品派发后,我们再次判断AGV上面是否还有产品,如果有,则执行5,6步骤,如果没有,则判断取货列表里面是否有任务

移动流程

  1. 判断当前状态是否等于stop,如果等于,则停止移动

  2. 计算当前位置和目标位置的直线距离,计算当前位置和目标位置的角度

  3. 如果剩余距离为0,则表示已到达目标点,停止移动

  4. 根据AGV的速度,和仿真速度,计算出50mm移动的距离

  5. 如果剩余距离小于移动距离,则AGV将直接移动到目标点上

  6. 当移动到目标点上后,将状态切换成stop,此时再判断AGV是否绑定了交通

  7. 如果没有绑定交通,我们就需要根据当前的业务状态,来判断是取货还是送货,如果是取货,则调用设备,进行产品派发,如果是送货,则自身派发产品

  8. 如果绑定了交通,我们就需要判断最优路径列表是否已经走完,走完表示我们已经到达了目标点,我们就需要提取到路径表里面下一个点位,然后再次循环移动流程

那接下来就一步一步看代码怎么实现的

代码实现

我们需要先给AGV定义一些私有属性

  speed: number //速度
  capacity: number  //装载容量

  moveType: 'deliverGoods' | 'pickupGoods' = 'deliverGoods' //业务类型
  agvStatus: 'stop' | 'move' = 'stop' //agv状态

  mapping: { source: BaseStation; target: BaseStation }[] = [] //源-目标映射表

  needLoadGoodsEvents: { id: string; productId: string; x: number; y: number }[] = [] //存储需要装载货物的事件
  loadProduct: { sourceId: string; targetId: string[]; product: Product }[] = [] // 存储装载的货物

  targetPosition: { x: number; y: number } = { x: 0, y: 0 }
  currentAngle: number = 0 // 当前角度,单位是度

  enableTraffic: boolean = false //是否启用交通网络
  currentPoint: string = '' // 当前点位
  targetPoint: string = ''  // 目标点位
  planPath: string[] = [] // 路径规划结果

  targetEntityIndex: number = 0 // 目标实体索引

mapping是一个很重要的设备映射列表数据,以下面这张图为例,此时的加工站就是source,加工站2就是target

然后提前设置一些工具函数

  //根据sourceId获取到targetId数组
  private getTargetIdArrayBySourceId(sourceId: string): string[] {
    return this.mapping.filter((item) => item.source.id === sourceId).map((item) => item.target.id)
  }

  //根据id获取位置
  private getPositionById(id: string): { x: number; y: number } {
    for (const item of this.mapping) {
      if (item.source.id === id) {
        return { x: item.source.x, y: item.source.y }
      }

      if (item.target.id === id) {
        return { x: item.target.x, y: item.target.y }
      }
    }
    return { x: 0, y: 0 }
  }

  //根据id获取实体类
  private getEntityById(id: string): BaseStation | undefined {
    for (const item of this.mapping) {
      if (item.source.id === id) {
        return item.source
      } else if (item.target.id === id) {
        return item.target
      }
    }
    return undefined
  }

  //根据位置获取实体类
  private getEntityByPosition(): BaseStation | null {
    let station = null as BaseStation | null
    for (const item of this.mapping) {
      if (item.source.x === this.x && item.source.y === this.y) {
        station = item.source
        break
      } else if (item.target.x === this.x && item.target.y === this.y) {
        station = item.target
        break
      }
    }
    if (station) {
      return station
    }

    //如果没有找到,则改变策略,根据当前位置,寻找最近的实体类
    let minDistance = 100000
    for (const item of this.mapping) {
      const distance1 = Math.sqrt(
        Math.pow(this.x - item.source.x, 2) + Math.pow(this.y - item.source.y, 2)
      )
      if (distance1 < minDistance) {
        station = item.source
        minDistance = distance1
      }
      const distance2 = Math.sqrt(
        Math.pow(this.x - item.target.x, 2) + Math.pow(this.y - item.target.y, 2)
      )
      if (distance2 < minDistance) {
        station = item.target
        minDistance = distance2
      }
    }
    return station
  }

  //设置映射关系
  public setMapping(source: BaseStation, target: BaseStation) {
    this.mapping.push({ source, target })
  }

  //初始化交通点位信息
  public initTrafficPoint(id: string, distance: [number, number]) {
    this.currentPoint = id
    this.x = distance[0]
    this.y = distance[1]
    this.enableTraffic = true
    messageTransfer('agv', 'transport', {
      targetId: this.id,
      x: this.x,
      y: this.y,
      angle: 0
    })
  }

接收产品以及暂存装载任务

set setNeedLoadGoodsEvents(data: { id: string; productId: string } | null) {
    if (data) {
      //先判断这个产品是否已经存在
      if (
        this.needLoadGoodsEvents.some(
          (event) => event.productId === data.productId && event.id === data.id
        )
      )
        return

      const { x, y } = this.getPositionById(data.id)
      this.needLoadGoodsEvents.push({ id: data.id, productId: data.productId, x, y })
      this.handleLoadGoodsEvent()
    } else {
      this.needLoadGoodsEvents.shift()
    }
  }

  public canReceiveProduct(id: string, product: Product): boolean {
    console.log(`[${currentTime}] 🚚 ${this.name} 接收产品 ${product.id}`)
    //接下来判断agv是否到达指定位置
    const { x: sourceX, y: sourceY } = this.getPositionById(id)
    //判断是否去启用了交通网络
    if (this.enableTraffic) {
      const entity = this.getEntityById(id)
      if (!entity) return false
      //根据entity的位置寻找最近的点位
      const nearestPoint = TrafficNetwork.getAdjacentPoints([entity.x, entity.y])
      if (!nearestPoint) return false
      //获取到目标点位
      const targetPosition = TrafficNetwork.getPositionById(nearestPoint.id)
      if (
        this.x === targetPosition[0] &&
        this.y === targetPosition[1] &&
        this.loadProduct.length <= this.capacity
      ) {
        return true
      } else {
        this.setNeedLoadGoodsEvents = { id, productId: product.id }
        return false
      }
    } else {
      //如果没有启用,则按正常的逻辑接收产品
      if (this.x === sourceX && this.y === sourceY && this.loadProduct.length <= this.capacity) {
        return true
      } else {
        this.setNeedLoadGoodsEvents = { id, productId: product.id }
        return false
      }
    }
  }

开始去拿货

  //处理需要装载货物的事件
  private handleLoadGoodsEvent() {
    if (this.loadProduct.length >= this.capacity) return
    if (this.agvStatus !== 'stop') return

    if (this.needLoadGoodsEvents.length === 0) return
    const event = this.needLoadGoodsEvents[0]

    if (event) {
      this.setNeedLoadGoodsEvents = null

      // 先判断这个设备是否处于堵塞
      const sourceEntity = this.getEntityById(event.id)
      if (sourceEntity && dispatcher.isDeviceInBlockOrSpecial(sourceEntity)) {
        this.pretreatmentTargetPosition(sourceEntity, event.x, event.y)
        this.moveType = 'pickupGoods'

        if (this.agvStatus === 'stop') {
          this.agvStatus = 'move'
          this.moving()
        } else {
          this.moving()
        }
      } else {
        setTimeout(() => {
          this.handleLoadGoodsEvent()
        }, 100)
      }
    }
  }

到达装货位置,接收货物

  //接受就绪产品
  receiveReadyProduct(productId: string): void {
    const product = getReadyProduct(productId)
    if (!product) {
      return
    }
    product.setFrom(this.id)
    this.onProductReceived(product)
  }
  //接收货物
  protected onProductReceived(product: Product): void {
    if (this.loadProduct.length > this.capacity) {
      new Error('AGV装载货物已满')
    }
    const sourceEntity = this.getEntityByPosition()
    if (sourceEntity) {
      const targetIds = this.getTargetIdArrayBySourceId(sourceEntity.id)
      this.loadProduct.push({
        sourceId: sourceEntity.id,
        targetId: targetIds,
        product
      })

      messageTransfer('product', 'transport', {
        targetId: this.id,
        productId: product.id,
        x: this.x,
        y: this.y,
        angle: this.currentAngle,
        duration: 0
      })

      //下一步,判断是继续取货还是送货
      if (this.loadProduct.length < this.capacity && this.needLoadGoodsEvents.length > 0) {
        //继续取货
        this.handleLoadGoodsEvent()
      } else if (this.loadProduct.length > 0) {
        //送货
        this.startDeliverGoods()
      } else {
        console.log('AGV 无效指令')
      }
    } else {
      console.log('AGV 无法找到源节点')
    }
  }

在送货前进行前置处理,比如规划路径,获取目标点位

  //开始送货的前置处理
  public startDeliverGoods() {
    if (this.loadProduct.length === 0) {
      this.agvStatus = 'stop'
      return
    }
    const { sourceId, product, targetId } = this.loadProduct[0]
    if (this.targetEntityIndex >= targetId.length) {
      this.targetEntityIndex = 0
    }
    const targetEntity = this.getEntityById(targetId[this.targetEntityIndex])
    this.targetEntityIndex++
    if (targetEntity && targetEntity.canReceiveProduct(sourceId, product)) {
      this.pretreatmentTargetPosition(targetEntity, targetEntity.x, targetEntity.y)
      this.moveType = 'deliverGoods'
      if (this.agvStatus === 'stop') {
        this.agvStatus = 'move'
        this.moving()
      } else {
        this.moving()
      }
    } else {
      schedule(1, () => this.startDeliverGoods(), 'agv find idle device')
    }
  }

pretreatmentTargetPosition方法,在移动前预先处理目标位置和路径

  public pretreatmentTargetPosition(station: BaseStation, x: number, y: number) {
    // 如果agv已经绑定了交通网络,则使用交通网络进行移动
    if (this.enableTraffic) {
      //先找到目标点位
      const targetPointData = TrafficNetwork.getAdjacentPoints([station.x, station.y])
      if (!targetPointData) return
      this.targetPoint = targetPointData.id

      //寻找最优路径
      this.planPath = TrafficNetwork.getOptimalPath(this.currentPoint, this.targetPoint)
      if (this.planPath.length > 1) {
        //去掉第一个元素
        this.planPath.shift()
      }
      const nextPoint = this.planPath[0]
      const nextPointPosition = TrafficNetwork.getPositionById(nextPoint)
      if (nextPointPosition.length < 2) {
        new Error(`AGV 路径错误`)
      }
      this.targetPosition = { x: nextPointPosition[0], y: nextPointPosition[1] }
    } else {
      this.targetPosition = { x, y }
    }
  }

开始移动

  //移动
  public moving() {
    if (this.agvStatus === 'stop') return
    if (simController.getStatus() !== 'running') return

    // 计算当前位置和目标位置的距离
    const dx = this.targetPosition.x - this.x
    const dy = this.targetPosition.y - this.y
    const distanceToTarget = Math.sqrt(dx * dx + dy * dy) // 直线距离

    // 计算目标角度(弧度转换为度数)
    const angleInRadians = Math.atan2(dy, dx) // 返回的是弧度
    const angleInDegrees = (angleInRadians * 180) / Math.PI // 将弧度转换为度数
    this.currentAngle = angleInDegrees // 更新AGV的当前角度

    // 如果AGV已经到达目标位置,则停止移动
    if (distanceToTarget === 0) {
      this.arriveDestination()
      return
    }

    const timeInterval = 0.05
    const distancePerStep = this.speed * timeInterval * SimulationSpeed.getSpeed // 计算每50ms AGV可以移动的距离

    // 如果目标距离小于AGV在50ms内能走的距离,就直接到目标位置
    if (distanceToTarget <= distancePerStep) {
      this.x = this.targetPosition.x
      this.y = this.targetPosition.y
    } else {
      // 计算单位向量
      const moveRatio = distancePerStep / distanceToTarget
      this.x += dx * moveRatio
      this.y += dy * moveRatio
    }

    this.loadProduct.forEach((product) => {
      messageTransfer('product', 'transport', {
        targetId: this.id,
        productId: product.product.id,
        x: this.x,
        y: this.y,
        angle: this.currentAngle,
        duration: 0
      })
    })
    messageTransfer('agv', 'transport', {
      targetId: this.id,
      x: this.x,
      y: this.y,
      angle: this.currentAngle
    })
    setTimeout(() => {
      this.moving()
    }, 50)
  }

移动到达目的地后

  //到达目的地
  public arriveDestination() {
    this.agvStatus = 'stop'

    /**
     * 到达目的地后,判断agv是否绑定了交通网络
     */
    if (this.enableTraffic) {
      if (this.currentPoint !== this.targetPoint && this.planPath.length > 1) {
        this.currentPoint = this.planPath[0]
        this.planPath.shift()
        const nextPoint = this.planPath[0]
        if (nextPoint) {
          const nextPointPosition = TrafficNetwork.getPositionById(nextPoint)
          this.targetPosition = { x: nextPointPosition[0], y: nextPointPosition[1] }
          //继续移动
          this.agvStatus = 'move'

          this.moving()
          return
        }
      }
    }

    if (this.moveType === 'pickupGoods') {
      const sourceEntity = this.getEntityByPosition()
      if (sourceEntity && dispatcher.isDeviceInBlockOrSpecial(sourceEntity)) {
        sourceEntity.tryDispatchCurrentProduct()
      }
    } else if (this.moveType === 'deliverGoods') {
      //派发货物
      this.tryDispatchCurrentProduct()
      /**
       * 如果还有货物则继续送货
       */
      if (this.loadProduct.length > 0) {
        this.startDeliverGoods()
      } else if (this.needLoadGoodsEvents.length > 0) {
        /**
         * 如果没有货物了,则检查是否有需要装载货物的事件
         */
        this.handleLoadGoodsEvent()
      }
    }
  }

派发货物

  //尝试派发当前货物
  public tryDispatchCurrentProduct(): void {
    if (this.loadProduct.length === 0) return
    const productItem = this.loadProduct[0]
    const targetEntity = this.getEntityByPosition()
    if (targetEntity && targetEntity.canReceiveProduct(productItem.sourceId, productItem.product)) {
      const product = productItem.product
      this.loadProduct.shift()
      //当前产品添加到就绪产品队列中
      addReadyProduct(product)
      targetEntity.receiveReadyProduct(product.id)
    }
  }

AGV的篇章到这结束,谢谢大家