目 錄
第三章 設備驅動的設計... 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。如下圖:
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兩個接口,如下圖:
盡管IDeviceGraphics視圖接口在框架平台內部並沒有直接使用,但是在二次開發顯示窗體的時候可以使用到,在《第13章 二次開發及應用》中會涉及到這部分內容。另外,在這個地方是有繼續擴展的余地的,可以開發一個UI驅動管理器,專門UI進行布局、數據顯示、設置屬性、以及通過箭頭對數據流向進行動態設置等。
原則上,在二次開發過程中繼承RunDevice1抽象類完成設備驅動的開發,RunDevice不再繼續提供接口服務。現在公司內部都是通過RunDevice1接口來完成驅動開發。
下面對驅動接口的主要功能設計部分進行詳細的介紹,以便大家對接口有一個整體的了解,掌握設計思想,僅供參考。
設備初始化是通過InitDevice(int devid)接口來完成,當設備驅動以插件的方式加載到框架平台時首先就會調用該接口函數,對設備進行初始化設置、以及中載參數和某一點的實時數據。
在這裡可以通過序列化文件、數據庫、配置文件等作為數據源進行初始化設備,在二次開發中可以靈活掌握。框架默認支持XML序列化的方式進行初始化設備。在“3.6 數據持久化設計”小節中進行詳細介紹。
運行設備接口是保證設備驅動能夠在框架平台下被驅動運行,是通過同步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個接口函數已經在框架平台中實現了流程化,不過它們都是虛函數,二次開發者可以進行重寫,但是不建議這樣操作,可以會破壞框架事務的處理流程。
為什麼會有虛擬設備呢?主要是為了整合同類設備的數據信息,例如:設備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接口的設備驅動,同時也具備虛擬設備的功能。繼承關系,如下圖:
虛擬設備與普通設備可以通過DeviceType來區別,如果把設備類型DeviceType設置為Virtual,那麼就會標識此設備為虛擬設備,可以對此設備設置處理公式,非虛擬設備的數據通過RunVirtualDevice接口函數傳遞進來,obj對象數據完全是二次開發者自定義,可能是對象類、可能是字符串、可能是數值。例如公式設置,如下圖:
虛擬設備在一般情況下不是經常用到,但是在一些特殊情況下可以完成特定的功能,為解決方案提供了可參考的實現方法。
協議驅動包括:發送數據協議驅動和接收數據協議驅動。分別對應著IRunDevice接口的SendProtocol和ReceiveProtocol屬性。
發送數據協議驅動主要負責把數據信息打包成byte[],以便通過IO通道進行發送。發送驅動主要包括ISendCommandDriver和ISendProtocol接口,他們的繼承關系,如下圖:
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接口,他們的繼承關系,如下圖:
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();
最後的測試結果,如下圖:
這是一個改進的協議驅動器,發送協議和接收協議都可以這樣應用,大家在設計這部分工作的時候可以進行參考。
當然,也可以通過配置文件的方式來開發一個協議驅動,有興趣朋友可以研究一下。
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)更高。盡管框架平台的性能要求並沒有太高,但是在設計的時候還是要保持一定的余地和超前性。
框架平台中的數據持久化默認采用的是序列化和反序列化技術,主要針對參數數據和實時數據,方便進行擴展,適用於數據量不大的應用場景。但是,當軟件異常退出或是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框架。
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都繼承自這個接口,完成各自的可實例化的操作類。繼承關系如下圖:
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參數為要發送的數據信息,可以重寫這個接口函數,完成特殊的發送數據要求。代碼如下圖:
接收完數據,需要把串口設置修改成默認的配置,避免影響其他設備驅動的通訊,代碼如下圖:
如果是網絡通訊方式,可以把IO實例轉換成ISessionSocket接口,進行自定義操作。對於IO數據交互這部分的設計還是比較靈活的,給予二次開發更多的靈活性。
設備通訊狀態包括:未知IO、通訊正常、通訊干擾、通訊中斷和通訊未知。分別對應著IRunDevice接口中的UnknownIO、DealData、CommunicateInterrupt、CommunicateError和CommunicateNone函數接口,接收到的數據信息會經過數據校驗,得到不同的狀態會調用相對應的函數接口。
未知IO狀態,代表IO通道實例為null或無法正常發送和接收數據,例如:當串口通訊時串口無法打開、當網絡通訊時沒有連接的Socket等。
通訊正常,代表接收的數據通過了IRunDevice接口的ReceiveProtocol接收協議的CheckData函數的校驗,可以對數據進行解析,並對數據進行後續的處理。
通訊干擾,代表可能在通訊過程中受到外界的電磁干擾、接收的數據有丟包現象、以及接收到的數據有粘包現象等。也就是說byte[]數據流和接收協議不匹配,可以不做任何數據解析和處理操作,也可以對已接收到的數據信息進行二次匹配操作。
通訊中斷,代表接收操作超時返回,並且未接收到任何byte[]數據信息,不做任何數據解析和處理操作。
通訊未知,代表設備通訊的初始狀態,不具有任何意義,但是框架平台保留了該狀態,一般情況下不會有調用CommunicateNone函數接口的情況。
在檢測通訊狀態時,如果發生了通訊狀態改變,那麼會調用CommunicateChanged(IOState ioState)接口函數,並把最新的通訊狀態以參數的形式傳遞給該接口,可以通過該接口完成對狀態改變的事件響應。
通訊狀態檢測的序列圖如下:
每個設備驅動都有一個定時器,是對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();
設備運行優先級通過兩部分來完成: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; }
這塊的邏輯是這樣的,如果檢測到命令緩存器有命令數據,獲得當前命令數據後把當前設備設置成高優先級。當某個設備驅動要定時讀取硬件設備的數據信息(非實時數據),這是一個特殊的命令,在發送完最後一個命令緩存器的命令數據後,為了驗證硬件設備狀態的一致性和持續性,下一次調度要再次執行當前的設備。例如:對硬件設備進行實時校准。
優先級調度設備只涉及到這兩處,也是經過長期的應用,最終確定的方案。
IRunDevice有一個IsRegLicense屬性,用於標識當前設備是否被授權,如果這個屬性為false,那麼會調用UnRegDevice接口函數,做出相應的事件響應。
這塊設計的比較簡單,主要考慮到框架平台可以對整個軟件進行授權,也可以對設備本身進行授權。
每個設備都具有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接口函數,可以此函數中寫釋放設備的資源,因為框架平台並不知道二次開發者在設備驅動中都用到了什麼資源。
允許二次開發者自定義設備驅動的上下文菜單(ContextMenu),因為不同類型的硬件設備肯定會存在功能上的差異,這種差異可以在菜單中體現。
當鼠標右鍵單擊設備運行器中的設備時,要求它彈出上下文菜單,這是一個基本的功能。框架平台監聽鼠標事件,如果是鼠標右鍵事件,則會調用IRunDevice中的ShowContextMenu接口函數,以便顯示自定義的上下文菜單。
實現的代碼也很簡單,如下:
/// <summary> /// 顯示菜單 /// </summary> public override void ShowContextMenu() { this.contextMenuStrip1.Show(Cursor.Position); }
IO通道監視用於顯示當前設備發送和接收的十六進制數據,對於設備的調試很有意義。所以,IRunDevice設備驅動提供了兩個接口函數完成此項功能:ShowMonitorIODialog()函數,用於顯示IO監視窗體;ShowMonitorIOData(byte[] data, string desc)函數,控制器在調度設備的時候會調用此函數,用於顯示當前發送和接收的數據信息,一般情況二次開發下不需要調用這兩個函數,框架平台已經集成了這項功能。如下圖:
關閉框架平台比啟動框架平台要復雜,涉及到:釋放托管資源、非托管資源、釋放資源的先後順序、線程退出等一系列的問題,如果處理不好,那麼有可能造成軟件界面退出了,後台的進程還存在,給數據處理、以及再次啟動框架平台帶來意想不到的問題。在後續的章節中會分部分進行介紹。
當關閉框架平台,內部會調用IRunDevice設備驅動的ExitDevice接口函數,這個函數與DeleteDevice有本質的區別。ExitDevice退出設備接口可能要對狀態進行初始值設置和數據置0操作,因為框架平台本身退出了,如果不進行該項操作,Web業務系統並不知道框架平台處於什麼的狀態。DeleteDevice刪除設備接口不涉及到ExitDevice相關操作,直接把記錄信息刪除就可以了,同時框架內部還涉及到對IO通道、控制器、以及配置文件的操作。
設備驅動設計這塊並沒有涉及到復雜的技術應用,但是涉及到的內容比較多。關鍵是根據應用場景,我們要賦予它什麼樣的功能、特性、屬性等,是綜合考慮、設計的復雜過程。一開始的設計中並沒有這麼多內容,在應用過程中可能這有點不合適、那有點不適合、需要增加新的東西等等,在慢慢改進、完善,所以說設計並不是多麼高深的領域,而是在把握大方向、結構化後漸進完善細節的過程。
技術都很簡單,但是把眾多簡單的技術組裝在一起就不一定簡單了。有人問你1+1等於幾?有人回答是2,有人回答是10,答案都是對的。簡單的技術的不同組合、不同場景的應用得到的效率也不一樣。
作為設備驅動接口,它的職能很單一,就是在框架平台內部進行交互;可是,隨著交互的維度、區域的不同,它的職能顯示又很復雜,也很重要。
作者:唯笑志在
Email:[email protected]
QQ:504547114
.NET開發技術聯盟:54256083
文檔下載:http://pan.baidu.com/s/1pJ7lZWf
官方網址:http://www.bmpj.net