工业仿真(simulation)--传送带(3)

140 阅读7分钟

本章讲解工业仿真领域里面的传送带

传送带简介

传送带的类型大致分为两种

  1. 皮带式传送带
  2. 滚筒式传送带

我们先对这两个传送带进行一个简短的介绍

1. 皮带式传送带

这是最常见、最经典的传送带类型,由一根连续的环形带套在两个或多个滚筒上,由一个滚筒(驱动滚筒)提供动力。

在仿真逻辑上,如果传送带停止运行,则上面的产品全都停止移动,并且上面的产品可以看作一个整体,牵一发而动全身

在实际应用中,皮带式的特点:结构简单、工作平稳、噪音小、输送能力强。

皮带式传送带

2. 滚筒式传送带

由一系列按一定间距排列的滚筒组成,分为动力滚筒线(由电机驱动)和无动力滚筒线(依靠人力或重力)

在仿真逻辑上,如果后面的产品停止了移动,则前面的产品依旧可以进行传送,每个产品都是独立移动的,互不干扰

在实际应用中,适用于输送底部平坦、刚性的货物(如托盘、纸箱、周转箱),易于实现分流、合流、积放等功能。

滚筒式传送带

本章节我们主要讲滚筒式传送带

设计思路

  1. 传送带和加工站一样,都需要继承基类BaseStation.ts
  2. 传送带的执行逻辑是【接收产品--产品开始移动--派发产品】
  • 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

那么我们就需要借助第三方工具,来实现下面的功能

  1. 根据相对点位(0-1)来计算出绝对坐标
  2. 根据相对点位(0-1)来计算出该点位的切面角度
  3. 计算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
    }
  }

逻辑思路大概就是这样

  1. 如果传送带上没有产品,就直接接收
  2. 如果有产品,则获取最前面的产品,然后计算该产品与开始位置之间还有多少距离,如果该距离大于接收产品的宽度,则接收新产品,如果小于,则放弃

然后放入到产品队列里面

  //接受就绪产品
  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. 产品移动

产品移动的大致思路如下

  1. 先派发产品,防止产品堆积
  2. 从readyList里面拿到就绪的产品,然后放入到产品队列里面
  3. 算出50mm内产品的移动距离
  4. 从后向前遍历产品队列
  5. 根据当前产品相对位置,再根据下一个产品的相对位置,计算出他们之间的间距,如果没有下一个产品,就拿终点作为写一个点位。
  6. 判断间距和移动距离的差值
  7. 如果间距大于移动距离,则产品移动到目标点位
  8. 如果间距小于移动距离,则判断是否是最后一个产品,如果是,则该产品修改为到达产品(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
      }
    }
  }

传送带的设计与开发就讲到这里,谢谢大家