用C#開發.Net CF 藍牙通信模塊
在Windows Mobile軟件開發中.Net正扮演著日益重要的角色,我們已經可以看到很多用.Net CF開發的軟件,這些軟件涉及到了日常應用的方方面面。在智能設備的軟件開發中,無線互聯是一個相當重要的一塊,我們可以看到,紅外幾乎是所有智能設備的標配,而藍牙也日益在越來越多的智能設備上出現,有了硬件,顯然要有相應的軟件相關的應用。
我們也知道,用.NET CF開發紅外通信應用時相當輕松的,因為.NET CF中有一個命名空間System.Net.IrDA就是用於紅外通信的通信模塊。但是,.NET CF中還沒有關於藍牙通信的模塊,所以目前來講做這方面的開發還有一定的困難。下面,就談談如何用C#開發.Net CF藍牙通信模塊。
一. 基本要點
首先明確一點,因為涉及到驅動硬件的問題,所以僅靠了解C#開發的相關知識顯然是無法完成開發的,我們必須對C++開發有所了解。但是為了簡單起見,我們不希望用C++寫半行代碼,所有的編碼工作全部使用C#,也就是說,使用的開發環境只需要使用Visual Studio.Net,不需要用其他的編輯器。
作為開發這類驅動硬件的程序的知識准備,您需要了解C++的基本知識,知道頭文件是怎麼一回事,知道托管代碼如何與非托管代碼交互。因為本文的核心是說明如何開發.Net CF藍牙通信模塊,所以前述這些准備知識並不作講述。
二. 關於藍牙
做藍牙通信模塊開發,自然先要知道藍牙通信是怎麼一回事。在我看來,藍牙通信應該和紅外通信模塊類似,當然我是從開發者的角度來講,抽象化以後應該就是這樣,當然藍牙和紅外通信也有很多不一樣的地方,這在面向對象設計裡面怎麼講,我想一定有很多人理解的比我透徹。好了,這就是我們的基本思路了。我曾經在網上查過關於藍牙開發的文章,很多人在.Net CF開發中把藍牙通信當作一個串行通信來處理,這也是不錯的,但是我不是很喜歡,因為這樣做的話,並不是針對藍牙來開發的,換言之,在使用過程中,需要先手動開啟藍牙,配對,連接,建立串行通道,然後開啟應用程序使用,你還要在應用程序中設置串行端口,對最終用戶來講,這是非常麻煩的。我覺得,這樣的解決方案冠上藍牙通信的名頭簡直就是……不多說了,書歸正傳。
在紅外通信中,我們知道,設備的DeviceID是一個Byte數組,那麼藍牙設備的DeviceID什麼樣子呢?我想這個大家都很清楚,是一串以“:”分隔的16進制數字。
紅外通信中,一般而言紅外並沒有開啟、關閉之類的狀態,但是藍牙有開啟、關閉、可發現三種狀態。
紅外沒有安全設置,而藍牙有安全設置,所以我們需要對藍牙設備進行配對,而紅外通信這部需要。
我們查看.Net的Socket地址族裡有IrDA,但是沒有藍牙相關的地址族,這是我們需要解決的問題。
三. 獲取設備ID
1.獲取本地設備的ID
我們查看Window CE 4.2的SDK文檔,得知獲取本地設備ID的函數是BthReadLocalAddr,在btdrt.dll中。SDK文檔中的英文原文是這樣的:“This function retrIEves the Bluetooth address of the current device.”好了,知道了這個就好說了:
首先封裝本地托管函數:
[DllImport("Btdrt.dll", SetLastError=true)]
public static extern int BthReadLocalAddr(byte[] pba);
這個函數得到的本地DeviceID也是一組byte數組,為了向人們顯示出來,我們要把它變為String:
string text1 = "";
text1 = text1 + pba[5].ToString("X2") + ":";
text1 = text1 + pba [4].ToString("X2") + ":";
text1 = text1 + pba [3].ToString("X2") + ":";
text1 = text1 + pba [2].ToString("X2") + ":";
text1 = text1 + pba [1].ToString("X2") + ":";
return (text1 + pba [0].ToString("X2"));
2.獲取遠程設備的ID
其實談到獲取遠程設備的ID就涉及到如何去發現遠程設備了,所以這裡就一並把發現設備的方法也說明了吧。
發現設備需要用到三個Winsock API,分別是WSALookupServiceBegin、WSALookupServiceNext和WSALookupServiceEnd,這三個API到底起什麼作用可以去查看Windows CE 4.2的SDK,這裡就不詳細解釋了,只談一下幾個需要注意的地方。
WSALookupServiceBegin的函數原形是這樣的:
INT WSALookupServiceBegin(
LPWSAQUERYSET lpqsRestrictions,
DWord dwControlFlags,
LPHANDLE lphLookup
);
我們用托管代碼進行包裝:
[DllImport("ws2.dll", EntryPoint="WSALookupServiceBegin", SetLastError=true)]
public static extern int CeLookupServiceBegin(byte[] pQuerySet, LookupFlags dwFlags, ref int lphLookup);
可以看到,本來lpqsRestrictions是一個struct,經過包裝後在托管代碼中成為了byte[],我們計算好該struct大概要占用多少個byte,struct中每一個成員在byte數組中的位置是怎樣的,裝配出來就好了。
由於是針對藍牙作的開發,所以我們要查看一下這些參數應該是哪些值。Windows CE 4.2的SDK中說,藍牙開發時,struct LPWSAQUERYSET中的如下成員應當為這些值:
The dwSize member must be sizeof(WSAQUERYSET).
The lpBlob member (itself a pointer to a BLOB structure) is optional, but if used, the device inquire parameters valid for LUP_FLUSHCACHE are the following:
The cbSize member of the BLOB structure must be sizeof(BTH_QUERY_DEVICE).
The pBlobData member is a pointer to a BTH_QUERY_DEVICE structure, for which the LAP member is the Bluetooth inquiry Access code, and the length member is the length of the inquiry, in seconds.
The dwNameSpace member must be NS_BTH.
All other WSAQUERYSET members are ignored.
具體什麼意思各位可以自己去理解,我想比我翻譯出來要好些,畢竟我英語很差的。根據以上要求,我們這樣裝配pQuerySet:
byte[] buffer1 = new byte[0x400];
BitConverter.GetBytes(60).CopyTo(buffer1, 0);
GCHandle handle1 = GCHandle.Alloc(blob1.ToByteArray(), GCHandleType.Pinned);
IntPtr ptr1 = handle1.AddrOfPinnedObject();
BitConverter.GetBytes((int) (ptr1.ToInt32() + 4)).CopyTo(buffer1, 0x38);
另外的兩個API也照類似方法調用即可。
在調用了WSALookupServiceNext之後,bytes數組pQuerySet中便包含了遠程設備的地址信息,下面我們需要把它找出來。通過閱讀SDK中WSAQUERYSET結構的說明和計算每個成員的位置之後,我們寫出如下代碼:
int num5 = BitConverter.ToInt32(buffer1, 0x30);
int num6 = Marshal.ReadInt32((IntPtr) num5, 8);
int num7 = Marshal.ReadInt32((IntPtr) num5, 12);
SocketAddress address1 = new SocketAddress(AddressFamily.UnspecifIEd, num7);
因為.Net框架的地址族裡面沒有藍牙,所以我們這裡用的是AddressFamily.UnspecifIEd。
然後的工作就是從中獲取遠程設備的ID了:
前面我們已經計算出,這個Address裡面的前六個字節是byte數組形式的設備ID,第七到第二十二個字節是藍牙的Service Guid,在後面四個字節是端口號,所以我們只需要分別提取出來即可。
四. 監聽服務
監聽服務調用的是非托管API WSASetService,其原型是
INT WSASetService(
LPWSAQUERYSET lpqsRegInfo,
WSAESETSERVICEOP essOperation,
DWord dwControlFlags
);
可以看到關鍵也是第一個參數,lpqsRegInfo,這也是一個struct,我們的包裝方法與前面的發現設備采用的方法類似,做藍牙通信時要注意其成員要如下設置:
lpqsRegInfo
dwSize
sizeof(WSAQUERYSET)
lpszServiceInstanceName
Not supported on Windows CE. Set to 0.
lpServiceClassId
Not supported on Windows CE. Set to 0.
dwNameSpace
NS_BTH.
dwNumberOfCsAddrs
Not supported on Windows CE. Set to 0.
IpcsaBuffer
Not supported on Windows CE. Set to 0.
lpBlob
Points to a BTHNS_SETBLOB structure, containing information about the service to be added.
*
All other WSAQUERYSET fIElds are ignored.
五. 連接
我們知道,IrDA中連接遠程服務是使用方法System.Net.Sockets.IrDAClIEnt類中的Connect方法。而這個方法又是調用的Socket類中的Connect方法。而Socket類是一個比較抽象的類,它並不綁定某個具體的地址族、SocketType和protocolType,所以在實例化的時候,需要指定這三個參數。我們也知道,在IrDA中,這三個參數分別是AddressFamily.Irda, SocketType.Stream,和ProtocolType.IP,那麼在藍牙中這三個參數分別是什麼呢?我們好像找不到。
且慢,真是這樣嗎?
我們知道在.Net中,這三個參數都是枚舉值,而枚舉在默認情況下,你可以認為就是int值的替代表現。
我們該如何知道這三個參數到底是什麼呢?
還是先看Socket類的Connect方法。
我們查查有關資料,可以知道這個方法實際上是調用的一個非托管函數:
[DllImport("mscoree", EntryPoint="@339")]
public static extern int connect(int s, byte[] name, int namelen);
也就是非托管的Socket API。
我們看Windows CE 4.2的SDK,可以看到,在使用藍牙進行連接的時候,需要使用WinSock擴展。我們還可以看到,在使用藍牙進行連接的時候,三個參數分別應當是AF_BTH、SOCK_STREAM和BTHPROTO_RFCOMM,至於這三個參數分別代表什麼,我們就要查看相關的頭文件了。
我們找到ws2bth.h頭文件,可以看到AF_BTH代表十進制數32,而BTHPROTO_RFCOMM代表十六進制數0x0003,恰好和ProtocolType.Ggp代表的數值是一致的。所以,我們在實例化Socket時是這麼寫的:
new Socket((AddressFamily) 0x20, SocketType.Stream, ProtocolType.Ggp);
Socket實例化出來了,其他的當然就都好說了,這裡不再贅述。
六. 藍牙的安全設置
藍牙比紅外多了安全方面的設置,所以就需要多一些代碼來處理這些。具體也就不多說了,其實也就是一些非托管代碼的包裝調用,這些API在Btdrt.dll中:
獲取配對碼請求:
[DllImport("Btdrt.dll", SetLastError=true)]
public static extern int BthGetPINRequest(byte[] pba);
設置配對碼:
[DllImport("btdrt.dll", SetLastError=true)]
public static extern int BthSetPIN(byte[] pba, int cPinLength, byte[] ppin);
比較麻煩點的是配對,總共有三步操作:
首先是創建ACL連接:
[DllImport("Btdrt.dll", SetLastError=true)]
public static extern int BthCreateACLConnection(byte[] pbt, ref ushort phandle);
然後是配對碼驗證:
[DllImport("Btdrt.dll", SetLastError=true)]
public static extern int BthAuthenticate(byte[] pbt);
然後一定要關閉連接:
[DllImport("Btdrt.dll", SetLastError=true)]
public static extern int BthCloseConnection(ushort handle);
七. 設置藍牙無線電狀態
我們知道,藍牙無線電有打開、關閉、可發現三種狀態,那麼我們如何實現編程控制呢?
我想這個一定大家都知道了,因為網上有很多關於這個的文章:
先寫一個枚舉:
public enum RadioMode
{
Connectable = 1,
Discoverable = 2,
PowerOff = 0
}
然後寫一個函數調用非托管代碼即可:
[DllImport("BthUtil.dll", SetLastError=true)]
public static extern int BthSetMode(RadioMode dwMode);
獲取無線電狀態的話就用下面的函數:
[DllImport("BthUtil.dll", SetLastError=true)]
public static extern int BthGetMode(ref RadioMode dwMode);
八. 已知的問題
可能是因為藍牙控制軟件還沒有實現標准化或者還是其他的問題,我們發現根據Windows CE 4.2 SDK 使用Winsock 擴展做的藍牙開發有一個問題,而且不論是本文中所述的托管代碼還是其他的非托管代碼,只要是用的這種思路用Winsock 2做的開發都會存在這樣一個問題,那就是不是在所有的Windows Mobile設備上都能正常運行。經過我的測試,我發現在很多使用另行開發的藍牙控制軟件的設備上,如聯想ET560、華碩MyPAL A730上都無法運行,而在沒有另行開發藍牙控制軟件的設備上是可以正常運行的,我不知道這是什麼原因,初步推測可能是廠商另行開發的藍牙控制軟件屏蔽了微軟的API的緣故,到底是不是這樣,還得請高人指點。