用C#開發.NET CF 藍芽通訊模組
最後更新:2017-02-28
來源:互聯網
上載者:User
用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++的基本知識,知道標頭檔是怎麼一回事,知道Managed 程式碼如何與Unmanaged 程式碼互動。因為本文的核心是說明如何開發.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
);
我們用Managed 程式碼進行封裝:
[DllImport("ws2.dll", EntryPoint="WSALookupServiceBegin", SetLastError=true)]
public static extern int CeLookupServiceBegin(byte[] pQuerySet, LookupFlags dwFlags, ref int lphLookup);
可以看到,本來lpqsRestrictions是一個struct,經過封裝後在Managed 程式碼中成為了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執行個體化出來了,其他的當然就都好說了,這裡不再贅述。
六. 藍芽的安全設定
藍芽比紅外多了安全方面的設定,所以就需要多一些代碼來處理這些。具體也就不多說了,其實也就是一些Unmanaged 程式碼的封裝調用,這些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
}
然後寫一個函數調用Unmanaged 程式碼即可:
[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 擴充做的藍芽開發有一個問題,而且不論是本文中所述的Managed 程式碼還是其他的Unmanaged 程式碼,只要是用的這種思路用Winsock 2做的開發都會存在這樣一個問題,那就是不是在所有的Windows Mobile裝置上都能正常運行。經過我的測試,我發現在很多使用另行開發的藍芽控制軟體的裝置上,如聯想ET560、華碩MyPAL A730上都無法運行,而在沒有另行開發藍芽控制軟體的裝置上是可以正常啟動並執行,我不知道這是什麼原因,初步推測可能是廠商另行開發的藍芽控制軟體屏蔽了微軟的API的緣故,到底是不是這樣,還得請高人指點。