蓝牙是一种低成本、短距离的无线通信技术。对于那些希望创建个人局域网(PANs)的人们来说,蓝牙技术已经越来越流行了。每个个人局域网都在独立设备的周围被动态地创建,并且为蜂窝式电话和PDA等设备提供了自动连接和即时共享数据的能力。
  
  为了在Java平台上开发支持蓝牙技术的软件,JCP定义了JSR82标准--Java蓝牙无线技术APIs(JABWT)。
  
  在这篇文章中,我将介绍一些关于蓝牙技术的背景,概述一下支持蓝牙技术的MIDlet应用程序的典型要素,然后介绍给你核心的Java蓝牙APIs。最后我们展示一些代码来演示如何使用这些APIs。
  
  实际上JSR82定义了两个独立的可选包:核心蓝牙API和对象交换(OBEX)API。这篇文章将对这两个中更为普遍的部分--核心蓝牙包javax.bluetooth进行详细地阐述,而OBEX API(对象交换),我们留到以后去讨论。
  
  蓝牙无线电技术基于在工业、科学以及医学(ISM)上公用的2.45GHz开放频段,这一频段无需授权并全球通用。当蓝牙设备互相连接时,他们将组成一个微微网(piconet),即以一个主设备和最大7个从设备的形式动态创建网络。蓝牙也支持piconet网之间的连接:当一个piconet中的主设备成为另一个piconet的从设备时,piconet与piconet间将形成桥接。
  
  蓝牙协议栈提供了一组的高层协议和API以完成发现服务和模拟串行I/O,还有一个关于包分割和重组的低层协议以及多路技术协议和质量服务。蓝牙互操作性profiles--不要与J2ME profiles搞混--它是用来描述跨平台互操作性和一致性需求的。
  
  蓝牙互操作性profiles包括三方面内容:通用访问profile(GAP)定义了设备管理功能性;服务发现应用profiles定义了服务发现方面的内容,串口profiles定义了互操作设备和模拟串口电缆的能力。你可以通过蓝牙规范(Bluetooth specification)学习这些和其它的profiles。蓝牙栈包含一个软件栈来映射一个固件栈(firmware),由图1所示:
   
  图片1:蓝牙协议栈
  
  JSR82揭示了蓝牙软件栈给Java平台的开发者。其中引起我们兴趣的是服务发现协议(SDP),用来模拟串口的串口profile RFCOMM,向上层协议提供诸如分割和重组等导向性连接的数据转换操作的逻辑链路控制及适配profile(L2CAP),以及多路技术协议。注意JABWT不支持无连接L2CAP。
  
  JABWT也包括对象交换API。OBEX也是高层API,它用来交换对象数据,诸如电子商业卡和日历标签之间以vCard和vCalendar的格式进行数据传输。在蓝牙上,对象交换通过RFCOMM发生。OBEX在最开始时是由红外(IrDA)引入的,并且它可以在IrDA协议、TCP/IP或者是其他协议的顶层实现。
  
  典型的蓝牙应用程序实例
  
  一个开启蓝牙功能的应用可以作为一个服务端或是一个客户端--一个服务的提供者或是消费者,或者它可以作为一个真正的点对点终端同时表现出服务和客户的行为。图2所示一个蓝牙规范用例:
   
  图2:一个典型的具有蓝牙功能的实际用例
  
  对这些用例的简要介绍:
  
  .初始化--所有具备蓝牙功能的应用程序必须先要初始化蓝牙栈。
  
  .客户端--一个客户对远端服务进行消费。首先它要发现所有附近的设备,然后对于每一个发现的设备搜索它感兴趣的服务。
  
  .服务器端--一个为客户端提供服务的服务器。它在服务发现数据库(SDDB)中对客户端进行注册,对他们进行有效广播。然后等待引入的连接,在他们进入时接受他们并为他们提供服务。最后,当不再需要服务时,应用程序会在服务发现数据库(SDDB)中将他们移除。图三:用例中参与活动的图表:
   
  图 3: 蓝牙应用程序活动图
  
  蓝牙应用程序中的元素
  
  图4显示了在MIDlet中一个典型蓝牙功能应用程序中的一些元素:
   
  图4: 具有蓝牙功能的 MIDlet (高端组织)
  
  中间的是核心应用程序My Bluetooth MIDlet,它扩展自javax.microedition.midlet.MIDlet。没有显示出来的还有MIDlet中实现的javax.microedition.lcdui.CommandListener以监听从用户接口中输入的命令。应用程序使用的剩余的类和接口都包含在了蓝牙规范中,像设备发现和服务,连接和服务消费,还有广播和提供服务。
  
  使用诸如MVC等设计模式是很好的实践。MVC把应用程序分解成用户接口(视图),应用程序行为和导航(控制器),以及数据(模型),当然在我们的案例中还要加上蓝牙API的支撑类和接口。像将分离的客户端、服务端行为构建成独立的类以便以后可以重用这些组件,也是很好的设计。
  
  Java蓝牙API核心概述
  
  JSR82需求的"最小公分母"是 受限连接设备配置(CLDC),可靠连接设备配置(CDC)是CLDC的超集,所以JABWT可以同时在CLDC和CDC上实现,简要地说,你可以在使用任何J2ME profile的上使用JABWT。在javax.bluetooth中我们可以发现,Java蓝牙API可以被分解为三个部分,在下面我们将讨论到它们:发现、设备管理和通信。
  
  蓝牙发现API
  
  客户端程序使用蓝牙发现API以搜索在其附近的设备和服务。服务代理类(DiscoveryAgent)同时支持设备与服务的发现。当设备和服务被发现时,想得到通知的客户端应用程序必须实现并注册DiscoveryListener接口,这个接口定义了设备发现通知和服务发现通知的回调。发现代理(DiscoveryAgent)与蓝牙客户端应用程序之间是典型的一对一的关系:
   
  图 5: DiscoveryAgent类和DiscoveryListener接口
  
  设备发现API
  
  你使用DiscoveryAgent类的"设备发现"方法来开始和取消设备发现:
  
  .retrieveDevices()重新获得已经发现或者附近的已知设备
  
  .startInquiry() 启动发现附近设备,也叫inquiry
  
  .cancelInquiry()取消当前进行的任何请求
  
  蓝牙发现代理在请求阶段的不同时候会分别调用DiscoveryListener(发现监听器)不同的回调方法:
  
  .deviceDiscovered() 指出是否有设备被发现。
  
  .inquiryCompleted() 指出是否请求已经成功、触发一个错误或已被取消。
  
  在图6中的状态图表阐明了设备发现的状态改变结束于相应的回调方法的返回。
   
  图 6: 设备发现状态表
  
  设备发现以调用startInquiry()函数开始。在请求进行时,蓝牙发现代理会在适当的时候调用回调方法DeviceDiscovered()和inquiryCompleted()。
  
  服务发现API
  
  你可以使用发现代理的服务发现方法来开始或取消服务发现:
  
  . selectService()启动服务发现搜索。(根据API手册应为尝试定位一个服务)
  
  . searchServices()启动服务发现搜索。
  
  . cancelServiceSearch()取消在正在进行中的任何的服务发现搜索操作。蓝牙发现代理在服务发现阶段的不同时候会分别调用DiscoveryListener的服务发现回调方法:
  
  . servicesDiscovered() 表示是否服务已被发现。
  
  . serviceSearchCompleted()表示服务发现是否已经完成。
  
  图7阐明了服务发现的状态改变结束于DiscoveryListener的回调方法的返回。
   
  图 7: 服务发现状态图表
  
  服务发现开始于对searchServices()的调用。当服务搜索进行时,蓝牙发现代理会在适当的时候回调servicesDiscovered()和 serviceSearchCompleted()方法。
  
  除了DiscoveryAgent和DiscoveryListener了,你在服务发现过程中还要使用到的类有UUID,ServiceRecord以及DataElement等。
  
  UUID类在蓝牙中,每个服务和服务属性都唯一地由"全球唯一标识符"(UUID)来校验。正如它的名字所暗示的,每一个这样的标识符都要在时空上保证唯一。UUID类可表现为短整形(16或32位)和长整形(128位)UUID。
  
  他提供了分别利用String和16位或32位数值来创建类的构造函数,提供了一个可以比较两个UUID(如果两个都是128位)的方法,还有一个可以转换一个UUID为一个字符串的方法。UUID实例是不可改变的(immutable),只有被UUID标示的服务可以被发现。
  
  在Linux下你用一个命令uuidgen -t可以生成一个UUID值;在Windows下则执行命令uuidgen 。UUID看起来就像如下的这个形式:2d266186-01fb-47c2-8d9f-10b8ec891363。当使用生成的UUID去创建一个UUID对象,你可以去掉连字符。
  
  SDDB和ServiceRecord接口
  
  在服务发现的中心是服务发现数据库(SDDB)和服务发现协议(SDP)。SDDB由蓝牙实现负责维护的数据库。它包含了服务记录(service records),后者代表了对客户端有效的服务。SDP对于基于JABWT应用程序来说是透明的;可以这么说,SDP是用于服务发现的。为重新获取服务纪录,一个本地设备SDP客户端会向一个远端设备上SDP服务器发出请求。
   
  图 8: SDDB
  
  每一笔服务记录都会由一个ServiceRecord的实例来表现。这个记录包含了描述服务细节的属性。这个类提供了几种有用的方法:
  
  .getAttributeIDs() 和 getAttributeValue()方法返回服务记录的属性。
  
  .getConnectionURL()方法获取链接的URL

  数据元素类
  
  一个服务可以有许多的属性,一些是强制性的,其他的是可选的。一个服务属性由一个数据元素对象来表现,这个数据元素对象提供了设置并取得属性值的方法。
  
  强制性属性是在注册一个服务之后被自动设定的。这些属性包括:
  
  ServiceRecordHandle,ServiceClassIDList,ServiceRecordState,ServiceID,还有ProtocolDescriptorList。
  
  如果你想要的话,还可以设置可选属性。可选属性有很多,但是有三个值得关注:ServiceName,ServiceDescription,和ProviderName。想得到更多的关于这些属性的信息,请参看JABWT的文档或蓝牙规范。
  
  设备管理API
  
  有3个主要的类来支持设备管理:
  
  .LocalDevice
  
  .RemoteDevice
  
  .DeviceClass
  
  本地设备类LocalDevice类标识了本地蓝牙设备。蓝牙应用程序和LocalDevice之间的关系是典型的一对一关系:
   
  图 10:本地设备类
  
  本地设备提供了方法来返回关于本地设备的信息,并且能够进入Bluetooth manager:
  
  .getBluetoothAddress()返回蓝牙设备地址。
  
  .getDeviceClass()返回设备类。
  
  .getFriendlyName()返回设备友好名称,蓝牙设备名通常是用户在蓝牙控制中心为其设置的我们将会在后面看到。
  
  .getRecord()返回一个指定蓝牙连接的服务记录。
  
  .updateRecord()方法用来为指定的ServiceRecord更新SDDB服务记录。
  
  .getDiscoverable()返回设备的可发现状态。
  
  .setDiscoverable()设置设备的可发现状态。
  
  .getDiscoveryAgent()返回一个参考给发现代理。
  
  .getProperty()返回一个设备的蓝牙属性。
  
  通过调用getProperty()方法你可以得到的属性包括:
  
  .bluetooth.api.version,蓝牙API版本
  
  .bluetooth.sd.attr.retrievable.max,一次性能够被获得的服务记录属性的最大值
  
  .bluetooth.connected.devices.max,支持的连接设备的最大值
  
  .bluetooth.sd.trans.max,同时发生的服务发现处理的最大值
  
  .bluetooth.l2cap.receiveMTU.max,L2CAP最大发射单元
  
  你可以在Javadoc文档中或是规范中学习更多的有关蓝牙属性的内容。
  
  远端设备类
  
  一个RemoteDevice的实例代表了一个远端蓝牙设备。在一个蓝牙客户端应用程序可以进行服务,消费之前,它必须发送一个设备请求来发现远端设备。典型的蓝牙应用程序和远端设备之间的关系是一对多:
   
  图 11: 远端设备类
  
  远端设备(RemoteDevice)提供的方法中,有些很类似于本地设备(LocalDevice)里提供的方法:
  
  .getBluetoothAddress()返回蓝牙地址。
  
  .getFriendlyName()返回蓝牙设备名。
  
  .getRemoteDevice()返回相应的被指定蓝牙连接的远端设备。
  
  .authenticate()尝试识别验证远端设备。
  
  .authorize()为指定的蓝牙连接去尝试批准远端设备访问本地设备。
  
  .encrypt()尝试为指定的蓝牙连接开启或关闭加密。
  
  .isAuthenticated()测试是否远端设备可以被验证。
  
  .isAuthorized()测试是否远端设备已经被蓝牙控制中心授权访问本地设备以进行蓝牙连接。
  
  .isEncrypted()测试是否本地设备和远端设备之间的通信被加密。
  
  .isTrustedDevice()测试是否远端设备被蓝牙控制中心指定为可信任的。
  
  DeviceClass类一个DeviceClass对象代表一个设备的设备类(CoD),例如一个打印机或者一部电话。CoD包括一个主类,一个辅的类,和服务类型或服务类。DeviceClass提供了如下方法:
  
  .getMajorDeviceClass()方法获取设备的主类。
  
  .getMinorDeviceClass()方法获取设备的辅类。
  
  .getServiceClasses()获取设备的服务类。
  
  当一个设备被发现,同时他的类也会被发现;当发现代理调用deviceDiscovered()时,其中一个参数就是DeviceClass。你可以通过它的getDeviceClass()方法找到本地设备的CoD。
  
  蓝牙通信
  
  JABWT连接是基于逻辑链路及适配层协议的。L2CAP是一个低级协议用来管理数据包,直到达到64k。L2CAP中的处理细节像消息分割和重组(SAR),多路连接。另外,串口Profile(SPP)提供RFCOMM,一个通过L2CAP层的串行模拟协议。
  
  L2CAP和RFCOMM连接都是基于通用连接框架(GCF)的,直接通向接口和类的层次去创建连接和执行IO命令。JABWT依靠L2CAP和RFCOMM协议扩展了通用连接框架(GCF)以分别支持通过L2CAPConnection 和StreamConnection类型连接。
  
  就在L2CAPConnection被JSR 82介绍的同时,StreamConnection在原始的javax.microedition.io的GCF中被定义了,它是依靠CLDC发展来的。注意,JABWT中L2CAPConnection仅支持面向连接的L2CAP连接。图12显示了以基于GCF形式的各接口通过蓝牙网络进行通信的过程:
   
  图 12: 通用连接框架和蓝牙连接类型
  
  层次定义了L2CAP和Stream的连接和连接通知器。一个连接定义了一个连接终端,当一个连接通知器执行了服务行为时,它会等待并接受L2CAP连接处理。
  
  处理L2CAP连接比处理流连接更复杂。使用L2CAP时,开发者们必须处理好最大消息的大小(即最大传输单位,或叫做MTU)、打断和重组等。这些复杂的事项在开发者使用流连接的时候被隐藏了,以使他们处理蓝牙连通时获得更好的效率。
  
  如同所有的GCF连接类型,你可以通过调用GCF连接工厂方法javax.microedition.io.Connector创建蓝牙连接。传递给Connector()方法的连接URL决定了要创建的连接类型:一个L2CAPConnection连接的URL格式:
  
  btspp://hostname:[CN | UUID];
  parameters
  
  一个RFCOMMStreamConnection连接的URL格式:
  
  btspp://hostname:[CN | UUID];
  parameters
  
  细节介绍:
  
  .btl2cap 是为L2CAPConnection设计的URL配置方案。
  
  .btspp是为RFCOMM StreamConnection的URL配置方案。
  
  .hostname 既可是localhost用于架设一个服务器连接,也可是一个用于创建客户端连接的蓝牙地址。
  
  .PSM是协议/服务多路复用值,在一个客户端连接服务器端时使用。在概念上是模拟一个TCP/IP端口。
  
  .CN是信道数值,在一个客户端连接服务器端时使用,也是模拟TCP/IP端口。
  
  .UUID是UUID(通用唯一标识符)值,在一个服务器上建立服务时使用。
  
  .parameters(参数)包括描述服务名称的名字和有于安全的参数:验证、授权和加密。服务器连接和客户端连接
  
  在连接的URL中,主机名称告诉连接工厂是否它应该创建一个客户端异或服务器端。如果使用单词localhost作为主机名将定义一个服务器连接。客户端想要连接到一个指定的的服务可以通过调用ServiceRecord.getConnectionURL()来找到该服务连接的URL。
  
  异常
  
  javax.bluetooth核心API定义了三个异常类:
  
  .当一个蓝牙L2CAP、RFCOMM或是OBEX-over-RFCOMM连接不能被成功建立会抛出BluetoothConnectionException异常。
  
  .一个试图在错误状态下进行蓝牙操作时,会抛出BluetoothStateException异常。
  
  .当在本地服务发现数据库(SDDB)中,添加或改变服务记录失败时,会抛出ServiceRegistrationException异常。