此文档为开发视界翻译转载者请注明出处(开发视界 www.sf.org.cn)否则追究法律责任 五、搜索和发布 蓝牙应用程序要实现共能,需要将自己的服务对外发布,这样其他的设备才能够找到他。 1、发布一个服务 下面的段落将讲述一个客户端应用程序如何将自身的蓝牙服务对外发布。文件中涉及的代码来自Chat example [4], 这个例子可以在S69第三版的SDK中找到。在S60第二版中也有很多有用的例子,比如蓝牙服务发布的案例。 1)SDP数据库 蓝牙的所有服务都可以用服务记录来表示。在第二章中提到,Symbian开发接口并没有提供一个单独的服务记录类。SDP数据库可以直接控制对服务记录和属性的访问。 Symbian设备的SDP数据库负责对外发布设备所能提供的所有服务。这就意味着这个数据库可以被一个以上以用程序访问。要完成整个请求,Symbian开发接口还需要将SDP数据库作为主机运行。需要注意的是,应用程序在访问服务器所提供的服务以前,要保证和服务器连接并打开一个线程。 RSdp表示SDP数据库服务器,允许一个应用程序与其连接(参考btsdp.h)。RSdpDatabase 表示服务器上的一格线程,而其后者是这两个类中比较重要的一个,因为他提供了一个客户端应用程序需要请求的大部分功能。 CChatBtServiceAdvertiser:: ConnectL()说明了如何连接一个数据库。调用CSdpSession::Connect 将于服务器建立和打开一个session,然后将打开蓝牙数据库。 void CChatBtServiceAdvertiser::ConnectL() { if ( !iIsConnected ) { User::LeaveIfError( iSdpSession.Connect() ); User::LeaveIfError( iSdpDatabase.Open( iSdpSession ) ); iIsConnected = ETrue; } } 以上介绍的两种Session和数据库在应用程序终止前,必须关闭。简单调用Close函数将关闭Session和数据库。
2)创建服务记录 前面已经讲过,在Symbian系统中并没有明确的服务记录结构。因此,SDP数据库来管理服务句柄集合和他们的相关属性从而创建一个服务记录。 以下的代码来自于CChatBtServiceAdvertiser::StartAdvertisingL( TInt aPort )解释了如何在数据库中创建一个服务记录。iRecord (TSdpServRecordHandle)作为一个参数传递,其包含了新建的服务记录的服务句柄。 iSdpDatabase.CreateServiceRecordL( ServiceClass(), iRecord ); 从前面的介绍中可以看到,CreateServiceRecordL函数提供了两种重载的版本。这两个版本都可以创建数据库。但是第一种版本只能给一个ID分配一种服务类,而第二种重载版本则可以表现出很多服务记录ID的事例。没一个ID都代表了一个基本服务类的一个特殊实例,而且允许服务类型搜索。比如,一个彩色打印机可以对外发布其服务为打印,但是更具体的,一个彩色打印机。这两种服务类的UUID都可以在提交给第二种重载函数中体现。
3)服务记录发布 尽管服务记录存在于SDP数据库库中,但是并没有分配任何相关的属性。发布服务记录以前需要对使用的数据提前声明。 添加到服务记录的第一种元素是一个协议列表。创建这个协议列表将使用前面介绍过的CAttrValueDES。
4)CSdpAttrValueDES 和 CSdpAttrValueDES 图表3说明了CSdpAttrValueDES和CSdpAttrValueDES的结构和数据元素类型有稍微的不同。
 图表3:CSdpAttrValueDES和CSdpAttrValueDEA的结构 以下代码是CSdpAttrValueDES类能提供的整个开发接口: class CSdpAttrValueDES : public CSdpAttrValueList { public: IMPORT_C static CSdpAttrValueDES* NewDESL(MSdpElementBuilder* aBuilder); virtual TSdpElementType Type() const; IMPORT_C virtual void AcceptVisitorl(MSdpAttributeValueVisitor& aVisitor); IMPORT_C virtual TUint DataSize() const; IMPORT_C void AppendValueL(CSdpAttrValue* aValue); IMPORT_C virtual MSdpElementBuilder* BuildUnknownL(TUint8 aType, TUint8 aSizeDesc, const TDesC8& aData); IMPORT_C virtual MSdpElementBuilder* BuildNilL(); IMPORT_C virtual MSdpElementBuilder* BuildUintL(const TDesC8& aUint); IMPORT_C virtual MSdpElementBuilder* BuildUUIDL(const TUUID& aUUID); IMPORT_C virtual MSdpElementBuilder* BuildBooleanl(TBool aBool); IMPORT_C virtual MSdpElementBuilder* BuildStringL(const TDesC8& aString); IMPORT_C virtual MSdpElementBuilder* BuildDESL(); IMPORT_C virtual MSdpElementBuilder* BuildDEAL(); IMPORT_C virtual MSdpElementBuilder* StartListL(); IMPORT_C virtual MSdpElementBuilder* EndListL(); IMPORT_C virtual MSdpElementBuilder* BuildURLL(const TDesC8& aString); IMPORT_C virtual MSdpElementBuilder* BuildEncodedL(const TDesC8& aString); virtual TSdpElementType Type() const=0; virtual TUint DataSize() const=0; virtual TUint Uint() const; virtual TInt Int() const; virtual TBool DoesIntFit() const; virtual TInt Bool() const; virtual const TUUID &UUID() const; virtual const TPtrC8 Des() const; … }; 创建一个数据元素流程需要使用StartListL, EndListL, 和 BuildxxxxL 函数。StartListL 函数打开和返回一个新的DES;BuildxxxxL 可以将创建的数据对外发布;成功发布以后,EndListL 函数将返回到上一个级别的列表中。 下面的代码描述了创建CSdpAttrValueDES的过程: CSdpAttrValueDES* vProtocolDescriptor = CSdpAttrValueDES::NewDESL( NULL ); CleanupStack::PushL( vProtocolDescriptor ); BuildProtocolDescriptionL( vProtocolDescriptor,aPort ); iSdpDatabase.UpdateAttributeL( iRecord, KSdpAttrIdProtocolDescriptorList, *vProtocolDescriptor ); CleanupStack::PopAndDestroy( vProtocolDescriptor ); 列表一旦建立,必须和属性同时发布。前面已经介绍过,需要使用StartL, EndListL和BuildxxxxL函数。 下面的代码描述了整个流程。这个例子实际是一个C++语言编写的。在例子中创建了一个父列表和两个子列表。第一个子列表包含一个UUID,被负值为KL2CAP;第二个子列表包含一个UUID负值为KRFCOMM和一个无符号8位整数,其值为KChannel。在本例子中,KChannel 被负值为“1”。TBuf8 用于在编码的时候明确需要使用得位数。 void CChatServiceAdvertiser::BuildProtocolDescriptionL( CSdpAttrValueDES* aProtocolDescriptor, TInt aPort ) { TBuf8<1> channel; channel.Append( ( TChar )aPort ); aProtocolDescriptor ->StartListL() ->BuildDESL() ->StartListL() // Details of lowest level protocol ->BuildUUIDL( KL2CAP ) ->EndListL() ->BuildDESL() ->StartListL() ->BuildUUIDL( KRFCOMM ) ->BuildUintL( channel ) ->EndListL() ->EndListL(); } KL2CAP 和 KRFCOMM是两个常量,他们只是蓝牙堆栈中的一部分。这些常量在头文件bt_sock.h中定义。 一个描述的列表一旦建立,必须和一个已存在的服务记录连接在一起。具体的操作可以从下面的代码中看出。 iSdpDatabase.UpdateAttributeL( iRecord, KSdpAttrIdProtocolDescriptorList, *vProtocolDescriptor ); CSdpDatabase已经介绍过了。KSdpAttrIdProtocolDescriptorList是一个常量,用来表示每个服务记录都具有的通用属性的值。该常量在btsdp.h中定义。 UpdateAttributeL具有下列结构: UpdateAttributeL( <Service Record Handle to update>, <Attribute ID of the Service Record to update>, <value to assign> ); UpdateAttributeL从来都不被赋予一个值。但是,他经常被用于在更新一个服务纪录的过程中销毁一个数据元素。UpdateAttributeL 的重载函数接受无符号整数或者长和短整数。服务纪录的初始化说明了重载函数的使用: // Add a name to the record iSdpDatabase.UpdateAttributeL( iRecord, KSdpAttrIdBasePrimaryLanguage + KSdpAttrIdOffsetServiceName, ServiceName() ); // Add a description to the record iSdpDatabase.UpdateAttributeL( iRecord, KSdpAttrIdBasePrimaryLanguage + KSdpAttrIdOffsetServiceDescription, ServiceDescription() ); 另外,所有的服务记录属性的ID都定义与Bluetooth Specification在头文件btsdp.h中声明! 需要注意,蓝牙服务的可见性可以进行设置。可以对其进行设置只适用于特殊的服务ID.也可以通过把它添加到Public Browse Group列表将其设置为公共,这样远程设备在浏览服务的时候都将发现这个服务。通常私有服务都是隐藏的,但是与其对应的连接可以发现这个服务,因为其知道服务ID。Public Browse Group UUID 定义于btsdp.h头文件中。将一个服务加入到Public Browse Group 列表中,需要使用以下代码: TUUID browseUUID(KPublicBrowseGroupUUID) CSdpAttrValueUUID* browseGroupAttr = CSdpAttrValueUUID::NewUUIDL(browseUUID); iSdpDatabase.UpdateAttributeL(iRecord, KSdpAttrIdBrowseGroupList, *browseGroupAttr); 5)删除服务广播 一个服务记录一旦添加到数据库,其他应用程序将搜索到该服务。如果要停止广播该服务,客户端应用程序必须从数据库中删除服务记录。这个操作比较简单,如下所示: iSdpDatabase.DeleteRecordL(iRecord); 6)停止SDP数据库 当一个客户端应用程序结束了蓝牙服务的时候,其应该与SDP数据库断开连接。这通过客户端应用程序调用Close函数实现: iSdpDatabase.Close(); iSdpSession.Close(); Symbian操作系统对连接到服务器的所有线程都进行跟踪,如果他们不是什么重要的系统服务。如果一个服务没有线程对其访问,系统将关闭该服务,这样可以节省操作系统资源。这也意味着,如果没有新的线程访问SDP数据库,那么他也将关闭,尽管可能还有其他蓝牙设备对其访问。因此,应用程序在向外发布服务的阶段,必须保证一个线程对数据库进行访问。应用程序开发者还必须保证,无效的SDP纪录不能发布服务。 2、改变搜索模式 如果客户端设置为限制搜索模式,那么服务器端也必须设置成限制搜索模式。 从Symbian操作系统8.0a版本开始,蓝牙通过Publish开发接口和Subscribe开发接口的一个常数KpropertyKeyBluetoothSetLimitedDiscoverableStatus(从S60第三版开始)和KPropertyKeyBluetoothLimitedDiscoverable (S60第二版)。当前的搜索模式可以用关键字KpropertyKeyBluetoothGetLimitedDiscoverableStatus检测。关于Publish和Subscribe开发接口的使用将在8,2中详细讲解。 在使用蓝牙API第一版的旧平台中,搜索是通过HCI命令static const TUint KHCIWriteDiscoverabilityIoctl.设置的。 void Ioctl(TUint KHCIWriteDiscoverabilityIoctl,TRequestStatus& aStatus,TDes8* aDesc=NULl,TUint aLevel=KSolBtHCI); 当前的搜索模式可以通过static const TUint KHCIReadDiscoverabilityIoctl 命令来检测。该命令没有参数。 3、蓝牙的注册 蓝牙的这册信息中包含了很多设备搜索信息和链接信息。蓝牙设备注册开发接口在蓝牙API第二版的时候做了很大的修改,删除了BTREGISTRY.DLL,提供了新的子线程完成相同的工作。新的开发接口取消了以前在开发过程中,尤其是UI任务的开发中反复的内容。这个修改是现有版本中比以前对端口直接操作更优秀的地方。 大部分的蓝牙注册操作都是异步的,而且大部分的运行程序在使用动态对象查询或者更新的时候都觉得很方便。 RBTRegServ类(注册访问线程,包含RBTMan线程),RBTRegistry(在与远程服务器注册的时候打开一个子线程),TBTRegistrySearch(对蓝牙注册的搜索设置标准),CBTRegistryResponse(一个辅助类,将蓝牙的注册结果对外公布),以上类都定义与头文件btmanclient.h中,提供了注册操作所需要的大部分方法。 1)增加一个设备 把一个设备添加到注册表并非像一般的UI操作一样运行,因为设备的添加需要配对。下面的代码,描述了如何使用注册API。 所有的注册操作都需要一个线程注册服务器: RBTRegServ regServ; User::LeaveIfError(regServ.Connect()); and a subsession for the particular operation in question: RBTRegistry view; User::LeaveIfError(view.Open(regServ)); 这个时候就可以添加一个CBTDevice对象了(在与一个声明的操作同步显示;应该使用一个动态对象来代替): TRequestStatus stat; view.AddDeviceL(aDevice,stat); User::WaitForRequest(stat); User::LeaveIfError(stat.Int()); 子线程(如果不在查询),就可以关闭了: view.Close(); regServ.Close(); 2)创建一个视图
待续.... |