本章讲解工业仿真领域里面的传送带
传送带简介
传送带的类型大致分为两种
- 皮带式传送带
- 滚筒式传送带
我们先对这两个传送带进行一个简短的介绍
1. 皮带式传送带
这是最常见、最经典的传送带类型,由一根连续的环形带套在两个或多个滚筒上,由一个滚筒(驱动滚筒)提供动力。
在仿真逻辑上,如果传送带停止运行,则上面的产品全都停止移动,并且上面的产品可以看作一个整体,牵一发而动全身
在实际应用中,皮带式的特点:结构简单、工作平稳、噪音小、输送能力强。
皮带式传送带
2. 滚筒式传送带
由一系列按一定间距排列的滚筒组成,分为动力滚筒线(由电机驱动)和无动力滚筒线(依靠人力或重力)
在仿真逻辑上,如果后面的产品停止了移动,则前面的产品依旧可以进行传送,每个产品都是独立移动的,互不干扰
在实际应用中,适用于输送底部平坦、刚性的货物(如托盘、纸箱、周转箱),易于实现分流、合流、积放等功能。
滚筒式传送带
本章节我们主要讲滚筒式传送带
设计思路
- 传送带和加工站一样,都需要继承基类BaseStation.ts
- 传送带的执行逻辑是【接收产品--产品开始移动--派发产品】
- 2.1 如何接收产品:只需要判断传送带上剩余的空间是否还可以容纳一个产品
- 2.2 移动和停止:给定单位时间内(假设是50毫秒),结合传送带速度和仿真速度,计算出产品的移动距离,并判断和下一个产品(也有可能是终点)的间距是否大于这个移动距离,如果大于移动距离,则当前产品可以移动到目标点位,如果小于移动距离,则产品只需要和下一个产品紧贴即可
- 2.3 如何派发产品:若有产品的移动距离大于间距,则说明一定有产品到达了终点,此时我们只需要拿到产品队列里面最后面的一个产品,该产品就是需要派发的产品
有了设计思路后,我们接下来就着手代码开发
代码开发
首先我们需要先知道,在画布中,传送带的本质是一个svg的path路径,格式如下
M 212 150 L 212 247 C 212 260.33 218.67 267 232 267 L 519 267 C 532.33 267 539 260.33 539 247 L 539 170 C 539 156.67 532.33 150 519 150 L 364 150
那么我们就需要借助第三方工具,来实现下面的功能
- 根据相对点位(0-1)来计算出绝对坐标
- 根据相对点位(0-1)来计算出该点位的切面角度
- 计算path的总长度
我在查找后,确定第三方工具
npm i svg-path-properties
实现功能的代码如下
import { svgPathProperties } from 'svg-path-properties'
/**
* 获取 SVG 路径上指定相对位置的坐标和角度
* @param path SVG path 字符串
* @param t 相对位置 (0 ~ 1),如 0.33 表示 33%
* @returns 坐标点 { x, y } 和角度 angle(单位:度)
*/
export function getPointAndAngleAt(
path: string,
t: number
): {
x: number
y: number
angle: number
} {
if (t < 0 || t > 1) {
throw new Error('参数 t 必须在 0 到 1 之间')
}
const properties = new svgPathProperties(path)
const totalLength = properties.getTotalLength()
// 获取指定位置的坐标点
const point = properties.getPointAtLength(totalLength * t)
// 获取指定位置的切线方向
const tangent = properties.getTangentAtLength(totalLength * t)
const angle = Math.atan2(tangent.y, tangent.x) * (180 / Math.PI) // 转换为角度
return {
x: point.x,
y: point.y,
angle
}
}
export function getLength = (path:string) =>{
const svgProperties = new svgPathProperties.svgPathProperties(path)
const length = svgProperties.getTotalLength()
}
那接下来我们来编写传送带实体类
1. 确定属性
由于Conveyor类继承了BaseStation基类,所以除了基本的 id 和name 外,还需要下面的属性值
path: string //path字符串
speed: number //速度
length: number //总长度
status: 'idle' | 'processing' = 'idle' //当前状态
startPointer: number = 0 //传送带开始的相对点位
endPointer: number = 1 //传送带结束的相对点位
readyList: Product[] = [] //传送带接收到新产品后,会将产品先存入到readyList中,然后会立即将readyList里面的产品放入到productLinkedList中进行移动
productLinkedList: { pointer: number; product: Product }[] = [] //传送带上产品的队列
reachProduct: null | Product = null //已到达终点的产品
2. 接收产品
//接收产品
public canReceiveProduct(id: string, product: Product): boolean {
//获取产品宽度
const productWidth = product.width
//如果产品队列为0,则直接返回true,表示可以接受产品
if (this.productLinkedList.length === 0) {
return true
}
//获取产品队列里面首个产品的位置
const startItem = this.productLinkedList[0]
//计算首个产品的开始点位
const startPointer = startItem.pointer - productWidth / 2 / this.length
//计算剩余空间
const remainingLength = startPointer * this.length
if (remainingLength < productWidth) {
return false
} else {
return true
}
}
逻辑思路大概就是这样
- 如果传送带上没有产品,就直接接收
- 如果有产品,则获取最前面的产品,然后计算该产品与开始位置之间还有多少距离,如果该距离大于接收产品的宽度,则接收新产品,如果小于,则放弃
然后放入到产品队列里面
//接受就绪产品
receiveReadyProduct(productId: string): void {
this.readyProduct = productId
const product = getReadyProduct(productId)
if (!product) {
this.setStatus('idle')
console.log(`[${currentTime}] ❌ ${productId} 没有发现`)
return
}
console.log(`[${currentTime}] ${product.id} 到达 --> ${this.name}`)
product.setFrom(this.id)
this.onProductReceived(product)
}
public onProductReceived(product: Product): void {
const initPosition = this.startPointer + product.width / 2 / this.length
const position = getPointAndAngleAt(this.path, initPosition)
messageTransfer('product', 'transport', {
targetId: this.id,
productId: product.id,
x: position.x,
y: position.y,
angle: position.angle,
duration: 0
})
this.readyList.push(product)
// this.putInProduct(product)
if (this.status === 'idle') {
this.setStatus('processing')
this.moving()
}
}
3. 产品移动
产品移动的大致思路如下
- 先派发产品,防止产品堆积
- 从readyList里面拿到就绪的产品,然后放入到产品队列里面
- 算出50mm内产品的移动距离
- 从后向前遍历产品队列
- 根据当前产品相对位置,再根据下一个产品的相对位置,计算出他们之间的间距,如果没有下一个产品,就拿终点作为写一个点位。
- 判断间距和移动距离的差值
- 如果间距大于移动距离,则产品移动到目标点位
- 如果间距小于移动距离,则判断是否是最后一个产品,如果是,则该产品修改为到达产品(reachProduct),如果不是,则该产品和下一个产品紧贴
//开始移动
private moving(): void {
if (this.status !== 'processing') return
if (simController.getStatus() !== 'running') return
//先派发产品
this.tryDispatchCurrentProduct()
//在拿首位产品
const firstProduct = this.readyList.shift()
if (firstProduct) {
this.putInProduct(firstProduct)
}
if (this.productLinkedList.length === 0) {
this.setStatus('idle')
return
}
//算出移动距离
const distance = ((this.speed / 20) * SimulationSpeed.getSpeed) / this.length
for (let i = this.productLinkedList.length - 1; i >= 0; i--) {
const item = this.productLinkedList[i]
//下一个产品的位置
let nextProductPointer = null as null | number
if (i === this.productLinkedList.length - 1) {
nextProductPointer = this.endPointer - item.product.width / 2 / this.length
} else {
nextProductPointer =
this.productLinkedList[i + 1].pointer -
(item.product.width / 2 / this.length +
this.productLinkedList[i + 1].product.width / 2 / this.length)
}
//计算间距
const pointerDistance = nextProductPointer - item.pointer
//如果间距大于移动距离,则移动
if (pointerDistance > distance) {
this.productLinkedList[i].pointer += distance
//计算移动时间
// const duration = (distance * this.length) / ((this.speed / 20) * SimulationSpeed.getSpeed)
const position = getPointAndAngleAt(this.path, this.productLinkedList[i].pointer)
messageTransfer('product', 'transport', {
targetId: this.id,
productId: item.product.id,
x: position.x,
y: position.y,
angle: position.angle,
duration: 0
})
}
//如果间距小于移动距离,则需要和下一个产品紧贴,并计算好移动距离和时间
else {
const remainingSpacing = ((): number => {
if (i === this.productLinkedList.length - 1) {
const spacing = this.endPointer - (item.pointer + item.product.width / 2 / this.length)
this.reachProduct = item.product
return spacing
} else {
//下一个产品
const nextProduct = this.productLinkedList[i + 1]
//算出剩余间距
const spacing =
nextProduct.pointer -
nextProduct.product.width / 2 / this.length -
(item.pointer + item.product.width / 2 / this.length)
return spacing
}
})()
if (remainingSpacing < 0.001) continue
this.productLinkedList[i].pointer += remainingSpacing
//计算移动时间
// const duration =
// (remainingSpacing * this.length) / ((this.speed / 20) * SimulationSpeed.getSpeed)
const position = getPointAndAngleAt(this.path, this.productLinkedList[i].pointer)
messageTransfer('product', 'transport', {
targetId: this.id,
productId: item.product.id,
x: position.x,
y: position.y,
angle: position.angle,
duration: 0
})
}
}
setTimeout(() => {
this.moving()
}, 50)
}
4. 派发产品
派发产品的思路和加工站实体类一致,都是遍历下一站所有设备,然后看哪个设备可以接受产品
public tryDispatchCurrentProduct(): void {
if (this.reachProduct === null) return
const productId = this.reachProduct.id
for (const next of this.nextStations) {
if (next.canReceiveProduct(this.id, this.reachProduct)) {
addReadyProduct(this.reachProduct)
next.receiveReadyProduct(productId)
this.reachProduct = null
this.removeProduct()
break
}
}
}
传送带的设计与开发就讲到这里,谢谢大家