天天看點

用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的緣故,到底是不是這樣,還得請高人指點。