
⭕ 报文模式/数据流模式
协议要求通过对应报文的组包模型,将不同数据组合成字节流的形式。
⭕ 数据块读写模式
写入到PLC特殊存储单元,如DB块、寄存器等。
先看示例报文



该程序示例在系统“脚本样例”中,可以一边阅读本文档,一边结合程序代码学习。如果找不到该样例,请确保软件是最新版本。如果需要使用样例进行测试,请将其拷贝到CommuScript文件夹内。
对接任何一台Agv,首先不是盲目的写脚本程序,而是清晰的搞清楚以下信息,并且深刻理解协议:
1. 通讯方式
常见的通讯方式有Tcp/IP、Modbus、S7、Http及Mqtt等,这些是基础底层协议,本案例即是采用了Modbus-Tcp的通讯方式。采用不同的协议一般有不同的数据传输模式,常见如下:

通过字节数组报文进行数据交互,该模式编写较为复杂,对于各种数据类型与字节数组的转换应当特别注意!否则很容易出错。但是该方式通讯效率较高,一般用于PLC、嵌入式设备的控制器中。

数据块及寄存器模式不依赖于具体的数据报文,而是通过专用的工业通讯协议(Modbus、S7等),直接对PLC内部的数据存储单元进行读写。一般用于PLC控制器中。
- json字符串模式
json字符串是一种可阅读的数据,所以调试更方便,一般用在Http、Mqtt及Tcp/IP中,由于json字符串的解析和编码较为复杂,一般只出现在嵌入式设备或者微型计算机作为控制器的场合。
2. 路径下发方式
由于不同控制器特性的不同,路径下发方式通常也有所不同,常见的有:
- 下发地标和运动控制信息
对于几乎所有的PLC控制器和大部分嵌入式控制器,都是支持该模式的,也就是路径的数据报文中除了下一个地标点外,如果到达下一个地表点(直行、旋转、漂移等)的信息都一起发送给控制器。
- 下发坐标和运动控制信息
部分SLAM或者反光板导航的AGV,不需要地标信息,而是需要坐标信息。其他的运动控制同上。
- 仅下发坐标
部分SLAM或者微型计算机控制器具备自主的运动控制处理,所以仅需要我们下发路径的坐标序列即可。
3. 定长包还是变长包
路径下发是一个比较特殊的数据,有可能某次导航交管后需要下发2条子路径信息,也可能是100条子路径,那么对应的数据包长度有以下两种处理方式:
- 定长包
一般受限于PLC的性能或者为了方便调试,定长包指的是:在数据流模式下,需要系统下发的字节流数据拥有固定的长度,在数据块读写模式下,需要写入固定数量的数据块。定长模式对于子路径数量的一般处理方式有以下两种:
⚪ 当路径数量小于定长包支持的路径数量时,剩余数据填空值或无效值
⚪ 当路径数量大于定长包支持的路径数量时,由通讯脚本拆分成多个数据包下发,大部分时候还需要考虑控制器是否正确接收到数据包并处理好,所以一般需要增加应答确认操作。
- 变长包
对于嵌入式设备或者微型计算机,由于算力高,程序编写方便,通常采用变长包处理方式,该方式不再需要拆分操作,但一定具有长度位!不要依靠包尾作为解包标识,因为数据中很可能随机具有和包尾一样的数据,极容易导致解包失败!
4. 动作执行方式
不同的控制器协议,动作执行一般包括以下几种:
- 随路径下发动作
- 额外的报文处理动作下发(即时动作指令)
- 以上二者兼得
★ 开始分析示例协议
有了以上的知识储备,我们来看上文样例提供的协议,可以确定如下信息:
⭕ 通讯方式为Modbus-Tcp,并且采用数据块读写模式。
⭕ 路径下发方式采用下发地标和运动控制信息方式。
⭕ 包长采用定长包方式,并且可接收路径最大为3条子路径。
⭕ 动作执行方式比较有趣,由于是数据块读写模式,我们即可在下发路径的时候填写动作数据,也可以单独下发[D4000-D4003]几个数据块进行即时动作,当然,是否具有这个功能我们需要和控制器工程师确认清楚。
推荐的编写方式
通讯数据结构框图

如图所示,左侧为混合调度系统内核,中间是我们要实现的通讯协议脚本,右侧是车体控制器。

⚪ 当我们要下发控制信息,混合调度系统,提供了 [事件 -> 数据]的标准转换手段,即我们只需要注册车体不同的事件,即可在事件参数中得到详细的路径、控制等信息,根据协议规范将其转换为对方可以识别解析的报文数据即可。

⚪ 当我们按照协议解析出车体控制器上传的数据后,通过系统提供的各种接收API,即可将数据送入内核进行进一步处理。

⚪ 在此我们强烈建议根据协议内容构造通讯信息结构,通讯信息结构中包括了控制数据结构及上传数据结构;其中控制数据结构主要是将协议中需要下发给车体的数据进行类型定义,而上传数据结构则是将车体上传给系统的数据进行类型定义。

⚪ 系统提供了丰富的通讯API工具,包括Tcp/IP、Modbus、S7、Mqtt、Http等底层协议,通过这些工具我们可以实现与车体的底层通讯协议互通。
一、抽象控制信息 ControlInfo
仔细阅读协议文档,我们可以很迅速的写出如下的类型:
/// <summary>/// 控制信息/// </summary>publicclassControlInfo{//心跳publicshort HB {set;get;}//任务IDpublicshort TaskId {set;get;}//任务目标点publicshort TaskTargetPoint {set;get;}//任务类型publicshort TaskType {set;get;}//站点总数publicshort StationsCount {set;get;}//路径节点信息publicRouteInfo[] RouteInfos {set;get;}=newRouteInfo[3];//请求取消任务publicshort CancelTask {set;get;}publicshort Reserve1 {set;get;}publicshort Reserve2 {set;get;}publicshort Reserve3 {set;get;}publicControlInfo(){for(var i =0; i <3; i++){ RouteInfos[i]=newRouteInfo();}}}/// <summary>/// 路径节点信息/// </summary>publicclassRouteInfo{//起点站点地标publicshort StartMark {set;get;}//起点X坐标publicint StartX {set;get;}//起点Y坐标publicint StartY {set;get;}//起点离开角度publicshort StartExitAngle {set;get;}//终点站点地标publicshort EndMark {set;get;}//终点X坐标publicint EndX {set;get;}//终点Y坐标publicint EndY {set;get;}//终点离开角度publicshort EndExitAngle {set;get;}//行驶指令publicshort DriveAction {set;get;}//线路类型publicshort RouteType {set;get;}//行驶速度publicshort Speed {set;get;}//站点动作指令publicshort Action {set;get;}//避障屏蔽publicshort PBSFilter {set;get;}//圆弧圆心X或曲线第一控制点Xpublicint ArcCenterXOrC1X {set;get;}//圆弧圆心Y或曲线第一控制点Ypublicint ArcCenterYOrC1Y {set;get;}//圆弧的半径publicshort ArcRadius {set;get;}//圆弧的起始弧度publicshort ArcStartRadian {set;get;}//圆弧的终点弧度publicshort ArcEndRadian {set;get;}//是否是最终节点publicshort IsEndOfRoute {set;get;}publicshort Reserve {set;get;}//节点序号publicshort Number {set;get;}}
C#
以上代码对应了样例协议中的系统下发小车部分,注意,由于路径信息是定长的,且为3。我们在代码中专门为路径信息构建了类型,然后在控制信息中使用该类型构建了一个长度为3的数组。
二、抽象小车上传信息 AckInfo
相同的操作,不同的是现在我们抽象的是小车上传的信息:
/// <summary>/// 小车上传信息/// </summary>publicclassAckInfo{//小车编号publicshort AgvNum {set;get;}//心跳,每秒自增,到最大值时从1开始报告publicshort HB {set;get;}//小车当前X坐标,单位毫米publicint XPos {set;get;}//小车当前Y坐标,单位毫米publicint YPos {set;get;}//小车当前角度,单位度 publicshort Angle {set;get;}//载货状态(0无货,1有货)publicshort WithCargo {set;get;}//货架编号publicshort ShelvesNum {set;get;}//控制模式(1半自动,2自动可控制)publicshort ControlMode {set;get;}//AGV运行速度publicshort Speed {set;get;}//电池电量百分比 1 - 100publicshort Battery {set;get;}//导航状态(0未知,1迷路,2正常)publicshort Status {set;get;}//小车报警错误码publicshort WarningCode {set;get;}//小车硬件错误码publicshort ErrorCode {set;get;}//当前起点路径点编号publicshort RouteStartIdx {set;get;}//当前终点路径点编号publicshort RouteEndIdx {set;get;}//行走的偏移量MMpublicshort Offset {set;get;}//任务IDpublicshort TaskId {set;get;}//任务状态publicshort TaskStatus {set;get;}//任务操作码publicshort TaskOperateCode {set;get;}//任务目标终点publicshort TaskTargetMark {set;get;}//AGV是否在站点上publicshort IsAgvOnStation {set;get;}//当前点请求充电publicshort RequestCharge {set;get;}//节点回复publicshort NodeResponse {set;get;}//预留变量1publicshort Reserve1 {set;get;}//预留变量2publicshort Reserve2 {set;get;}//预留变量3publicshort Reserve3 {set;get;}//预留变量4publicshort Reserve4 {set;get;}//预留变量5publicshort Reserve5 {set;get;}//预留变量6publicshort Reserve6 {set;get;}//预留变量7publicshort Reserve7 {set;get;}}
C#
三、构建通讯数据信息 CommuInfo
有了下发数据类型和上传数据类型后,我们可以构造一个总的数据类型来包装这些重要数据:
/// <summary>/// 通讯信息/// </summary>publicclassCommuInfo{/// <summary>/// 隶属于Agv/// </summary>publicIAgv Belong {set;get;}/// <summary>/// 通讯IP/// </summary>publicstring IP {set;get;}="127.0.0.1";/// <summary>/// 通讯端口/// </summary>publicint Port {set;get;}=502;/// <summary>/// 控制信息/// </summary>publicControlInfo CtrlInfo {set;get;}/// <summary>/// 小车上传信息/// </summary>publicAckInfo AckInfo {set;get;}/// <summary>/// 构造函数/// </summary>/// <param name="agv">隶属于的Agv</param>/// <param name="ip">通讯IP</param>/// <param name="port">通讯端口,如果是Modbus,一般是固定的,不用设置</param>publicCommuInfo(IAgv agv,string ip,int? port =null){this.CtrlInfo =newControlInfo();this.AckInfo =newAckInfo();this.Belong = agv;this.IP = ip;if(port !=null){this.Port =(int)port;} agv.Tag =this;}}
C#
CommuInfo类型包含了控制信息对象、小车上传信息对象,同时我们还需要将Agv对象、IP地址及端口等相关对象信息传递到CommuInfo信息类中。有了这些信息,我们接下来要做的工作是将对象转换为协议格式下的数据。
★ 赋值给Agv的附加标签Tag
⚪ 可以看到代码中包含了这么一段:
publicCommuInfo(IAgv agv,string ip,int? port =null){... agv.Tag =this;}
C#
⚪ 极力推荐的设计模式!。系统将脚本分为了应用脚本和通讯脚本两个不同功能的部分,之所以做如此设计,目的有以下几点:
- 调试流程和逻辑的时候我们不希望中断通讯,所以通讯脚本本就该独立。
- 有些车体的通讯需要心跳保持,如果通讯脚本心跳中断,可能会报警。
⚪ 搞清楚了分为两个脚本的原因,问题也随时而来,我们很多时候需要应用脚本和通讯脚本进行数据交互,虽然可以采用A程序集引用B的方案,但是为了更好的进行数据交互,我们推荐使用每个对象都具备的Tag属性。
Tag属性是一个object类型的属性,可以作为一个临时数据存放的缓存。推荐使用Tag属性进行应用脚本和通讯脚本互操作、数据共享的媒介!
⚪ 我们在应用脚本中如果希望访问上述定义的CommuInfo对象,可以先引入拥有CommuInfo类型的脚本程序集,然后使用方式如下:
var agv = API.GetAgv("XXX");var commuInfo = agv.Tag as[ScriptNamespace].CommuInfo;
C#
四、协议数据转换为信息类型【上报】
/// <summary>/// 从控制器读取数据/// </summary>publicasyncTaskReadFromDev(){//通过Modbus读取相关寄存器数据var data =await API.ModbusRead(ModBusModeEnum.Tcp,this.IP,502,4200,32,(ushort[] dataTmp)=>{},null,1);if(data.Length ==32){//将数据解析赋值到小车上传信息结构中this.AckInfo.AgvNum =(short)data[0];this.AckInfo.HB =(short)data[1];this.AckInfo.XPos =((int)data[3]<<16)|((int)data[2]<<0);this.AckInfo.YPos =((int)data[5]<<16)|((int)data[4]<<0);this.AckInfo.Angle =(short)data[6];this.AckInfo.WithCargo =(short)data[7];this.AckInfo.ShelvesNum =(short)data[8];this.AckInfo.ControlMode =(short)data[9];this.AckInfo.Speed =(short)data[10];this.AckInfo.Battery =(short)data[11];this.AckInfo.Status =(short)data[12];this.AckInfo.WarningCode =(short)data[13];this.AckInfo.ErrorCode =(short)data[14];this.AckInfo.RouteStartIdx =(short)data[15];this.AckInfo.RouteEndIdx =(short)data[16];this.AckInfo.Offset =(short)data[17];this.AckInfo.TaskId =(short)data[18];this.AckInfo.TaskStatus =(short)data[19];this.AckInfo.TaskOperateCode =(short)data[20];this.AckInfo.TaskTargetMark =(short)data[21];this.AckInfo.IsAgvOnStation =(short)data[22];this.AckInfo.RequestCharge =(short)data[23];this.AckInfo.NodeResponse =(short)data[24];this.AckInfo.Reserve1 =(short)data[25];this.AckInfo.Reserve2 =(short)data[26];this.AckInfo.Reserve3 =(short)data[27];this.AckInfo.Reserve4 =(short)data[28];this.AckInfo.Reserve5 =(short)data[29];this.AckInfo.Reserve6 =(short)data[30];this.AckInfo.Reserve7 =(short)data[31];}}
C#
以上代码我们通过系统提供的通讯API函数,读取了PLC的4200开始的寄存器数据,然后通过数据转换将其赋值给小车上传信息结构中对应的属性。这样后续我们只需要对AckInfo对象进行处理即可完全掌握小车的当前情况。
五、控制类型转换为协议数据【下发】
/// <summary>/// 将路径信息写入到控制器/// </summary>publicasyncTaskWriteRouteToDev(){ CtrlInfo.HB++;if(CtrlInfo.HB >=32760) CtrlInfo.HB =1;var frame =newList<short>(); frame.Add(CtrlInfo.HB); frame.Add(CtrlInfo.TaskId); frame.Add(CtrlInfo.TaskTargetPoint); frame.Add(CtrlInfo.TaskType); frame.Add(CtrlInfo.StationsCount);for(var i =0; i <3; i++){var route = CtrlInfo.RouteInfos[i]; frame.Add(route.StartMark);//注意,协议中说明了是小端var ushort1 =(short)(route.StartX &0x0000ffff);var ushort2 =(short)((route.StartX >>16)&0x0000ffff); frame.Add(ushort1); frame.Add(ushort2); ushort1 =(short)(route.StartY &0x0000ffff); ushort2 =(short)((route.StartY >>16)&0x0000ffff); frame.Add(ushort1); frame.Add(ushort2); frame.Add(route.StartExitAngle); frame.Add(route.EndMark); ushort1 =(short)(route.EndX &0x0000ffff); ushort2 =(short)((route.EndX >>16)&0x0000ffff); frame.Add(ushort1); frame.Add(ushort2); ushort1 =(short)(route.EndY &0x0000ffff); ushort2 =(short)((route.EndY >>16)&0x0000ffff); frame.Add(ushort1); frame.Add(ushort2); frame.Add(route.EndExitAngle); frame.Add(route.DriveAction); frame.Add(route.RouteType); frame.Add(route.Speed); frame.Add(route.Action); frame.Add(route.PBSFilter); ushort1 =(short)(route.ArcCenterXOrC1X &0x0000ffff); ushort2 =(short)((route.ArcCenterXOrC1X >>16)&0x0000ffff); frame.Add(ushort1); frame.Add(ushort2); ushort1 =(short)(route.ArcCenterYOrC1Y &0x0000ffff); ushort2 =(short)((route.ArcCenterYOrC1Y >>16)&0x0000ffff); frame.Add(ushort1); frame.Add(ushort2); frame.Add(route.ArcRadius); frame.Add(route.ArcStartRadian); frame.Add(route.ArcEndRadian); frame.Add(route.IsEndOfRoute); frame.Add(route.Reserve); frame.Add(route.Number);} frame.Add(CtrlInfo.CancelTask); frame.Add(CtrlInfo.Reserve1); frame.Add(CtrlInfo.Reserve2); frame.Add(CtrlInfo.Reserve3);var data = frame.Select(x =>(ushort)x).ToArray();//通过Modbus写入相关寄存器数据await API.ModbusMultipleWrite(ModBusModeEnum.Tcp,this.IP,this.Port,4000, data,null,null,1);}
C#
以上代码我们将控制信息按照协议要求,组包成了对应的数据,然后通过系统提供的通讯API函数,将数据发送给控制器。这样我们就有了下发数据给控制器的功能。

以上内容编写完成后,我们则具有了框架图中的该部分。让我们先测试一下读取和写入是否正常!
六、测试读取与写入数据
不同的通讯底层协议具有不同的测试手段,本案例中由于使用了Modbus-tcp的方式,我们推荐使用HSLCommunicationOnDemo测试工具,该工具可以很好的模拟Modbus,S7等工业设备的通讯模块。
| 底层协议 | 推荐工具名称 | 下载地址 | 备注 |
|---|---|---|---|
| Tcp/IP | sscom5.13.1 | sscom | 提供连接如果失效请自行搜索下载 |
| S7通讯 | HSLCommunicationOnDemo | HSLCommunicationOnDemo | 提供连接如果失效请自行搜索下载 |
| Modbus通讯 | HSLCommunicationOnDemo | HSLCommunicationOnDemo | 提供连接如果失效请自行搜索下载 |
| 三菱MC | HSLCommunicationOnDemo | HSLCommunicationOnDemo | 提供连接如果失效请自行搜索下载 |
| Http通讯 | postman/ProfControl | postman | 提供连接如果失效请自行搜索下载 |
| Mqtt通讯 |
编写测试方法
测试程序我们先简单对一些数据进行赋值,然后调用写入和读取方法
/// <summary>/// 测试程序/// </summary>publicasyncTaskTest(){ CtrlInfo.TaskId =0x1234; CtrlInfo.TaskTargetPoint =0x5678; CtrlInfo.RouteInfos[0].StartMark =0x55AA; CtrlInfo.RouteInfos[1].StartMark =0x55AA; CtrlInfo.RouteInfos[2].StartMark =0x55AA; CtrlInfo.CancelTask =0x4321;//写入测试awaitWriteRouteToDev();//读取测试awaitReadFromDev();//当4200地址接收到88时,打印信息if(this.AckInfo.AgvNum ==88){ Tools.trace("接收到了88");}}
C#
我们需要在通讯脚本的初始化函数(Init)的注册函数(Register)里,实例化CommuInfo,如下文所示
IAgv belongAgv =null;publicvoidInit(paramsobject[] args){var map =newEventMap<IAgv>();//注册对接设备的事件,产生发送报文 map.Register =(agv)=>{ belongAgv = agv;var info =newCommuInfo(agv,"127.0.0.1");...};...}
C#
然后我们通过一个定时器,每隔1秒钟执行这个测试方法
API.SetInterval(tc =>{if(belongAgv !=null){var info = belongAgv.Tag asCommuInfo; info?.Test();}},1000);
C#
配置测试项目
我们编辑好脚本文件后,需要建立一个项目,添加一辆Agv,并且将Agv通讯配置完成,如下图所示:
⭕ 在地图上建立一个Agv:

⭕ 打开设置–通讯配置:

⭕ 新增一个配置项,注意协议选取正在开发的通讯脚本项目名称:

⭕ 测试时可以使用回环地址:127.0.0.1,别忘记添加Agv的配置:

打开并配置测试工具
双击打开HSL调试工具,在左侧列表中找到Modbus–Modbus Server,双击打开Modbus服务器。

打开后如下图所示,确认端口是标准的502,点击启动服务按钮:

启动服务后,我们可以在下方区域看到Agv已经连接到了虚拟服务器,并且通讯报文也被打印出来。

查看通讯脚本写入的信息
由于测试程序我们对一些属性配置了初始值
CtrlInfo.TaskId =0x1234;CtrlInfo.TaskTargetPoint =0x5678;CtrlInfo.RouteInfos[0].StartMark =0x55AA;CtrlInfo.RouteInfos[1].StartMark =0x55AA;CtrlInfo.RouteInfos[2].StartMark =0x55AA;CtrlInfo.CancelTask =0x4321;
C#
此时我们通过HSL工具读取4000开始的6个数据,可以看到HSL工具打印出的信息如下:

将其转换为16进制分别为 0x0153,0x1234,0x5678,0x0000,0x0000,0x55AA,这与我们测试程序内给定的数值是完全匹配的。
查看虚拟控制器写入的信息
由于测试程序,我们循环读取4200开始的数据31个数据,并且当第一个数据为88时,我们将在控制台打印“接收到了88”。
我们在HSL工具右侧在地址4200位置写入十进制88:

然后打开系统的控制台,此时我们可以看到,每隔1秒钟,控制台将打印一次“接收到了88”。

注释掉相关测试程序
测试完成后,我们没必要删除测试程序,可以通过注释的方式将相关代码禁用,请勿将测试程序运用于现场!
七、通过事件生成有效的数据
第六步我们通过临时数据验证了通讯,接下来我们需要真正开始协议数据的获取与转换。 之所以我们抽象控制信息、小车上传信息及通讯数据信息,目的有以下几点:
(1)将编码数据与上层数据去耦,如果我们直接从系统中取上层数据进行直接编码,这样的程序缺少层次,并且阅读困难。有了抽象的中间层类型,我们无论是验证协议还是校对都可以分为【上层数据 – 中间数据】及【中间数据 – 编码数据】两部分进行,条理清晰便于维护。
(2)我们无形中构造了一个缓存空间,比如说我们读取车体数据,如果读取时直接传送到系统内核,一旦出现网络问题或其他问题,我们无法在此过程中停止;相反的,有了中间层数据,我们完全可以校验无误后传送到系统内核。
⚪ 混合调度系统下发给控制器路径,动作等操作,都是通过【内核事件 – 携带数据】的模型进行的。系统提供了丰富的事件,每个事件都携带了详细的事件参数。
对于下发路径,最重要的事件即是:GetNewRoutesEvent。
GetNewRoutesEvent事件代表了每次混合调度系统对车体进行导航-交管完成后,生成了确认路径的事件,该事件携带了确认路径的所有信息,包括但不限于:
(1) 路径经过的所有子路径信息
(2) 路径经过的所有站点的信息,有了站点信息,对应的关键坐标也就有了
(3) 路径在每个站点的运动控制信息,包括了前进后退、旋转、漂移等
对于样例中的协议,他每次仅可以接收三段子路径,接收完成后在上传信息中应答,所以我们需要对确认路径进行拆分。
定义枚举
任何协议文档中都具有很多枚举数据,为了后期更直观的使用,我们需要专门建立一个Enums.cs的文件来定义这些枚举,根据样例协议信息,我们可以轻松定义如下枚举:
publicenumWarningCodeEnum{ 无错误 =0x0001, 电量低预警 =0x0002, 电量低停机 =0x0004, 避障触发停机 =0x0008, 对二维码故障 =0x0010,}publicenumErrorCodeEnum{ 无错误 =0x0001, 驱动器报警 =0x0002, 激光通讯中断 =0x0004, 避障通讯中断 =0x0008, 急停按下报警 =0x0010,}publicenumTaskStatusAckEnum{ 空闲 =1, 配送中_在路上 =2, 操作中_到达目的地_开始操作 =3, 完成_到达目的地_已完成动作完成这个任务 =4,}publicenumTaskOperatorCodeEnum{ 无动作_小车在执行动作状态 =0, 取料 =1, 放料 =2, 充电 =3,}publicenumRequestChargingEnum{ 无动作 =0, 请求开启充电桩 =10, 调度开启充电桩_小车检测到20后复位 =20,}publicenumTaskTypeEnum{ 移动 =0, 取料 =1, 放料 =2, 充电 =3,}publicenumRouteTypeEnum{ 无 =0, 直线 =1, 贝塞尔 =2, 圆弧 =3,}publicenumDriveActionEnum{ 停止 =0, 前进 =1, 后退 =2, 左岔路前进 =3, 右岔路前进 =4, 左岔路后退 =5, 右岔路后退 =6, 原地左转前进 =7, 原地右转前进 =8, 原地左转后退 =9, 原地右转后退 =10, 旋转180前进 =11, 左平移 =12, 右平移 =13, 左转后左平移 =14, 右转后左平移 =15, 左转后右平移 =16, 右转后右平移 =17,}publicenumPBSFilterEnum{ 不用屏蔽避障 =0, 左避障 =1, 右避障 =2, 前避障 =3, 后避障 =4, 左右避障屏蔽 =5, 屏蔽前后避障 =6, 前后左右全部屏蔽 =7,}publicenumCancelTaskEunm{ 无动作 =0, 调度请求取消当前任务 =10, 小车已经取消任务 =20,}publicenumEndOfRouteActionEnum{ 无 =0, 取料 =1, 放料 =2, 对二维码取料 =3, 充电 =7,}
C#
编写子路径 – 数据转换程序
为了可以实现下发路径功能,我们需要先对子路径进行协议编码,子路径协议编码主要就是将IRouteItem内的信息转换成协议可以识别的数据。系统的IRouteItem内部包含了子路径的全部信息,无论对接什么类型的车体,均可以在IRouteIten-Decision内部找到相关信息。请仔细阅读IRouteItem和Decision的相关文档。
/// <summary>/// 生成路径数据/// </summary>/// <param name="route">子路径单元(系统)</param>/// <param name="routeInfo">子路径信息(协议)</param>publicvoidGenerateRouteData(IRouteItem route,RouteInfo routeInfo){//转换起始站点地标 routeInfo.StartMark = Convert.ToInt16(route.InStation.Mark);//转换起始站点坐标 routeInfo.StartX =(int)route.InStation.View.KeyPoint.X; routeInfo.StartY =(int)route.InStation.View.KeyPoint.Y; routeInfo.StartExitAngle =(short)route.InAngle;//转换子路径离开站点地标 routeInfo.EndMark = Convert.ToInt16(route.OutStation.Mark);//转换子路径离开站点坐标 routeInfo.EndX =(int)route.OutStation.View.KeyPoint.X; routeInfo.EndY =(int)route.OutStation.View.KeyPoint.Y; routeInfo.EndExitAngle =(short)route.OutAngle;//运动控制处理,如果需要旋转if(route.Decision.NeedRotate){//判断是否为左转var rotateLeft = route.Decision.RotateAngle <0&& Math.Abs(route.Decision.RotateAngle)>=15&& Math.Abs(route.Decision.RotateAngle)<=75;//判断是否为右转var rotateRight = route.Decision.RotateAngle >0&& Math.Abs(route.Decision.RotateAngle)>=15&& Math.Abs(route.Decision.RotateAngle)<=75;//判断是否为180度转向var rotate180 = Math.Abs(route.Decision.RotateAngle)>=165&& Math.Abs(route.Decision.RotateAngle)<=195;//如果工角=0,是正方向行驶if(route.Decision.WorkAngle ==0){if(rotateLeft) routeInfo.DriveAction =(short)DriveActionEnum.原地左转前进;elseif(rotateRight) routeInfo.DriveAction =(short)DriveActionEnum.原地右转前进;elseif(rotate180) routeInfo.DriveAction =(short)DriveActionEnum.旋转180前进;}//如果工角=180,是反方向行驶elseif(route.Decision.WorkAngle ==180){if(rotateLeft) routeInfo.DriveAction =(short)DriveActionEnum.原地左转后退;elseif(rotateRight) routeInfo.DriveAction =(short)DriveActionEnum.原地右转后退;}//如果工角=-90,是左漂移elseif(route.Decision.WorkAngle ==-90){if(rotateLeft) routeInfo.DriveAction =(short)DriveActionEnum.左转后左平移;elseif(rotateRight) routeInfo.DriveAction =(short)DriveActionEnum.右转后左平移;}//如果工角=90,是右漂移elseif(route.Decision.WorkAngle ==90){if(rotateLeft) routeInfo.DriveAction =(short)DriveActionEnum.左转后右平移;elseif(rotateRight) routeInfo.DriveAction =(short)DriveActionEnum.右转后右平移;}}//如果不需要旋转else{//如果工角=0,是正方向行驶if(route.Decision.WorkAngle ==0){if(route.Decision.MaybeBranchMode == BranchMode.MaybeMiddle) routeInfo.DriveAction =(short)DriveActionEnum.前进;elseif(route.Decision.MaybeBranchMode == BranchMode.MaybeLeft) routeInfo.DriveAction =(short)DriveActionEnum.左岔路前进;elseif(route.Decision.MaybeBranchMode == BranchMode.MaybeRight) routeInfo.DriveAction =(short)DriveActionEnum.右岔路前进;}//如果工角=180,是反方向行驶elseif(route.Decision.WorkAngle ==180){if(route.Decision.MaybeBranchMode == BranchMode.MaybeMiddle) routeInfo.DriveAction =(short)DriveActionEnum.后退;elseif(route.Decision.MaybeBranchMode == BranchMode.MaybeLeft) routeInfo.DriveAction =(short)DriveActionEnum.左岔路后退;elseif(route.Decision.MaybeBranchMode == BranchMode.MaybeRight) routeInfo.DriveAction =(short)DriveActionEnum.右岔路后退;}//如果工角=-90,是左漂移elseif(route.Decision.WorkAngle ==-90){ routeInfo.DriveAction =(short)DriveActionEnum.左平移;}//如果工角=90,是右漂移elseif(route.Decision.WorkAngle ==90){ routeInfo.DriveAction =(short)DriveActionEnum.右平移;}}//判定路径类型if(route.Decision.Info.CurPath.Path isLineView) routeInfo.RouteType =(short)RouteTypeEnum.直线;elseif(route.Decision.Info.CurPath.Path isBezierView) routeInfo.RouteType =(short)RouteTypeEnum.贝塞尔;elseif(route.Decision.Info.CurPath.Path isArcView) routeInfo.RouteType =(short)RouteTypeEnum.圆弧;//配置子路径车速 routeInfo.Speed =(short)(route.Decision.Info.CurPath.Path.Model asIPathModel).AgvSpeed; routeInfo.Action =0x0000; routeInfo.PBSFilter =0x0000;if(route.Decision.Info.CurPath.Path isBezierView){}elseif(route.Decision.Info.CurPath.Path isArcView){}//判断是否是总导航路径的终点if(route.OutMark == Belong.API.TargetMark) routeInfo.IsEndOfRoute =0x0001;else routeInfo.IsEndOfRoute =0x0000; routeInfo.Reserve =0x5555;//便于分析报文//分配路径需要 routeInfo.Number =(short)CtrlInfo.NodeCnt; CtrlInfo.NodeCnt++;}
C#
编写下发路径 – 数据转换程序
有了子路径下发功能后,我们需要根据协议需求,编写下发路径的报文。样例协议由于是Modbus对接PLC控制器,并且采用了一次3条子路径的定长包方式,所以我们需要对当前路径进行拆分。
/// <summary>/// 路径拆分处理/// </summary>publicvoidRouteProcess(){//如果接收到了请求路径if(AckInfo.NodeResponse ==1){//当前缓存路径不为空if(CtrlInfo.CurrentRoute !=null){ CtrlInfo.TaskId = CurTaskID; CtrlInfo.TaskTargetPoint = Convert.ToInt16(Belong.API.TargetMark); CtrlInfo.TaskType =(short)TaskTypeEnum.移动; CtrlInfo.StationsCount =(short)CtrlInfo.CurrentRoute.Stations.Count;var cnt =0;//从总路径中拆分出3个子路径编码,如果剩余路径不够三个,则清空相关数据位for(var i = CtrlInfo.CurrentRouteIdx; i < CtrlInfo.CurrentRoute.Count +3; i++){if(cnt >=3)break;var routeInfo = CtrlInfo.RouteInfos[cnt];//复位数据 routeInfo.Reset();//超出当前子路径长度则不填值if(i < CtrlInfo.CurrentRoute.Count){var route = CtrlInfo.CurrentRoute[i];GenerateRouteData(route, routeInfo);} cnt++;}//将数据写到车体控制器WriteRouteToDev(); CtrlInfo.CurrentRouteIdx += cnt;if(CtrlInfo.CurrentRouteIdx >= CtrlInfo.CurrentRoute.Count){ CtrlInfo.CurrentRoute =null; CtrlInfo.CurrentRouteIdx =0;}}}}
C#
通过事件触发路径下发
完成了子路径编码及确认路径拆分编码后,我们通过GetNewRoutesEvent事件进行确认路径的缓存
//得到可以导航的全部路径信息privatevoidAgv_GetNewRoutesEvent(object sender,GetNewRoutesEventArgs e){ Tools.trace("获取到新导航路径");var agv = sender asIAgv;var routes = e.Route;var devInfo =this.Commu.GetDevInfo(agv);var taskID = agv.Tasks.FirstOrDefault()?.ID.ToString();//通过Tag获取到通讯信息对象var commuInfo = agv.Tag asCommuInfo;if(commuInfo !=null){//设置当前需要下发的确认路径 commuInfo.SetCurrentRoute(routes, taskID);}}
C#
修改Init函数中的定时器代码,每间隔1秒进行一次设备上传数据读取,并且检查是否右拆分路径需要更新下发
publicvoidInit(paramsobject[] args){... API.SetInterval(tc =>{if(belongAgv !=null){var info = belongAgv.Tag asCommuInfo;//每隔一秒读取一次控制器上传数据 info.ReadFromDev();//每隔一秒尝试下发拆分路径至控制器 info.RouteProcess();//info?.Test();}},1000);}
C#


