一:简介
AGV(自动导引运输车)是现代工业物流中不可或缺的核心装备之一,其重要性主要体现在以下几个方面:
- 核心价值:AGV是连接生产、仓储、配送等各个环节的“自动化血脉”。它无需人工驾驶,可以24/7不间断工作,是实现“黑灯工厂”和全自动化物流的关键组成部分。
- 智能调度:通过与上位管理系统(如WMS、ERP、MES)集成,AGV可以接收指令,智能规划最优路径,实现物料、半成品、成品的自动、精准、高效流转。
AGV的重要性可以概括为:它是工业4.0和智能制造的基石之一,通过将“物”的流动自动化、数字化和智能化,从根本上提升了现代工业物流的效率、柔性和可靠性,是企业降本增效、提升核心竞争力的关键工具。
二:仿真软件中的应用
在仿真软件中AGV可以有多种外观,例如搬运机器人,叉车,货车,飞机。甚至可以把人看作AGV的一种。
在正常情况下,如果两台设备相连接后,如果不指定搬运设备,那么线的箭头就是橙色
只有当我们指定了搬运设备,箭头就会变成蓝色
此时加工站的下一站就不再是加工站2,而是AGV,当加工站派发产品时,会将产品派发给AGV上面
并且在AGV内部会维护一张设备连接表,当我们收到加工站的产品时,会从这张表里面去找,应该将产品送到哪台设备上
当我们的AGV有了起点和终点后,我们就可以去拿货或者是送货,此时就需要判断我们的AGV是否绑定了交通
- 如果绑定了交通,那么AGV就必须按照道路规划的路线来走
- 如果没有绑定,那么就可以按照直线的方式过去
那接下来我们来看在代码里面如何实现AGV功能
三:AGV代码实现
设计思路
AGV和其他节点一致,也是继承了BaseStation类,总结来说,流程如下
主线流程
判断是否可以接收产品,此时不管能不能接收新产品,都需要将接收任务暂存起来
如果可以接收新产品,则移动到设备位置进行接收
移动到设备位置后,再次调用设备派发,拿到新产品
拿到新产品后,我们需要再次查看是否还有装载产品的任务,如果有,就再次执行2,3步骤
在送货前,还需要做一些前置处理,需要先判断有哪些设备处于空闲状态,如果有,就随机挑选一台,如果没有,就间隔一秒钟后,再次执行第5步骤
根据AGV是否绑定了交通,我们给AGV规划一条路线,此时将AGV的业务逻辑改为送货,然后开始移动
产品派发后,我们再次判断AGV上面是否还有产品,如果有,则执行5,6步骤,如果没有,则判断取货列表里面是否有任务
移动流程
判断当前状态是否等于stop,如果等于,则停止移动
计算当前位置和目标位置的直线距离,计算当前位置和目标位置的角度
如果剩余距离为0,则表示已到达目标点,停止移动
根据AGV的速度,和仿真速度,计算出50mm移动的距离
如果剩余距离小于移动距离,则AGV将直接移动到目标点上
当移动到目标点上后,将状态切换成stop,此时再判断AGV是否绑定了交通
如果没有绑定交通,我们就需要根据当前的业务状态,来判断是取货还是送货,如果是取货,则调用设备,进行产品派发,如果是送货,则自身派发产品
如果绑定了交通,我们就需要判断最优路径列表是否已经走完,走完表示我们已经到达了目标点,我们就需要提取到路径表里面下一个点位,然后再次循环移动流程
那接下来就一步一步看代码怎么实现的
代码实现
我们需要先给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的篇章到这结束,谢谢大家