调度模拟模型类库
车辆监控类
用于模拟和控制一个移动车辆,可能是一个 AGV(自动导向车)。包含了车辆的一些属性,例如位置、速度、状态等信息,以及一些方法用于控制车辆的行为,例如启动、停止、移动等。
构造函数初始化了车辆的一些基本属性,例如状态、位置、速度等,以及设定了一个定时器 车辆激活定时器,用于周期性地调用 移动方法使车辆移动。
代表车辆是否被锁定标志的布尔变量、车辆的编号、当前和下一地标信息对象、车辆运行的距离、车辆的当前路径(地标列表)
移动方法:使车辆按预设路径移动,首先禁用 车辆激活定时器以防止移动过程中被再次调用。然后如果存在可用的当前路径和一个大于0的缩放率,它将使车辆沿当前路径的地标移动,移动完成或者没有可用路径时,移动方法将再次启用 车辆激活定时器,并在捕获到异常时抛出。
发送移动方法:创建了一个新的任务用于调用步点改变事件,在车辆的位置改变后,步点改变事件被触发。
停止和开始方法:分别用于停止和开始车辆的电影。在停止方法中,是否移动布尔变量被设置为false,并禁用车辆激活定时器。而在开始方法中,布尔变量是否移动被置为true并启用车辆激活定时器。
最后,车辆激活方法在每个车辆激活定时器事件中被调用,其本质上是一个异步操作用于调用移动方法来使车辆移动。
1.1 构造函数
初始化:是否锁定为false,状态码为0【0-待命 1-运行中 2-停止】,当前地标信息、下一地标信息、当前路线中的地标信息列表、路线中地标的代码字符串列表、转弯地标的代码列表、待命地标代码、执行的任务编号、机械臂所在地标的代码、速度0.02(单位不详)、当前位置编号-1,创建车辆激活计时器且初始状态为禁用、绑定计时器的Elapsed事件与车辆激活方法、设置计时器的触发间隔为50毫秒。
1.2 类属性定义
车辆是否被锁定、车辆的AGV ID、当前地标信息、当前站点、下一站点、车辆当前X坐标、车辆当前Y坐标、车辆行驶距离、车辆速度、下一地标信息、车辆状态、当前路线(地标列表)、路线中地标代码列表、转弯地标代码列表、待命地标代码、执行的任务编号、是否返回布尔变量、任务详情ID、机械臂地标、操作类型、放置类型、缩放比例、步点改变委托、步点变化委托的实例即步点变化事件、车辆激活定时器、车辆是否移动中
1.3 移动方法
移动方法
{
尝试 :
在移动前先禁用计时器
当小车正在移动且缩放比率大于0时
{
如果当前路线中存在地标
{
对当前路线的地标列表去重
如果当前坐标与最终目标地标在一定误差范围内
{
获取当前开始时间
更新当前地标为当期路线的最终地标
更新X坐标
更新Y坐标
清空下一地标信息
更新当前站点编号
运行距离清零
清空当前路线
更新状态为待命
禁用计时器
调用发送移动方法,更新UI
返回,结束移动昂发
}
如果当前状态是待命
{
记录开始时间
更新当前地标为第一个路线地标
运行距离清零
更新X坐标
更新Y坐标
更新旧的站点为当前站点编号
更新当前站点编号
查找下一个路线地标
确保下一地标不越界
更新状态为运行中
}
否则
{
查找当前坐标一定范围内的地标
如果找到地标且下一地标存在,且找到的地标与下一地标的地标码一致
{
更新X坐标为找到的地标
更新Y坐标
查找地标在路线中的索引
更新当前地标
更新旧的站点为当前站点编号
更新当前站点编号
查找下一路标地标确保不越界
运行距离清零
更新状态为运行
记录结束时间
}
否则
{
记录开始时间
如果当前地标与下一地标都非空
{
更新运行距离
更新状态为运行
}
}
}
调用发送移动方法更新UI
}
否则
{
更新状态为待命
清空路线地标代码列表
清空转弯地标代码列表
释放资源
禁用计时器
}
}
最终启用计时器
捕获异常,向上传递异常
}
1.4 发送移动方法,用于异步的触发(步点改变事件非空)步点改变的事件
1.5 停止方法,用于停止小车移动
设置车辆是否移动中为false、禁用计时器
1.6 启动方法:启动小车移动
设置是否移动中为true,开启车辆激活计时器
1.7 车辆激活方法
线程休眠25毫秒,调用移动方法。
2. 日志记录类
用于记录错误、运行日志、各类操作或事件的信息
类中有两个锁,错误锁对象、锁对象,用于保证记录日志的线程安全。
写入错误日志方法:用于记录错误日志,输入是异常对象,将异常消息和堆栈跟踪内容写入到以日期命令的错误日志文件中。
写入日志方法:记录运行日志,输入是一个字符串,它将创建一个新任务并试图将输入字符串写入到一个以日期命名的日志文件中。
写入交通管制信息日志、写入接收AGV指令消息日志、写入发送AGV消息日志、写入接收充电桩命令日志、记录发送充电桩命令的方法、记录掉线日志的方法、记录充电日志的方法、记录所有AGV小车的报警信息的方法、记录任务生成日志的方法、记录按钮盒操作日志的方法、删除五天前的日志的方法。
用于记录日志的静态方法。这些方法用来记录应用程序运行时产生的各类信息,比如错误日志、运行日志、和特定的交通管制日志等等,通过异步线程写入文本文件并加锁以确保线程安全。同时,类中提供了一个方法用于删除五天前的日志文件,以管理日志空间。每个记录方法都进行了异常捕获,但没有进一步处理或抛出异常。这是一个典型的日志助手类,用于辅助软件日志管理。
3. 路线规划数据类
管理和计算导航路径规划。
属性: 获取路线的锁对象、最终路径(地标列表)、关闭列表(已经被考虑过的地标)、路径规划过程中关键点及其备选(stack)、地标哈希表、所有路段信息列表、开始地标、结束地标、所有地标列表、静态锁对象
方法: 构造函数(初始化路线的所有路段信息)、获取起始和节数地标之间的最短路径、修复路径使它成为最优路径、根据给定的起始和结束地标计算路径、获取前一个地标的所有邻接地标、计算两个坐标点之间的距离、创建对象的深拷贝(用于复制一份地标或其他对象)、计算方向为地标分配方向和角度信息、返回路径的字符串格式、计算三点之间的角度。
这个类主要是为AGV提供最短路径规划,确保AGV能够高效准确地在各地标间导航,它通过多种内部方法完成路径的计算、优化和方向调整,并可以对路径进行深拷贝和转换为字符串格式。此外,还通过异常处理和日志记录帮助开发人员诊断和调试问题。在路径规划中,它会考虑路径的多样性、效率以及车辆的实际行驶方向和角度。
3.0 构造函数
输入参数:线段列表。初始化线段列表、初始化所有地标列表。
3.1 属性 打开的节点数量属性-所有地标数
3.2 修复路线的方法
{
尝试:
是否已修复置为false
计算当前路线的每个节点的导向,复制给最终路径地标列表
创建原始路线,对最终路线列表的深拷贝
找到路线中所有分支地标,即专项为None的地标
遍历所有分支地标i
{
获取第i个地标
遍历所有i+1后的分支地标j
{
获取第j个地标
若两个分支点都非空
{
从原始路线中提取这两个地标之间的段,得到旧的路线段
清空关闭列表
清空路线列表
重新计算两个分支地标之间的最短距离
如果新路线比原路线段,则替换原来的路线段
{
原始路线-移除原始路线段
原始路线-插入新的路线段
标记已修复
跳出循环
}
}
}
如果已修复,则结束循环
}
如果有修复
{
创建修复后原始路线的深拷贝,赋值给最终路线地标列表
迭代调用修复路线的方法
}
否则
{
创建修复后的原始路线深拷贝赋值给最终路线地标列表
}
捕获异常,记录异常日志
}
3.3 获取从起点到终点的路线
输入起始地标、终点地标
尝试
{
锁定,确保同时只有一个线程可以访问此段代码,防止数据竞争
{
判断起点或终点是否为空,是则返回空列表
清空当前路径列表、关闭列表、以及分支点堆栈
创建用于存储可能路径的列表 (地标列表的列表)
计算从起点到终点的路线
将计算出的路线深拷贝加入路线列表
创建原始路线和关闭列表的深拷贝
创建分支点堆栈的深拷贝
遍历所有分支路径点
{
清空并重新设置分支点堆栈、路线列表、关闭列表
获取当前分支点
尝试删除该分支点会后的路径,准备重新计算
重新计算分支点到终点的路线
添加新计算的路径到可能路径列表
}
从所有可能的路径中找到包含终点地标且节点数最少的路线
如果没有找到有效路线,则返回空列表
对找到的路线深拷贝赋值给路径列表
对路径列表进行方向计算并返回
}
}
捕获异常,记录异常日志,返回空路线列表
3.4 计算起点到终点的路线
该函数是一个递归函数,用于计算从起始地标到终点地标的最佳路线。该方法将结合路线分段信息,关闭列表,和其他路径选择来逐步构建最终的路线。在计算过程中,它会检查邻接地标,根据距离终点的距离来排序邻接地标,并在必要时备份和尝试其他路线选择。这种算法在寻找复杂路径和处理分支路线时非常有用,特别适用于自动导引车(AGV)系统中的路径规划
尝试
{
如果存在线段
{
定义前一个地标为起始地标
如果关闭列表的数量小于打开的节点数量且起点和终点不相同
{
如果起点不在关闭列表中,则将其添加到路线列表和关闭列表
获取当前节点的下一个节点列表
如果没有找到下一个节点
{
如果存在其他的分支节点
{
移除当前节点到列表末端的路线部分
更换当前节点为分支节点
}
否则
{
如果没有下一个节点也没有其他分支节点,则结束路径计算
}
}
如果找到一个下一个节点
{
如果这个节点不在关闭列表中
{
将其添加到路线列表中
将其添加到关闭列表中
}
设置当前节点为这个节点
}
否则(如果存在多个下一个节点)
{
对所有找到的下一个节点按与终点的距离进行排序
如果排序后没有节点,则抛出异常
否则
{
如果排序后的第一个节点不在关闭列表中
{
将其添加到路线列表
将其添加到关闭列表
将剩余的节点作为分支节点加入到备选地标堆栈
}
}
设置当前节点为这个节点
}
}
递归调用自身,继续计算下一个节点到终点的路线
}
}
捕获异常,抛出
3.5 获取下一个地标的方法
创建一个新的地标列表
找到所有以前一地标作为起点的线段
遍历每个线段
{
如果该线段额结束地标没有包含在关闭列表中,则选取它
{
从所有地标的列表中找到该线段的结束地标
将找到的结束地标添加到地标列表中
}
}
返回找到的所有下一个地标列表
3.6 获取路线分支中的另一个地标的方法
创建一个新的地标列表
找到所有以前一地标作为起点的线段
遍历每个线段
{
如果该线段的结束地标没有包含在当前路线中,则选取它
{
从所有地标的列表中找到该线段的结束地标
将找到的结束地标添加到地标列表中
}
}
返回找到的所有下一个地标列表
3.7 计算两点之间的距离的方法
3.8 深拷贝对象的方法
定义类型为T的变量
创建一个内存流
使用二进制格式化器创建对象的深拷贝
序列化对象到内存流中
把内存流的位置重置为0
反序列化内存流中的数据到一个新的对象
返回新创建的对象的深拷贝
3.9 计算转向和前进后退的方法
尝试
{
如果路线中的地标数量小于等于1则不进行计算
加载所有线段信息
加载AGV坐标信息
初始化相关变量
获取系统参数中关于计算转弯类型的配置
如果路线地标数3个以上
{
从第二个开始遍历地标
{
获取上一地标、拐点地标、下一地标
除了最后一段外,计算每一段的转向和角度
{
如果计算方式不是基于角度
{
获取当前路段
如果找到路段且路段中有转向指示,则设置转向
否则
{
如果存在多个以当前地标为起点的路段,则计算转向
{
计算几何关系,用于判断左转或右转
根据值判断转向,设置枚举值
}
}
如果路段中有移动指示,则设置移动方向
}
否则-如果计算方式是基于角度
{
如果上一地标和下一地标的X坐标想通,表示垂直方向移动
{
找到对应的路段
如果路段中有角度指示,则设置角度
否则
{
计算默认角度,根据Y坐标判断是向上还是向下
}
}
如果上一地标和下一地标的Y坐标相同,表示水平方向移动
{
找到对应路段
如果路段中有角度指示,则设置角度
否则
{
计算默认j角度,根据X坐标判断是向右还是向左
}
}
}
}
否则
{
判断是否按照角度来计算转向
{
获取上一地标和拐点之间的线段信息
如果线段存在并定义了转向,直接赋值转向
否则
{
如果有多个起始于该上一地标的路段,进行转向计算
}
如果线段信息中有移动方向指示,更新移动方向
获取拐点和下一地标之间的线段信息
如果线段存在并定义了转向,直接赋值转向
如果线段信息中有移动方向指示,更新移动方向
}
}
否则,如果按照角度来计算
{
如果拐点和下一地标在X坐标上相同,说明在Y轴方向上移动
{
略
}
如果拐点和下一地标在Y坐标上相同,说明在X轴方向上移动
{略
}
}
}
}
}
捕获异常
3.10 得到路线的地标集合
创建路线的深拷贝
结果路线的深拷贝
遍历拷贝的路线
{
检查是否有转向信息
{
获取除当前地标外所有地标按距离升序排列的表
获取当前地标在结果路线中的索引
遍历最近的地标
{
最多插入四个地标
如果结果路线中没有该地标,则插入到结果路线中
}
}
}
获取去重后地标代号字符串,并转换为列表
返回地标集合字符串列表
3.11 计算三点之间的夹角
4. 模拟器类
管理模拟环境中的车辆(AGV)的状态、路径规划、任务分配等。
属性:存放所有小车的基础状态信息列表、所有线段信息列表、监控的小车列表、所有地标信息列表、路径规划数据类实例、交通控制器实例、系统信息的键值对集合、小车移动委托、小车初始化委托、小车移动事件、小车初始化事件、小车定时器、定义锁对象(停止锁、启动锁、处理任务锁)、锁资源列表、储位信息静态列表、任务刷新定时器。
方法:初始化模拟器,加载AGV车辆、地标、线段等信息、并启动定时任务。停止模拟器,禁用定时器并清除车辆占用资源。创建任务,支持通过呼叫盒触发的任务创建和点对点的任务创建。负责定时检查并分配任务给AGV车辆。定时启动因交通控制而停止的车辆。处理车辆到达新位置时的事件和数据更新。
类的设计用于模拟AGV车辆的调度、路径规划和任务分派。通过定时器周期性检查任务分派情况和交通管制状态,确保AGV车辆能够根据分配的任务高效移动。此外,还能够根据按钮盒输入创建指定的任务,实现与物理按钮盒交互。
4.1 初始化模拟器,清空监控的小车列表,加载小车、地标、线段以及储位信息,初始化路径规划和交通控制器,设置并启动定时任务。
清空小车监控列表,准备重新加载小车信息。
加载小车档案
加载地标信息
加载所有线段
同步线段到静态变量,以便在其他地方共享使用。
加载储位信息到字典中。
初始化车辆监控列表:遍历车辆集合,为每一个小车档案创建一个车辆监控器 对象,并设置相关属性(如AGV ID、当前站点、站点地标、缩放比例等)。根据系统参数中的缩放比例和地标信息,计算小车的实际坐标,并将这些对象添加到 监控车辆列表中。
触发小车初始化事件:如果 车辆初始化事件不为空,则触发该事件,传递初始化完成的小车集合 监控车辆列表。
初始化路线规划数据:使用加载的所有线段信息实例化路径规划数据类实例。
初始化交通控制器:使用小车监控列表、所有线段、系统参数和所有地标初始化一个交通控制器对象。
启用并配置定时任务:配置 计时器以启动被交通管制停止的小车,设置为每秒 (1000ms) 触发一次。配置定时刷新任务计时器以分派任务,设置为每两秒 (2000ms) 触发一次。并为这两个计时器分别绑定事件处理函数
完成以上步骤后,如果过程中没有发生任何异常,方法将返回 true,表示初始化成功。否则,捕获异常并返回 false。
4.2 停止模拟器,禁用定时任务,释放所有小车资源。
4.3 根据呼叫盒的ID和按钮ID创建任务。任务的创建依据呼叫盒的详细配置。
根据呼叫盒ID加载呼叫盒信息,加载呼叫盒详细信息,按照按钮ID和呼叫盒ID加载当前呼叫盒详细信息。
如果当前呼叫盒操作类型为呼叫,加载任务详细信息,开始创建任务,根据当前呼叫盒位置ID找到储位信息,检查是否允许创建任务,生成任务号,创建任务信息实体,创建任务明细,保存任务并返回操作结果。如果操作成功则锁定相关的储位。
如果操作类型为监控,更新储位状态。重新获取当前按钮详细信息,寻找与当前按钮关联的储位信息,设置储位存储状态,更新储位存储状态。
如果操作类型为放行,寻找与放行操作关联的储位信息,查找需要放行的小车,执行放行操作并获取操作结果。
4.4 创建从一个地标到另一个地标的任务并保存这个任务。
生成一个新的GUID作为任务编号(调度号),创建一个新的任务信息实例(任务编号、类型、状态、测试用的呼叫地标、站点编号)、创建任务的第一个明细,表示任务的起始地(调度号、第一个任务明细的ID、起始地标编号、任务明细的状态、是否允许执行,添加到任务信息中),创建任务的第二个明细,表示任务的目的地(第二个任务明细的ID,目的地标编号、任务明细的状态、是否允许执行,添加到任务信息中),调用数据访问层的方法保存任务信息,并接收操作返回信息。
4.5 定时任务事件,触发任务分配。
暂停任务刷新定时器,上锁以保护任务分派逻辑,调用分派任务的方法。无论如何最后都要重新启动任务刷新定时器。
4.6 分派任务到小车,这个过程涵盖了任务的领取和执行的多个步骤,包括查询当前有效任务,找到合适的AGV执行任务,并对这些任务进行排队和释放。
使用lock关键字确保此段代码同一时间只被一个线程访问
{
临时禁用任务刷新定时器
遍历监控的所有小车,更新执行了任务且完成的小车状态
{
如果有执行中的任务且任务明细ID不为-1,则更新任务
{
如果小车待命并且当前站点为机械臂地标
{
更新储位状态
更新任务明细和主任务状态
检查任务是否完成
}
}
}
查找并分配当前有效任务
如果任务非空并且任务数大于0
{
遍历所有调度任务信息
{
跳过没有任务明细的任务
寻找合适的AGV领取任务
查找具体要执行的任务明细
如果任务状态为待处理(0)
{
如果任务明细非空
{
如果任务已经分配了AGV
{
查找已经分配给此任务的空闲AGV
}
否则查找任何一个空闲的AGV
}
}
如果任务状态为正在执行(1)
{
查找执行当前任务的AGV(无论其状态如何)
}
如果找到了合适的AGV并其位于有效地点
{
如果该AGV处于返回状态,将其标记为非返回状态
}
否则
{
如果没有找到合适的AGV,跳过当前轮次任务分配
}
如果找到合适的AGV,将执行任务相关操作
如果任务明细全部完成,那么需要将主任务状态更新并且让对应的agv回待命点
如果没有任务明细,代码不执行任何操作
否则
{
如果AGV正好在任务地标上,并且任务明细处于待执行或正在执行状态
{
设定AGV的操作类型、放置类型、目标地标及执行的任务编号和明细ID
更新任务明细状态并解锁储位
跳过后续逻辑,开始下一轮循环
}
判断AGV当前的任务明细是否等待放行命令,不允许执行则跳过
若当前任务明细不允许执行,直接跳过
更新任务为执行状态,记录AGV的当前任务信息
基于AGV当前所在地标和任务地标,规划AGV的移动路线
如果起止地标非空
{
如果起始地和目的地确定,计算路线并为AGV设定路线
如果计算出的路线有效,且当前没有交通管制阻止AGV启动,则AGV根据设定的路线开始移动
}
}
}
}
否则
{
如果没有待分配任务,则清除未释放任务的小车状态
}
使没有任务且不在待命点的小车返回
遍历所有没有返回待命点的小车
{
如果小车没有设置待命点,跳过此次循环
查找小车当前所在地标和待命点对应的地标信息
如果找不到起始地标或结束地标,跳过此次循环
否则
{
为小车规划从当前位置到待命点的路线
如果没有规划到有效路线,跳过此次循环
如果当前没有交通管制阻止小车启动
{
请求交通资源
// 注释掉的代码是路线初始化回调,如果有必要可以启用
添加步进变化的事件处理
启动小车
}
}
}
}
}
捕获异常
无论如何最终
{
最终始终确保启用任务刷新定时器
}
4.7 定时任务事件,触发被交通管制的车辆的重新启动。
禁用计时器,创建一个新的后台线程来处理交通管制开始的逻辑。最终确保计时器重新启用。
4.8 在小车走完一个步点后触发,主要是触发车辆移动事件,并对于车辆位置变化处理交通管制。
尝试转换发送者为车辆监控器对象,如果转换成功,则处理小车相关数据。
4.9 异步处理小车数据更新,包括移动和交通管制的处理。
创建新的任务异步处理小车移动逻辑:执行小车移动方法,如果车的旧站点并非当前站点,处理交通管制停止逻辑。
4.10 计算两个地标之间的欧几里得距离。
4.11 当小车完成任务到达一个步点之后,会去更新对应储位的状态。
创建数据库操作实例,创建哈希表用于参数传递,设置锁定储位的AGV ID,设置地标代码,创建储位信息,如果小车有对应的装卸货地标并且当前地点就是该装卸货地标,查找对应的储位信息,如果找到,根据操作类型和放置类型更新储位状态(储位为有货、储位为预定 储位为空),解锁储位锁车信息,解除储位锁状态,将新的储位状态放入哈希表,在数据库中更新储位状态。
5. 模拟器变量类
在整个模拟器中共享某些变量。唯一的变量时路段信息列表。
6. 交通控制器类
用于模拟环境中的交通管制,主要用于处理AGV(Automated Guided Vehicle)在路径规划和运行中的交通冲突和管制问题。
属性:用于线程同步锁定对象、存储已锁资源的列表(用来记录那些资源如路径段已被哪些AGV占用)、当前交通管制器所管理的AGV列表、存储所有路径段信息、存储系统参数的字典、存储所有地标的列表。
方法:检查当前AGV是否需要停止(例如遇到交通冲突)、处理AGV在佳通管制下的停止逻辑、启动在交通管制下暂停的AGV、计算两点间的距离、计算当前车辆的行走资源集(如它所需要临时占用的路径段)、处理AGV是否可以继续行驶的逻辑、在启动AGV前检查是否存在交通管制的冲突、与AGV直接相关的交通管制逻辑提供支持的其他方法以确定是否可以继续行驶或需要停止。
6.1 构造函数
初始化,接收小车列表、所有路段、系统参数、所有地标信息
6.2 检查当前小车是否需要停止
判断同向,即我当前行走路线资源集中是否有车辆的当前位置
判断是否路线交叉,即我当前的行走路线资源集有交集的
判断是否有旋转,即我的行走资源中是否有其他车的旋转资源
判断管制区域内是否有车
同向必须停:如果当前车未锁定另一辆车,则添加锁定资源
如果发现我的行走路线中有需要旋转的车辆,并且旋转的车已经在旋转区域内,那么本车必须停
如果即将旋转的车还没在自己旋转区域内,并且当前车已经在即将旋转的车的旋转区域内,锁定对方
我的旋转区域内有车,我必须停,不管对方是否不是被我锁住的,也要被对方锁住。
处理路线交叉的情况,如果两车交叉处的路线重合,根据路线的进程确定哪辆车停。
6.3 管制后只发停止
使用停止锁, 确保传入对象非空,将传入对象转换为车辆监控器类型计算当前车辆的行走资源集和旋转资源集,如果车辆状态为1(运行状态),检查当前车辆是否需要停止,如果需要,调用停车方法,将车辆状态设置为锁定
6.4 管制后只发启动
使用启动锁,获取当前所有的锁定资源的深拷贝,遍历所有锁定资源,查找被该资源锁定的车辆,如果没有找到车辆,则继续下一个资源,处理当前被停止的车辆的行走资源集,查找当前车辆停止位置在其行驶路线中的索引(如果没找到索引,获取当前路线资源集的深拷贝,如果路线资源集中的地标数量不少于2个,清空原有的路线资源集,仅保留前两个地标,以简化处理逻辑,继续下一个锁定资源。),计算并更新车辆的行走资源集和旋转资源集,后略。
6.5 计算资源集方法
6.6 单独计算当前车的行走资源集合
6.7 单独计算当前车是否行走
6.8 单独判断交通管制
6.9 启动前判断交通管制
6.10 启动前判断是否可启动
作者陈晓永:智能装备专业高级工程师,软件工程师。机器人自动化产线仿真动画制作
The End