前言
好记性,不如烂笔头。大家好,我是不自由的小码。既然来了那么点个关注,点个赞再走吧!
今天这篇文章是要教大家C#如何基于ASTM协议与设备进行通信交互。
什么是ASTM协议标准
官方的解释太专业,按照自己理解,举个例子,比如在医疗行业,用于与化验设备(LIS)通信交互,那它就是一个数据交互协议,如果你是C#跟Java程序员,那你可以理解他跟json、xml一样,都是数据交换标准的一种格式。
数据通信逻辑
以双向通信为例,整体通讯分三步,设备发送请求、HIS回传项目、设备回传结果。其实最终实现效果正确的话,单向双向都是一个逻辑,因为单向的话是设备直接发结果,通信程序要做的就是重复第一步,一直ACK(应答)直到EOT(结束)。根据收到的内容去判断,数据是来自设备请求,还是设备回传。举个例子,这就像两个人对话。
一、设备发送请求
唐三(ENQ):小舞~中午该吃饭了。
小舞(ACK):收到!
唐三(STX):今天中午我们吃十万年魂兽吧。
小舞(ACK):收到!
唐三(STX):放点辣。
小舞(ACK):收到!
…
唐三(ETO):就这些,在没其它要求了。
小舞(无需回答):心里在想,不理三哥了,做个饭要求真多!
二、程序回传项目
小舞(ENQ):三哥~,辣子魔晶王做好了。
唐三(ACK):收到!
小舞(STX):你看,这里面我放了辣椒。
唐三(ACK):收到!
小舞(STX):你看,这里面我放了大蒜。
唐三(ACK):收到!
…
小舞(ETO):好了,就这么多项目,开始吃吧。
唐三(无需回答):心里想,吃个饭真不容易啊。唠叨了半天。
三、设备回传结果
唐三(ENQ):小舞~我得提几点意见。
小舞(ACK):收到!
唐三(STX):今天中午的发米饭做硬了。
小舞(ACK):收到!
唐三(STX):辣椒放的倒是合适。
小舞(ACK):收到!
…
唐三(ETO):就提这些意见吧。
小舞(无需回答):心里在想,不理三哥了,管他呢,反正这顿饭吃完了!
数据发送格式
<STX>FN<FRAME><CR><ETX>CHECKSUM<CR><LF>
尖括号中的都是协议控制字符,下面会有解释。CHECKSUM是计算内容。
CHECKSUM 算法
ASTM协议的校验码算法:((所有的字符的ASCII码总和)除以256的余数)的十六进制。可以封装一个函数用于后续的计算。
public static string GetCheckSum(string str)
{
// 十进制数组之和除以256的余数转成十六进制字符
byte[] byteArr = ASCIIEncoding.UTF8.GetBytes(str);
int sum = 0;
for (int i = 0; i < byteArr.Length; i++)
{
sum += byteArr[i];
}
sum %= 256;
return sum.ToString("X2");
}
协议控制字符
C#代码部分值得关注,程序后续都需要靠这个转义控制字符进行编写。
控制字符转换
可以单独封装一个转化方法,在拼接数据是明文的时候,直接将字符串传入,得到设备识别的带ASCII控制字符的数据。
public char CR = (char)13;
public char ENQ = (char)5;
public char ACK = (char)6;
public char ETX = (char)3;
public char LF = (char)10;
public char ETB = (char)17;
public char NAK = (char)15;
public char EOT = (char)4;
public char STX = (char)2;
private string ChangeAscii(string str)
{
return str
.Replace("<ENQ>", ENQ.ToString())
.Replace("<CR>", CR.ToString())
.Replace("<ACK>", ACK.ToString())
.Replace("<ETX>", ETX.ToString())
.Replace("<LF>", LF.ToString())
.Replace("<ETB>", ETB.ToString())
.Replace("<NAK>", NAK.ToString())
.Replace("<EOT>", EOT.ToString())
.Replace("<STX>", STX.ToString());
}
数据交换过程剖析
1.当首次设备向LIS通讯软件发送请求的时候,通讯软件只需要AKC。
//设备:<ENQ>
if (str.Contains(((char)5).ToString()))
{
//LIS: <ACK>
this.SendMessage(((char)6).ToString());
return;
}
2.设备收到通讯软件ACK后,会继续向LIS发送请求数据,格式为:<STX>FN<FRAME><CR><ETX>CHECKSUM<CR><LF>
,直到设备发送EOT结束。
//设备:<EOT>
if (str.Contains(((char)4).ToString()))
{
//存储过程开始处理数据
if (string.IsNullOrEmpty(sbStr.ToString()))
{
//LIS处理数据结果:设备发送了空数据,无需后续处理
return;
}
string returnData = LisProcedure(sbStr.ToString());//调用函数返回数据,自定义,根据自己业务。
if (string.IsNullOrEmpty(returnData))
{
//已经是最后一步无需继续向设备发消息
sbStr.Clear();
return;//说明是最后一步直接停止交互
}
// 根据STX拆分数据
string[] strArrayOut = ChangeAscii(returnData).Split(new char[] { (char)2 }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < strArrayOut.Length; i++)
{
//LIS:<STX>1H|\^&|<CR><ETX>checksum<CR><LF>
string strOutSplit = (char)2 + strArrayOut[i];
//将返回的数据
strListOut.Add(strOutSplit);//strListOut全局变量,在LIS像设备发送项目的时候使用
}
//LIS向设备发ENQ
port.SendMessage(((char)5).ToString());
sbStr.Clear();//清空数据
return;
}
值得注意的是,每次接收数据,只有在LF结尾的时候才表示当条数据被完整接收,否则继续拼接接收。收到一条数据,就要应答ACK一次,设备才会继续发送。
strACK += str;
if (str.Contains((char)10))//若果是LF结尾完成拼接,此时才是当前接收数据的完整串
{
//设备
//接收到的数据:去除\0,<CR><ETX>替换为<ETX>,根据自己的业务去写
sbStr.Append(strACK.Trim('\0').Replace($"{((char)13).ToString() + ((char)3).ToString()}", $"{((char)3).ToString()}"));
//LIS:<ACK>
this.SendMessage(((char)6).ToString());
strACK = "";
}
3.LIS准备好数据以后,通讯软件要向设备发送请求ENQ,设备这时会回答ACK。当LIS通讯收到设备的应答ACK后,开始发送数据STX,设备继续应答,LIS通信继续发送,当数据发送完毕后,LIS通信软件发送EOT结束给设备。
if (str.Contains(((char)6).ToString()))//如果收到的是ACK
{
//接收到设备的ACK后,发送数据
if (strListOut.Count > 0)
{
this.SendMessage(ChangeAscii(strListOut[0]));
strListOut.RemoveAt(0);
}
else
{
//LIS向设备发EOT
port.SendMessage(((char)4).ToString());
}
return;
}
4.化验结果出来以后,设备要向通讯软件发送请求ENQ,通讯软件收到后需要应答ACK,设备收到通讯软件的ACK后,会向通讯软件发送明细结果STX,通讯软件继续应答ACK,直到收到设备的EOT。
逻辑共用,上面第1第2项说明。原理都是一样的,代码都是一套代码无需重写。
唯一的差别就是,LIS拿到接收的数据,要判断该数据是请求项目数据,还是化验结果数据。然后做相应的数据库处理就好了。
5.完整代码
public void GetMessage(string str)
{
if (str.Contains(((char)6).ToString()))//如果收到的是ACK
{
//接收到设备的ACK后,发送数据
if (strListOut.Count > 0)
{
this.SendMessage(ChangeAscii(strListOut[0]));
strListOut.RemoveAt(0);
}
else
{
//LIS向设备发EOT
port.SendMessage(((char)4).ToString());
}
return;
}
//设备:<ENQ>
if (str.Contains(((char)5).ToString()))
{
//LIS: <ACK>
this.SendMessage(((char)6).ToString());
return;
}
//设备:<EOT>
if (str.Contains(((char)4).ToString()))
{
//存储过程开始处理数据
if (string.IsNullOrEmpty(sbStr.ToString()))
{
//LIS处理数据结果:设备发送了空数据,无需后续处理
return;
}
string returnData = LisProcedure(sbStr.ToString());//调用函数返回数据,自定义,根据自己业务。
if (string.IsNullOrEmpty(returnData))
{
//已经是最后一步无需继续向设备发消息
sbStr.Clear();
return;//说明是最后一步直接停止交互
}
// 根据STX拆分数据
string[] strArrayOut = ChangeAscii(returnData).Split(new char[] { (char)2 }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < strArrayOut.Length; i++)
{
//LIS:<STX>1H|\^&|<CR><ETX>checksum<CR><LF>
string strOutSplit = (char)2 + strArrayOut[i];
//将返回的数据
strListOut.Add(strOutSplit);//strListOut全局变量,在LIS像设备发送项目的时候使用
}
//LIS向设备发ENQ
port.SendMessage(((char)5).ToString());
sbStr.Clear();//清空数据
return;
}
//下面这段这么写是为了接收完整数据
strACK += str;
if (str.Contains((char)10))//若果是LF结尾完成拼接,此时才是当前接收数据的完整串
{
//设备
//接收到的数据:去除\0,<CR><ETX>替换为<ETX>,根据自己的业务去写
sbStr.Append(strACK.Trim('\0').Replace($"{((char)13).ToString() + ((char)3).ToString()}", $"{((char)3).ToString()}"));
//LIS:<ACK>
this.SendMessage(((char)6).ToString());
strACK = "";
}
}
小结
快点去进行实战吧。点个赞跟关注小码在走吧。其实本片博文另一个名字叫《C# LIS实现串口ASTM协议双向通信之斗罗大陆篇》