[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

时间:2023-03-08 16:08:15

目       录

第三章           设备驱动的设计... 2

3.1           初始化设备... 4

3.2           运行设备接口设计... 4

3.3           虚拟设备接口设计... 6

3.4           协议驱动设计... 7

3.5           命令缓存设计... 17

3.6           数据持久化设计... 24

3.7           IO数据交互设计... 26

3.8           通讯状态设计... 30

3.9           定时任务设计... 33

3.10        运行优先级设计... 33

3.11         授权设计... 35

3.12        事件响应设计... 36

3.13        上下文菜单设计... 38

3.14        IO通道监视设计... 39

3.15        关闭框架平台... 39

3.16        小结... 40

第三章     设备驱动的设计

接口(interface)可以把所需成员组合起来,封装成具有一定功能的集合。可以把它理解为一个模板,其中定义了一类对象的公共部分的动作、特性、反应等,分别对应面向对象编程中的方法、属性、事件等概念。抽象类或实体类可以继承接口完成对方法、属性和事件的特定响应,对事物进行具体的规范和多态的表述。

SuperIO的设备驱动接口(IRunDevice)是框架平台与设备驱动之间的交互规范,是加载设备驱动(插件)唯一的入口点,以保证设备驱动能够在框架平台下友好、稳定的运行。

继承IRunDevice设备驱动接口有两个抽象类:RunDevice和RunDevice1。如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

RunDevice和RunDevice1被定义为抽象类,是因为抽象类也是“接口”形式的一种,针对接口实现了设备驱动通用部分的方法、属性和事件,把必须实现的接口规定为抽象方法,把不是必须实现的扩展性接口规定为虚方法。作为框架平台只负责对外提供必要的接口,框架内部是不能new出来一个实例作为设备驱动在框架平台下运行,否则就破坏了框架本身的通用性。所以,所有的设备驱动(插件)可以继承这两个抽象类进行二次开发,对于特殊情况的设备驱动(插件)可以直接继承接口全新的进行二次开发,但是不建议这样做。需要二次开发者灵活掌握。

RunDevice是早期设计抽象类,它本质上是一个UserControl自定义控件,在此基础上实现了IRunDevice设备驱动接口。起初的想法是把设备驱动插件设计成一个可显示的UI,可以在设备容器中任意拖动,并且设置相关的属性信息。但是,这样涉及到一个问题,可显示的UserControl自定义控件同时又是抽象类,那么在设计时将无法手动编辑UI界面,只能通过代码实现UI显示布局、以及显示内容,那么就失去了二次开发的快捷性、方便性。所以,RunDevice在代码实现上分成了Debug和Release两种模块,Debug用于开发、编辑模式,Release用于发布模式,代码实现如下:

#if DEBUG
public partial class RunDevice
#else
public abstract partial class RunDevice
#endif
: UserControl, IRunDevice
{
......
}

即使这样,也很不方便,在设计上很不理想,所以就有了RunDevice1纯抽象类。在框架平台中增加了一个IDeviceGraphics视图接口,代码如下:

    public interface IDeviceGraphics
{
Control DeviceGraphics { get; set; }
}

RunDevice1抽象类实际上是继承了IRunDevice和IDeviceGraphics两个接口,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

尽管IDeviceGraphics视图接口在框架平台内部并没有直接使用,但是在二次开发显示窗体的时候可以使用到,在《第13章 二次开发及应用》中会涉及到这部分内容。另外,在这个地方是有继续扩展的余地的,可以开发一个UI驱动管理器,专门UI进行布局、数据显示、设置属性、以及通过箭头对数据流向进行动态设置等。

原则上,在二次开发过程中继承RunDevice1抽象类完成设备驱动的开发,RunDevice不再继续提供接口服务。现在公司内部都是通过RunDevice1接口来完成驱动开发。

下面对驱动接口的主要功能设计部分进行详细的介绍,以便大家对接口有一个整体的了解,掌握设计思想,仅供参考。

3.1    初始化设备

设备初始化是通过InitDevice(int devid)接口来完成,当设备驱动以插件的方式加载到框架平台时首先就会调用该接口函数,对设备进行初始化设置、以及中载参数和某一点的实时数据。

在这里可以通过序列化文件、数据库、配置文件等作为数据源进行初始化设备,在二次开发中可以灵活掌握。框架默认支持XML序列化的方式进行初始化设备。在“3.6 数据持久化设计”小节中进行详细介绍。

3.2    运行设备接口设计

运行设备接口是保证设备驱动能够在框架平台下被驱动运行,是通过同步RunIODevice和异步AsyncRunIODevice两个接口函数来完成,这两个接口函数分别包括一个构造函数,代码如下:

public interface IRunDevice
{
/// <summary>
/// 同步运行设备(IO)
/// </summary>
/// <param name="io">io实例对象</param>
void RunIODevice(IIOChannel io); /// <summary>
/// 异步运行设备(IO)
/// </summary>
/// <param name="io">io实例对象</param>
void AsyncRunIODevice(IIOChannel io);
//----------------------------------------------// /// <summary>
/// 同步运行设备(byte[])
/// </summary>
/// <param name="revData">接收到的数据</param>
void RunIODevice(byte[] revData); /// <summary>
/// 异步运行设备(byte[])
/// </summary>
/// <param name="revData">接收到的数据</param>
void AsyncRunIODevice(byte[] revData);
}

如果从参数角度分类,那么运行设备接口分两类:IO参数和byte[]参数。IO参数类型的接口,在运行设备的过程中会把实例化后的IO对象传递进来,主要目的是为二次开发者提供自定义发送、接收数据的不同需要。byte[]参数类型的接口,是把已经接收到的数据信息传递进来,在并发模式通讯和自控模式通讯的异步接收后会调用此类接口函数。

这4个接口函数已经在框架平台中实现了流程化,不过它们都是虚函数,二次开发者可以进行重写,但是不建议这样操作,可以会破坏框架事务的处理流程。

3.3    虚拟设备接口设计

为什么会有虚拟设备呢?主要是为了整合同类设备的数据信息,例如:设备A和设备B是同类设备,数据信息也一样,可以开发一个虚拟设备对这两个设备的数据进行二次处理,以到达特殊功能的要求。框架平台把其他设备处理后的数据通过虚拟设备接口完成业务调度,IVirtualDevice虚拟设备接口很简单,代码如下:

public interface IVirtualDevice
{
/// <summary>
/// 运行虚拟设备
/// </summary>
/// <param name="devid">设备ID</param>
/// <param name="obj">数据对象</param>
void RunVirtualDevice(int devid,object obj);
}

RunDevice1抽象类实际上也继承了这个虚拟设备接口,在框架内部共享一个引用地址空间,也就是说继承RunDevice1接口的设备驱动,同时也具备虚拟设备的功能。继承关系,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

虚拟设备与普通设备可以通过DeviceType来区别,如果把设备类型DeviceType设置为Virtual,那么就会标识此设备为虚拟设备,可以对此设备设置处理公式,非虚拟设备的数据通过RunVirtualDevice接口函数传递进来,obj对象数据完全是二次开发者自定义,可能是对象类、可能是字符串、可能是数值。例如公式设置,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

虚拟设备在一般情况下不是经常用到,但是在一些特殊情况下可以完成特定的功能,为解决方案提供了可参考的实现方法。

3.4    协议驱动设计

协议驱动包括:发送数据协议驱动和接收数据协议驱动。分别对应着IRunDevice接口的SendProtocol和ReceiveProtocol属性。

     发送数据协议驱动主要负责把数据信息打包成byte[],以便通过IO通道进行发送。发送驱动主要包括ISendCommandDriver和ISendProtocol接口,他们的继承关系,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

ISendCommandDriver主要定义了命令函数和驱动函数,根据输入的命令字和辅助信息调用不同的命令函数,并且自定义把数据打包成byte[]。接口代码如下:

    /// <summary>
/// 这是命令驱动器,根据发送的不同命令调用相应的FunctionXX(byte[] data)
/// </summary>
public interface ISendCommandDriver
{
/// <summary>
/// 驱动Function00-FunctionFF函数
/// </summary>
/// <param name="devaddr">设备地址</param>
/// <param name="funNum">调用函数的命令字节</param>
/// <param name="cmd">命令字节数组</param>
/// <param name="obj">操作的对象</param>
/// <returns></returns>
byte[] DriverFunction(int devaddr,byte funNum,byte[] cmd,object obj); /// <summary>
/// 根据发送不同的命令调用不同的函数
/// </summary>
/// <param name="devaddr">设备地址</param>
/// <param name="cmd">发送的命令,升序地址为0的命令字节是主发送命令,其他的为命令参数</param>
/// <param name="obj">输入要操作的对象,用不到的情况,可以为NULL</param>
/// <returns>发送的字节数组</returns>
byte[] Function00(int devaddr, byte[] cmd, object obj);
byte[] Function01(int devaddr, byte[] cmd, object obj);
......
byte[] FunctionFF(int devaddr, byte[] cmd, object obj);
byte[] FunctionNone(int devaddr, byte[] cmd, object obj);
}

ISendProtocol主要定义了校验数据和获得要发送的数据接口,是设备驱动最终要使用的接口。接口代码如下:

 public interface ISendProtocol:ISendCommandDriver
{
/// <summary>
/// 获得检验和
/// </summary>
/// <param name="data">输入数据</param>
/// <returns></returns>
byte[] GetCheckData(byte[] data); /// <summary>
/// 获得发送数据命令
/// </summary>
/// <param name="addr">设备地址</param>
/// <param name="cmd">命令集合</param>
/// <param name="obj">操作对象</param>
/// <param name="isbox">是否是通讯箱通讯</param>
/// <returns>返回要发送的命令</returns>
byte[] GetSendCmdBytes(int addr, byte[] cmd, object obj, bool isbox);
}

      接收数据协议驱动主要负责对byte[]数据进行数据解析,以便后续业务的数据处理。接收驱动主要包括IReceiveCommandDriver和IReceiveProtocol接口,他们的继承关系,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

IReceiveCommandDriver与ISendCommandDriver类似,主要定义了命令函数和驱动函数,根据输入byte[]数据调用相应的命令函数,并且自定义把byte[]解析成需要的数据信息。接口代码如下:

 /// <summary>
/// 这是命令驱动器,根据返回的不同命令调用相应的FunctionXX(byte[] data)
/// </summary>
public interface IReceiveCommandDriver
{
/// <summary>
/// 这是命令驱动的入口,根据输入的不同命令调用不同的函数。
/// </summary>
/// <param name="funNum">输入解析的命令(0x00-0xff)</param>
/// <param name="data"></param>
/// <returns></returns>
object DriverFunction(byte funNum, byte[] data, object obj); /// <summary>
/// 不同的命令字节对相着不同的函数
/// </summary>
/// <param name="data">输入接收到的数据</param>
/// <returns>返回解析的类实例,用户可能自定义该函数的具体操作</returns>
object Function00(byte[] data, object obj);
object Function01(byte[] data, object obj);
......
object FunctionFF(byte[] data, object obj);
object FunctionNone(byte[] data, object obj);
}

IReceiveProtocol主要定义解析byte[]数据的各部分接口函数,是设备驱动最终要使用的接口。接口代码如下:

  public interface IReceiveProtocol :IReceiveCommandDriver
{
/// <summary>
/// 解析当前接收到的数据,此函数已经重写,用到了ICommandDriver的DriverFunction函数
/// </summary>
/// <param name="data">输入接收到的数据</param>
/// <param name="obj">输入其他辅助参数</param>
/// <param name="analysistype">协议解析的具体方式,如果开发者重写该函数,可以用到该参数</param>
/// <returns>返回具本的对象</returns>
object GetAnalysisData(byte[] data, object obj, int analysistype);
/// <summary>
/// 数据校验
/// </summary>
/// <param name="data">输入接收到的数据</param>
/// <returns>true:校验成功 false:校验失败</returns>
bool CheckData(byte[] data); /// <summary>
/// 获得命令集全,如果命令和命令参数
/// </summary>
/// <param name="data">输入接收到的数据</param>
/// <returns>返回命令集合</returns>
byte[] GetCommand(byte[] data); /// <summary>
/// 获得该设备的地址
/// </summary>
/// <param name="data">输入接收到的数据</param>
/// <returns>返回地址</returns>
int GetAddress(byte[] data); /// <summary>
/// 协议头
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
byte[] GetProHead(byte[] data); /// <summary>
/// 协议尾
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
byte[] GetProEnd(byte[] data); /// <summary>
/// 状态
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
object GetState(byte[] data); /// <summary>
/// 根据查ProHead ProEnd找可用信息,和ProHead ProEnd配合使用
/// </summary>
/// <param name="data">接收到的数据</param>
/// <param name="sbytes">协议头</param>
/// <param name="ebytes">协议尾</param>
/// <returns></returns>
byte[] FindAvailableBytes(byte[] data);// byte[] sbytes, byte[] ebytes /// <summary>
/// 协议头
/// </summary>
byte[] ProHead { set;get; } /// <summary>
/// 协议尾
/// </summary>
byte[] ProEnd { set;get;}
//---------------------------------------------------------------------------//
}

这就是发送协议和接收协议驱动,设计的比较简单、容易理解。但是驱动各命令函数的代码,使用的是if…else if…else实现,显得代码笨拙、而且效率不高。当时为了完成功能,只是简单的这样实现了,并没有多想,并且延续使用到现在,考虑到老设备驱动的兼容性,并没有做出相应的改进。

      协议驱动改进如下:

针对协议驱动部分有很多种设计的方式,例如用命令设计模式与插件加载的方式。这种方式包括三部分:命令接口、协议驱动器、命令实体类。

命令接口规定了协议驱动器在调用命令时涉及到的属性和动作,所有命令类型都必须继承自命令接口,接口定义代码如下:

public interface ICommand
{
/// <summary>
/// 命令标识
/// </summary>
byte Command { get; } /// <summary>
/// 执行命令
/// </summary>
/// <param name="para">输入的参数</param>
/// <returns>对象数据</returns>
object Excute(object para);
}

协议驱动器负责以插件的方式加载程序集中的命令,并且根据命令字驱动不同的命令和作出响应,驱动器的代码如下:

public class ProtocolDriver
{
private List<ICommand> _cmdCache; /// <summary>
/// 构造
/// </summary>
public ProtocolDriver()
{
_cmdCache=new List<ICommand>();
} /// <summary>
/// 析构
/// </summary>
~ProtocolDriver()
{
if(_cmdCache.Count>0)
_cmdCache.Clear();
_cmdCache = null;
}
/// <summary>
/// 初始化命令,并加载到缓存,第一次运行会慢些,但是后续的执行效率很高
/// </summary>
public void InitDriver()
{
Assembly asm = Assembly.GetExecutingAssembly();
Type[] types = asm.GetTypes();
foreach (Type t in types)
{
if (typeof(ICommand).IsAssignableFrom(t))
{
if (t.Name != "ICommand")
{
ICommand cmd =(ICommand)t.Assembly.CreateInstance(t.FullName);
_cmdCache.Add(cmd);
}
}
}
} /// <summary>
/// 根据命令字驱动不同的命令
/// </summary>
/// <param name="cmdByte"></param>
/// <returns></returns>
public object DriverCommand(byte cmdByte)
{
ICommand cmd = _cmdCache.FirstOrDefault(c => c.Command == cmdByte);
if (cmd != null)
return cmd.Excute(null);
else
return null;
}
}

每个命令实体类都继承自ICommand接口,并实现该接口,我们实现两个自定义命令实体类:CommandA和CommandB。代码如下:

public class CommandA:ICommand
{
public byte Command
{
get { return 0x0a; }
} public object Excute(object para)
{
return "CommandA";
}
}
public class CommandB:ICommand
{
public byte Command
{
get { return 0x0b; }
}
public object Excute(object para)
{
return "CommandB";
}
}

接下来,我们写一段测试代码,对协议驱动器进行测试,代码如下:

ProtocolDriver driver=new ProtocolDriver();
driver.InitDriver();
Console.WriteLine(driver.DriverCommand(0x0a));
Console.WriteLine(driver.DriverCommand(0x0b));
Console.Read();

最后的测试结果,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

这是一个改进的协议驱动器,发送协议和接收协议都可以这样应用,大家在设计这部分工作的时候可以进行参考。

当然,也可以通过配置文件的方式来开发一个协议驱动,有兴趣朋友可以研究一下。

3.5    命令缓存设计

IRunDevice接口中的CommandCache属性是一个命令缓存器,可以把设备要发送的命令数据临时存储在命令缓存中,框架平台会通过调用GetSendBytes接口函数来提取命令缓存器中的命令数据,之后会从缓存器中删除该命令数据;如果命令缓存器不存在任何命令数据,那么会调GetRealTimeCommand接口函数来获得默认的命令数据,代码如下:

 /// <summary>
/// 获得发送字节数组
/// </summary>
/// <returns></returns>
public byte[] GetSendBytes()
{
byte[] data = new byte[] { };
//如果没有命令就增加实时数据的命令
if (this.CommandCache.Count <= 0)
{
data = this.GetRealTimeCommand();
this.RunDevicePriority = RunDevicePriority.Normal;
}
else
{
data = this.CommandCache.GetCacheCommand();
this.RunDevicePriority = RunDevicePriority.Priority;
}
return data;
}

命令缓存器主要包括两部分:命令对象和命令缓存。

命令对象是一个实体类,主要是对关键字、byte[]数组和优先级属性进行封装。命令缓存在获得命令对象时对优先级进行了判断,以便优先发送命令级别高的数据信息。命令对象代码如下:

    public interface ICommand
{
/// <summary>
/// 命令
/// </summary>
byte[] CmdBytes { get; } /// <summary>
/// 命令名称
/// </summary>
string CmdKey { get; }
/// <summary>
/// 发送优先级,暂时不用
/// </summary>
CommandPriority Priority { get; }
}

命令缓存是用于对命令对象的管理器,完整代码如下:

/// <summary>
/// 线程安全的轻量泛型类提供了从一组键到一组值的映射。
/// </summary>
/// <typeparam name="TKey">字典中的键的类型</typeparam>
/// <typeparam name="TValue">字典中的值的类型</typeparam>
public class CommandCache
{
#region Fields
/// <summary>
/// 内部的 Dictionary 容器
/// </summary>
private List<Command> _CmdCache = new List<Command>(); /// <summary>
/// 用于并发同步访问的 RW 锁对象
/// </summary>
private ReaderWriterLock rwLock = new ReaderWriterLock();
/// <summary>
/// 一个 TimeSpan,用于指定超时时间。
/// </summary>
private readonly TimeSpan lockTimeOut = TimeSpan.FromMilliseconds(100);
#endregion #region Methods
/// <summary>
/// 将指定的键和值添加到字典中。
/// Exceptions:
/// ArgumentException - Dictionary 中已存在具有相同键的元素。
/// </summary>
/// <param name="key">要添加的元素的键。</param>
/// <param name="value">添加的元素的值。对于引用类型,该值可以为 空引用</param>
public void Add(string cmdkey, byte[] cmdbytes)
{
this.Add(cmdkey, cmdbytes, CommandPriority.Normal);
} public void Add(string cmdkey, byte[] cmdbytes, CommandPriority priority)
{
rwLock.AcquireWriterLock(lockTimeOut);
try
{
Command cmd = new Command(cmdkey, cmdbytes,priority);
this._CmdCache.Add(cmd);
}
finally { rwLock.ReleaseWriterLock(); }
} public void Add(Command cmd)
{
rwLock.AcquireWriterLock(lockTimeOut);
try
{
if (cmd == null) return;
this._CmdCache.Add(cmd);
}
finally { rwLock.ReleaseWriterLock(); }
} /// <summary>
/// 删除命令
/// </summary>
/// <param name="cmdkey"></param>
public void Remove(string cmdkey)
{
rwLock.AcquireWriterLock(lockTimeOut);
try
{
for (int i = 0; i < this._CmdCache.Count; i++)
{
if (String.Compare(this._CmdCache[i].CmdKey, cmdkey) == 0)
{
this._CmdCache.RemoveAt(i);
break;
}
}
}
finally { rwLock.ReleaseWriterLock(); }
} /// <summary>
/// 中移除所有的键和值。
/// </summary>
public void Clear()
{
if (this._CmdCache.Count > 0)
{
rwLock.AcquireWriterLock(lockTimeOut);
try
{
this._CmdCache.Clear();
}
finally { rwLock.ReleaseWriterLock(); }
}
} /// <summary>
/// 按优先级获得命令
/// </summary>
/// <param name="priority"></param>
/// <returns></returns>
private byte[] GetCacheCommand(CommandPriority priority)
{
if (this._CmdCache.Count <= 0) return new byte[] { };
rwLock.AcquireReaderLock(lockTimeOut);
try
{
byte[] cmd = new byte[] { };
if (priority == CommandPriority.Normal)
{
cmd = this._CmdCache[0].CmdBytes;
this._CmdCache.RemoveAt(0);
}
else
{
for (int i = 0; i < this._CmdCache.Count; i++)
{
if (this._CmdCache[i].Priority==CommandPriority.High)
{
cmd = this._CmdCache[i].CmdBytes;
this._CmdCache.RemoveAt(i);
break;
}
}
}
return cmd;
}
finally
{
rwLock.ReleaseReaderLock();
}
} /// <summary>
/// 顺序获得命令
/// </summary>
/// <returns></returns>
public byte[] GetCacheCommand()
{
return GetCacheCommand(CommandPriority.Normal);
} public int Count
{
get { return this._CmdCache.Count; }
}
#endregion
}

这里用到了ReaderWriterLock读写同步锁,用于同步对资源的访问。 在特定时刻,它允许多个线程同时进行读访问,或者允许单个线程进行写访问。 ReaderWriterLock 所提供的吞吐量比简单的一次只允许一个线程的锁(如 Monitor)更高。尽管框架平台的性能要求并没有太高,但是在设计的时候还是要保持一定的余地和超前性。

3.6    数据持久化设计

框架平台中的数据持久化默认采用的是序列化和反序列化技术,主要针对参数数据和实时数据,方便进行扩展,适用于数据量不大的应用场景。但是,当软件异常退出或是PC机突然断电时,如果正在序列化数据,那么序列化文件有可能遭到破坏;再次重新启动软件进行反序列化的时候,将会出现异常,可能导致软件无法正常启动。

为了解决这个问题,框架平台从两方面进行了考虑:框架本身的稳定性、技术手段。框架的稳定性经过多年来的考验表现很不错;技术手段方面,在反序列化时对序列化文件进行有效性验证,如果判断文件遭到破坏,那么会调用RepairSerialize修复文件接口,对文件进行修复。接口代码,如下:

public interface ISerializeOperation
{
/// <summary>
/// 保存序列化的文件路径
/// </summary>
string SaveSerializePath { get;} /// <summary>
/// 保存序列化文件
/// </summary>
/// <typeparam name="T">序列化类型</typeparam>
/// <param name="t">序列化实例</param>
void SaveSerialize<T>(T t); //Serialize /// <summary>
/// 获得序列化实例
/// </summary>
/// <typeparam name="T">序列化类型</typeparam>
/// <returns>序列化实例</returns>
T GetSerialize<T>(); //Deserialize /// <summary>
/// 删除序列化文件
/// </summary>
void DeleteSerializeFile(); /// <summary>
/// 修复序列化文件
/// </summary>
/// <typeparam name="T">序列化类型</typeparam>
/// <param name="devid">设备ID</param>
/// <returns>序列化实例</returns>
object RepairSerialize(int devid, int devaddr, string devname);
}

数据持久化本质上就是一个接口和一个可序列化的抽象类,IRunDevice的DeviceRealTimeData实时数据属性和DeviceParameter参数数据属性就是继承自ISerializeOperation接口。如果二次开发者想把数据存储在SQL Server或其他数据库,可以直接继承ISerializeOperation接口,在序列化SaveSerialize接口中写保存操作,在反序列化GetSerialize中写读取操作。而不是继承SerializeOperation抽象类,它只提供了序列化和反序列化XML文件的操作。

当然,这块也有很大的改进余地,第一、接口名称的定义不太好。第二、可以提供多种存储方案,类似于ORM框架。

3.7    IO数据交互设计

IRunDevice提供了读IO数据和写IO数据的接口函数,框架平台并把IO对象实例输入给读、写接口函数,二次开发时可以重写这两个接口函数,完成对IO的复杂操作,例如:多次发送数据、循环读取数据等操作。接口函数定义如下:

public interface IRunDevice
{
......
/// <summary>
/// 发送IO数据接口
/// </summary>
/// <param name="senddata"></param>
void Send(IIOChannel io, byte[] senddata); /// <summary>
/// 读取IO数据接口
/// </summary>
/// <param name="io"></param>
/// <returns></returns>
byte[] Receive(IIOChannel io);
......
}

IIOChannel接口代表IO通道,串口IO和网络IO都继承自这个接口,完成各自的可实例化的操作类。继承关系如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

IIOChannel接口定义如下:

 public interface IIOChannel:IDisposable
{
/// <summary>
/// 同步锁
/// </summary>
object SyncLock { get; } /// <summary>
/// IO关键字
/// </summary>
string Key { get; } /// <summary>
/// IO通道,可以是COM,也可以是SOCKET
/// </summary>
object IO{get;} /// <summary>
/// 读IO;
/// </summary>
/// <returns></returns>
byte[] ReadIO(); /// <summary>
/// 写IO
/// </summary>
int WriteIO(byte[] data); /// <summary>
/// 关闭
/// </summary>
void Close(); /// <summary>
/// IO类型
/// </summary>
CommunicationType IOType { get; } /// <summary>
/// 是否被释放了
/// </summary>
bool IsDisposed { get; }
}

RunDevice1设备抽象类继承IRunDevice接口,很简单的实现了读IO数据和写IO数据的接口函数,实现代码如下:

  /// <summary>
/// 发送数据接口,用于设备发送bytes,可重写
/// </summary>
/// <param name="io">IO通道</param>
/// <param name="sendbytes">字节数据组</param>
public virtual void Send(SuperIO.CommunicateController.IIOChannel io, byte[] sendbytes)
{
io.WriteIO(sendbytes);
} /// <summary>
/// 接收数据接口,用于接收bytes,可重写
/// </summary>
/// <param name="io">IO通道</param>
/// <returns>返回字节数组</returns>
public virtual byte[] Receive(SuperIO.CommunicateController.IIOChannel io)
{
return io.ReadIO();
}

这两个是虚函数,可以重写(override)这两个函数,完成自定义操作。设备驱动继承SuperIO.Device.RunDevice1抽象类,里边有一个虚函数Send(IIOChannel io, byte[] sendbytes),io参数为通讯操作实例,sendbytes参数为要发送的数据信息,可以重写这个接口函数,完成特殊的发送数据要求。代码如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

接收完数据,需要把串口设置修改成默认的配置,避免影响其他设备驱动的通讯,代码如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

如果是网络通讯方式,可以把IO实例转换成ISessionSocket接口,进行自定义操作。对于IO数据交互这部分的设计还是比较灵活的,给予二次开发更多的灵活性。

3.8    通讯状态设计

设备通讯状态包括:未知IO、通讯正常、通讯干扰、通讯中断和通讯未知。分别对应着IRunDevice接口中的UnknownIO、DealData、CommunicateInterrupt、CommunicateError和CommunicateNone函数接口,接收到的数据信息会经过数据校验,得到不同的状态会调用相对应的函数接口。

未知IO状态,代表IO通道实例为null或无法正常发送和接收数据,例如:当串口通讯时串口无法打开、当网络通讯时没有连接的Socket等。

通讯正常,代表接收的数据通过了IRunDevice接口的ReceiveProtocol接收协议的CheckData函数的校验,可以对数据进行解析,并对数据进行后续的处理。

通讯干扰,代表可能在通讯过程中受到外界的电磁干扰、接收的数据有丢包现象、以及接收到的数据有粘包现象等。也就是说byte[]数据流和接收协议不匹配,可以不做任何数据解析和处理操作,也可以对已接收到的数据信息进行二次匹配操作。

通讯中断,代表接收操作超时返回,并且未接收到任何byte[]数据信息,不做任何数据解析和处理操作。

通讯未知,代表设备通讯的初始状态,不具有任何意义,但是框架平台保留了该状态,一般情况下不会有调用CommunicateNone函数接口的情况。

在检测通讯状态时,如果发生了通讯状态改变,那么会调用CommunicateChanged(IOState ioState)接口函数,并把最新的通讯状态以参数的形式传递给该接口,可以通过该接口完成对状态改变的事件响应。

通讯状态检测的序列图如下:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

3.9    定时任务设计

每个设备驱动都有一个定时器,是对System.Timers.Timer时钟的二次封装,用于执行定时任务,例如:定时清除数据、在自控模式通讯机制下定时发送请求命令等。

定时任务包括三部分:IsStartTimer属性,用于启动和停止定时任务;TimerInterval属性,用于设置定时任务执行周期;DeviceTimer函数,如果IsStartTimer为启动定时任务,那么DeviceTimer会根据TimerInterval设置的周期定时被调用,这是一个虚函数。

接口代码如下:

/// <summary>
/// 是否开启时钟,标识是否调用DeviceTimer接口函数。
/// </summary>
bool IsStartTimer { set; get;} /// <summary>
/// 时钟间隔值,标识定时调用DeviceTimer接口函数的周期
/// </summary>
int TimerInterval { set; get;} /// <summary>
/// 设备定时器,响应定时任务
/// </summary>
void DeviceTimer();

3.10     运行优先级设计

设备运行优先级通过两部分来完成:1. IDeviceManager设备管理器中定义了GetPriorityDevice接口,用于返回当前高优先级的设备驱动(IRunDevice)。2. IRunDevice中定义了GetSendBytes,用于返回当前要发送的命令数据,同时设置当前设备的优先级。

IIOController接口在对设备列表进行任务调度的时候,首先,通过GetPriorityDevice接口获得优先级高的设备;其次,通过调用GetSendBytes获得要发送的命令数据。

    GetPriorityDevice接口的代码实现如下:

/// <summary>
/// 获得当前优先级别高的设备
/// </summary>
/// <param name="vals">每个控制器的可用设备数组</param>
/// <returns>高优先级的设备</returns>
public IRunDevice GetPriorityDevice(IRunDevice[] vals)
{
IRunDevice rundev = null;
foreach (IRunDevice dev in vals)
{
if (
dev.DeviceRealTimeData.IOState == IOState.Communicate
&&
(dev.RunDevicePriority == RunDevicePriority.Priority
||
dev.CommandCache.Count > 0))
{
rundev = dev;
break;
}
}
return rundev;
}

首先,当前设备必须为通讯状态,否则即使优先调度此设备也不具有实际意义;其次,当前设备的优先级属性为Priority,或者设备的命令缓存器有可用数据,判断其中一个属性条件为true。这两个条件同时符合的时候才能判断这个设备可以优先被调度。这是比较容易理解的。

     GetSendBytes接口代码如下:

/// <summary>
/// 获得发送字节数组
/// </summary>
/// <returns></returns>
public byte[] GetSendBytes()
{
byte[] data = new byte[] { };
//如果没有命令就增加实时数据的命令
if (this.CommandCache.Count <= 0)
{
data = this.GetRealTimeCommand();
this.RunDevicePriority = RunDevicePriority.Normal;
}
else
{
data = this.CommandCache.GetCacheCommand();
this.RunDevicePriority = RunDevicePriority.Priority;
}
return data;
}

这块的逻辑是这样的,如果检测到命令缓存器有命令数据,获得当前命令数据后把当前设备设置成高优先级。当某个设备驱动要定时读取硬件设备的数据信息(非实时数据),这是一个特殊的命令,在发送完最后一个命令缓存器的命令数据后,为了验证硬件设备状态的一致性和持续性,下一次调度要再次执行当前的设备。例如:对硬件设备进行实时校准。

优先级调度设备只涉及到这两处,也是经过长期的应用,最终确定的方案。

3.11     授权设计

IRunDevice有一个IsRegLicense属性,用于标识当前设备是否被授权,如果这个属性为false,那么会调用UnRegDevice接口函数,做出相应的事件响应。

这块设计的比较简单,主要考虑到框架平台可以对整个软件进行授权,也可以对设备本身进行授权。

3.12     事件响应设计

每个设备都具有7个事件,以完成不同的功能。IDeviceController总体控制

器会对设备的事件进行订阅,并且做出响应和驱动其他模块,在《第6章     总体控制器的设计》中会进行详细的介绍。

下面对不同的事件进行详细的说明:

/// <summary>
/// 接收数据事件
/// </summary>
event ReceiveDataHandler ReceiveDataHandler;

说明:这只是一个预留的事件,如果设备调用该事件只是在运行监视器中显示当前设备接收了多个数据,并没有涉及更多的实际用处。一般情况下可以不需要调用,可以通过DeviceRuningLogHandler事件完成同样的功能。

/// <summary>
/// 发送数据事件
/// </summary>
event SendDataHandler SendDataHandler;

说明:一开始这个事件与ReceiveDataHandler事件类似。但是,后来增加了自控通讯模式,对于网络通讯的时候,设备可以自定义定时发送数据。这个事件涉及到对设备和网络控制器的操作。

/// <summary>
/// 设备日志输出事件
/// </summary>
event DeviceRuningLogHandler DeviceRuningLogHandler;

说明:这个事件与运行监视器关联,触发这事件后,会把字符信息显示到运行监视器里。如下图:

/// <summary>
/// 更新设备运行器事件
/// </summary>
event UpdateContainerHandler UpdateContainerHandler;

说明:触发该事件会更新该设备在设备运行器中的数据信息。如下图:

/// <summary>
/// 串口参数改变事件
/// </summary>
event COMParameterExchangeHandler COMParameterExchangeHandler;

说明:当触发该事件会对串口控制器和串口IO进行操作,涉及的实例对象会动态变化。新版本框架平台中对该操作进行了优化,逻辑比较清晰、效率有所提高。

/// <summary>
/// 设备数据对象改变事件
/// </summary>
event DeviceObjectChangedHandler DeviceObjectChangedHandler;

说明:这个事件比较重要,是驱动其他相关模块的事件源,二次开发者可以自定义数据对象,并把数据对象通过此事件进行响应,自定义显示接口、自定义输出数据接口、服务接口等会得到传递过来的数据对象,相当于驱动其他模块的事件源。

/// <summary>
/// 删除设备事件
/// </summary>
event DeleteDeviceHandler DeleteDeviceHandler;

说明:这个是删除设备事件,触发该事件后,框架平台会释放IO资源、IO控制器资源、以及修改配置文件信息等,一切操作成功后会调用IRunDevice中的DeleteDevice接口函数,可以此函数中写释放设备的资源,因为框架平台并不知道二次开发者在设备驱动中都用到了什么资源。

3.13     上下文菜单设计

允许二次开发者自定义设备驱动的上下文菜单(ContextMenu),因为不同类型的硬件设备肯定会存在功能上的差异,这种差异可以在菜单中体现。

当鼠标右键单击设备运行器中的设备时,要求它弹出上下文菜单,这是一个基本的功能。框架平台监听鼠标事件,如果是鼠标右键事件,则会调用IRunDevice中的ShowContextMenu接口函数,以便显示自定义的上下文菜单。

实现的代码也很简单,如下:

/// <summary>
/// 显示菜单
/// </summary>
public override void ShowContextMenu()
{
this.contextMenuStrip1.Show(Cursor.Position);
}

3.14     IO通道监视设计

IO通道监视用于显示当前设备发送和接收的十六进制数据,对于设备的调试很有意义。所以,IRunDevice设备驱动提供了两个接口函数完成此项功能:ShowMonitorIODialog()函数,用于显示IO监视窗体;ShowMonitorIOData(byte[] data, string desc)函数,控制器在调度设备的时候会调用此函数,用于显示当前发送和接收的数据信息,一般情况二次开发下不需要调用这两个函数,框架平台已经集成了这项功能。如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计

3.15     关闭框架平台

关闭框架平台比启动框架平台要复杂,涉及到:释放托管资源、非托管资源、释放资源的先后顺序、线程退出等一系列的问题,如果处理不好,那么有可能造成软件界面退出了,后台的进程还存在,给数据处理、以及再次启动框架平台带来意想不到的问题。在后续的章节中会分部分进行介绍。

当关闭框架平台,内部会调用IRunDevice设备驱动的ExitDevice接口函数,这个函数与DeleteDevice有本质的区别。ExitDevice退出设备接口可能要对状态进行初始值设置和数据置0操作,因为框架平台本身退出了,如果不进行该项操作,Web业务系统并不知道框架平台处于什么的状态。DeleteDevice删除设备接口不涉及到ExitDevice相关操作,直接把记录信息删除就可以了,同时框架内部还涉及到对IO通道、控制器、以及配置文件的操作。

3.16     小结

设备驱动设计这块并没有涉及到复杂的技术应用,但是涉及到的内容比较多。关键是根据应用场景,我们要赋予它什么样的功能、特性、属性等,是综合考虑、设计的复杂过程。一开始的设计中并没有这么多内容,在应用过程中可能这有点不合适、那有点不适合、需要增加新的东西等等,在慢慢改进、完善,所以说设计并不是多么高深的领域,而是在把握大方向、结构化后渐进完善细节的过程。

技术都很简单,但是把众多简单的技术组装在一起就不一定简单了。有人问你1+1等于几?有人回答是2,有人回答是10,答案都是对的。简单的技术的不同组合、不同场景的应用得到的效率也不一样。

作为设备驱动接口,它的职能很单一,就是在框架平台内部进行交互;可是,随着交互的维度、区域的不同,它的职能显示又很复杂,也很重要。

作者:唯笑志在

Email:504547114@qq.com

QQ:504547114

.NET开发技术联盟:54256083

文档下载:http://pan.baidu.com/s/1pJ7lZWf

官方网址:http://www.bmpj.net