用C#开发.NET CF 蓝牙通信模块

用 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 。

我们看 <font

Published At
Categories with Web编程
Tagged with
comments powered by Disqus