首页 > 其他分享 >iOS 蓝牙开发详解(基本知识、相关类图、交互流程)

iOS 蓝牙开发详解(基本知识、相关类图、交互流程)

时间:2023-01-18 01:33:26浏览次数:62  
标签:peripheral 基本知识 iOS 类图 param error CBPeripheral options 外设

http://t.zoukankan.com/lijianyi-p-14765794.html

 

本文从以下三方面讲解下蓝牙开发

1、蓝牙相关基本知识

2、蓝牙相关类图

3、蓝牙交互流程

一、蓝牙相关基本知识

涉及到蓝牙开发,首先有几个问题是需要我们理解的

1、任何设备既可以是中心设备、也可以是外围设备

2、外设 和 中心设备 之间通过特征建立一个双向的数据通道

3、CBCentralManager主要操作中心设备,处理链接上外设之前的操作,链接上外设后,主要靠CBPeripheral(主要操作外设)处理外设相关操作(服务、特征、数据读写)

4、中心设备管理 CBCentralManager

中心控制类,主要管理中心设备,以及处理跟外设(外围设备)相关操作,主要是扫描、链接、断开外设。

操作中心设备的核心类。

很重要的协议CBCentralManagerDelegate,包含中心设备状态(是否打开蓝牙)回调、发现外设回调、链接外设成功回调、链接外设失败回调、外设链接断开回调等方法。

一个中心设备可以链接多个外围设备。

5、外围设备 CBPeripheral

外设类,包含设备的基础属性,名字,uuid等信息。向外设写入数据。

当中心设备连接到外设后,需要通过外设对象的代理方法进行数据交互。

操作外围设备的核心类。

很重要的协议CBPeripheralDelegate,包含发现服务回调、发现特征回调、特征的通知设置改变回调、特征更新回调、特征已写入数据回调等方法。

一个设备包含多个服务、一个服务包含多个特征、一个特征又包含多个描述。

6、外围设备管理 CBPeripheralManager

设备的控制,主要可以为设备设置Service以及Characteristic,可以手动配置特定的服务和特征值,也可看作可以自定义蓝牙协议,例如将手机作为外设时可以为自己的手机蓝牙设置服务和特征值。CBCentralManager更适合将自己的软件作为中心。

用的较少


7、服务 CBService

服务对象是用来管理外设提供的一些数据服务的。

一个服务可以包含多个特征

8、特征 CBCharacteristic

通过绑定服务中的特征值来进行数据的读写操作。

特征就是具体键值对,提供数据的地方。

每个特征属性分为这么几种:读,写,通知等几种方式。

有时读、写、通知可以是同一个特征,也可以读、写、通知各用一个特征表示。

一个特征可以包含多个描述。

一般我们操作到特征这一层

9、描述 CBDescriptor

每个characteristic可以对应一个或多个Description 供用户描述characteristic的信息或属性。

10、CBAttribute

 CBService,CBCharacteristic,CBDescriptor 类都继承自 CBAttribute,

它们有一个共同的属性 CBUUID,用来作为唯一的标识。

二、蓝牙相关类图

下面用类图简述下:

一个中心设备可以连接多个外设,一个外设包含多个服务,一个服务包含多个特征,一个特征包含多个描述

服务、特征、描述都用CBUUID唯一标识

  

三、蓝牙交互流程

下面简述下以手机作为中心设备、其它作为外围设备的交互流程,大致流程如图所示

下面是操作详解

1、创建中心设备管理对象(初始化中心设备)

CBCentralManager 的创建是异步的,如果初始化完成之后没有被当前创建它的类所持有,就会在下一次 RunLoop 迭代的时候释放。

创建线程写nil,为主线程。

初始化成功后,就会触发 CBCentralManagerDelegate 中的中心设备状态更新方法:centralManagerDidUpdateState:

NSDictionary *options = @{CBCentralManagerOptionShowPowerAlertKey:@NO};
        self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:options];
/*!
 *  @method initWithDelegate:queue:options:
 *
 *  @param delegate The delegate that will receive central role events.
 *  @param queue    The dispatch queue on which the events will be dispatched.
 *  @param options  An optional dictionary specifying options for the manager.
 *
 *  @discussion     The initialization call. The events of the central role will be dispatched on the provided queue.
 *                  If <i>nil</i>, the main queue will be used.
 *
 *    @seealso        CBCentralManagerOptionShowPowerAlertKey
 *    @seealso        CBCentralManagerOptionRestoreIdentifierKey
 *
 */
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
                           queue:(nullable dispatch_queue_t)queue
                         options:(nullable NSDictionary<NSString *, id> *)options NS_AVAILABLE(10_9, 7_0) NS_DESIGNATED_INITIALIZER;
@protocol CBCentralManagerDelegate <NSObject>

@required

/*!
 *  @method centralManagerDidUpdateState:
 *
 *  @param central  The central manager whose state has changed.
 *
 *  @discussion     Invoked whenever the central manager's state has been updated. Commands should only be issued when the state is
 *                  <code>CBCentralManagerStatePoweredOn</code>. A state below <code>CBCentralManagerStatePoweredOn</code>
 *                  implies that scanning has stopped and any connected peripherals have been disconnected. If the state moves below
 *                  <code>CBCentralManagerStatePoweredOff</code>, all <code>CBPeripheral</code> objects obtained from this central
 *                  manager become invalid and must be retrieved or discovered again.
 *
 *  @see            state
 *
 */
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;

2、扫描外围设备

在CBCentralManagerDelegate 中的中心设备状态更新方法:centralManagerDidUpdateState:中,

当中心设备处于CBManagerStatePoweredOn 状态的时候开始扫描周边设备(可以使用指定的 UUID 发现特定的 Service,也可以传入 nil,表示发现所有周边的蓝牙设备,不过还是建议只发现自己需要服务的设备)。

扫描外设:scanForPeripheralsWithServices:options:

该操作由CBCentralManager对象通过scanForPeripheralsWithServices:options:方法实现

case CBManagerStatePoweredOn:
                //蓝牙正常开启
                [self startScan];
                break;
- (void)startScan {
    
//    [self.centralManager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
//    扫描所有设备 当指定设备不好使时可以使用该方法
//    [self.centralManager scanForPeripheralsWithServices:nil options:nil];
//    扫描指定设备 快速
    [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:self.peripheralServiceUUID]] options:nil];
    
    [self startTime];
}
/*!
 *  @method scanForPeripheralsWithServices:options:
 *
 *  @param serviceUUIDs A list of <code>CBUUID</code> objects representing the service(s) to scan for.
 *  @param options      An optional dictionary specifying options for the scan.
 *
 *  @discussion         Starts scanning for peripherals that are advertising any of the services listed in <i>serviceUUIDs</i>. Although strongly discouraged,
 *                      if <i>serviceUUIDs</i> is <i>nil</i> all discovered peripherals will be returned. If the central is already scanning with different
 *                      <i>serviceUUIDs</i> or <i>options</i>, the provided parameters will replace them.
 *                      Applications that have specified the <code>bluetooth-central</code> background mode are allowed to scan while backgrounded, with two
 *                      caveats: the scan must specify one or more service types in <i>serviceUUIDs</i>, and the <code>CBCentralManagerScanOptionAllowDuplicatesKey</code>
 *                      scan option will be ignored.
 *
 *  @see                centralManager:didDiscoverPeripheral:advertisementData:RSSI:
 *  @seealso            CBCentralManagerScanOptionAllowDuplicatesKey
 *    @seealso            CBCentralManagerScanOptionSolicitedServiceUUIDsKey
 *
 */
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;

3、发现外围设备

CBCentralManager对象执行扫描外设方法scanForPeripheralsWithServices:options:后

会触发 CBCentralManagerDelegate 中的方法:(发现外设)centralManager:didDiscoverPeripheral:advertisementData:RSSI:

如果在扫描时指定了明确的服务,那么此时该方法里的外设就是包含该服务的外设,

如果传入的是nil,那么此时该方法里的外设就是周边所有打开的蓝牙设备。

/*!
 *  @method centralManager:didDiscoverPeripheral:advertisementData:RSSI:
 *
 *  @param central              The central manager providing this update.
 *  @param peripheral           A <code>CBPeripheral</code> object.
 *  @param advertisementData    A dictionary containing any advertisement and scan response data.
 *  @param RSSI                 The current RSSI of <i>peripheral</i>, in dBm. A value of <code>127</code> is reserved and indicates the RSSI
 *                                was not available.
 *
 *  @discussion                 This method is invoked while scanning, upon the discovery of <i>peripheral</i> by <i>central</i>. A discovered peripheral must
 *                              be retained in order to use it; otherwise, it is assumed to not be of interest and will be cleaned up by the central manager. For
 *                              a list of <i>advertisementData</i> keys, see {@link CBAdvertisementDataLocalNameKey} and other similar constants.
 *
 *  @seealso                    CBAdvertisementData.h
 *
 */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;

4、链接外设

在CBCentralManagerDelegate 中的发现外设方法:centralManager:didDiscoverPeripheral:advertisementData:RSSI:中,

我们会链接外设:connectPeripheral:options:

该操作由CBCentralManager对象通过connectPeripheral:options:方法实现

我们通过名称或者厂商数据来确定我们需要链接的外设(过滤外设),找到后停止扫描,然后链接该外设,即链接指定外设

[central connectPeripheral:peripheral options:nil];
/*!
 *  @method connectPeripheral:options:
 *
 *  @param peripheral   The <code>CBPeripheral</code> to be connected.
 *  @param options      An optional dictionary specifying connection behavior options.
 *
 *  @discussion         Initiates a connection to <i>peripheral</i>. Connection attempts never time out and, depending on the outcome, will result
 *                      in a call to either {@link centralManager:didConnectPeripheral:} or {@link centralManager:didFailToConnectPeripheral:error:}.
 *                      Pending attempts are cancelled automatically upon deallocation of <i>peripheral</i>, and explicitly via {@link cancelPeripheralConnection}.
 *
 *  @see                centralManager:didConnectPeripheral:
 *  @see                centralManager:didFailToConnectPeripheral:error:
 *  @seealso            CBConnectPeripheralOptionNotifyOnConnectionKey
 *  @seealso            CBConnectPeripheralOptionNotifyOnDisconnectionKey
 *  @seealso            CBConnectPeripheralOptionNotifyOnNotificationKey
 *  @seealso            CBConnectPeripheralOptionEnableTransportBridgingKey
 *    @seealso            CBConnectPeripheralOptionRequiresANCS
 *
 */
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options;

5、链接外设结果回调

CBCentralManager对象执行链接外设方法connectPeripheral:options:后

会触发 CBCentralManagerDelegate 中的方法:

链接外设成功回调:centralManager:didConnectPeripheral:

/*!
 *  @method centralManager:didConnectPeripheral:
 *
 *  @param central      The central manager providing this information.
 *  @param peripheral   The <code>CBPeripheral</code> that has connected.
 *
 *  @discussion         This method is invoked when a connection initiated by {@link connectPeripheral:options:} has succeeded.
 *
 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;

链接外设失败回调:centralManager:didFailToConnectPeripheral:error:

/*!
 *  @method centralManager:didFailToConnectPeripheral:error:
 *
 *  @param central      The central manager providing this information.
 *  @param peripheral   The <code>CBPeripheral</code> that has failed to connect.
 *  @param error        The cause of the failure.
 *
 *  @discussion         This method is invoked when a connection initiated by {@link connectPeripheral:options:} has failed to complete. As connection attempts do not
 *                      timeout, the failure of a connection is atypical and usually indicative of a transient issue.
 *
 */
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;

6、查找服务

在CBCentralManagerDelegate 中的链接外设成功回调方法:centralManager:didConnectPeripheral:中,

我们会查找服务:discoverServices:

该操作由CBPeripheral对象通过discoverServices:方法实现

[peripheral discoverServices:@[[CBUUID UUIDWithString:self.peripheralServiceUUID]]];
/*!
 *  @method discoverServices:
 *
 *  @param serviceUUIDs A list of <code>CBUUID</code> objects representing the service types to be discovered. If <i>nil</i>,
 *                        all services will be discovered.
 *
 *  @discussion            Discovers available service(s) on the peripheral.
 *
 *  @see                peripheral:didDiscoverServices:
 */
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;

7、发现服务

CBPeripheral对象执行查找服务方法discoverServices:后,

会触发 CBPeripheralDelegate 中的发现服务方法:peripheral:didDiscoverServices:

/*!
 *  @method peripheral:didDiscoverServices:
 *
 *  @param peripheral    The peripheral providing this information.
 *    @param error        If an error occurred, the cause of the failure.
 *
 *  @discussion            This method returns the result of a @link discoverServices: @/link call. If the service(s) were read successfully, they can be retrieved via
 *                        <i>peripheral</i>'s @link services @/link property.
 *
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;

8、查找特征

在 CBPeripheralDelegate 中的发现服务方法:peripheral:didDiscoverServices:中,

我们会查找特征:discoverCharacteristics:forService:

该操作由CBPeripheral对象通过discoverCharacteristics:forService:方法实现

[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:self.peripheralCharacteristicRTXUUID]] forService:service];
/*!
 *  @method discoverCharacteristics:forService:
 *
 *  @param characteristicUUIDs    A list of <code>CBUUID</code> objects representing the characteristic types to be discovered. If <i>nil</i>,
 *                                all characteristics of <i>service</i> will be discovered.
 *  @param service                A GATT service.
 *
 *  @discussion                    Discovers the specified characteristic(s) of <i>service</i>.
 *
 *  @see                        peripheral:didDiscoverCharacteristicsForService:error:
 */
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service;

9、发现特征

CBPeripheral对象执行查找特征方法discoverCharacteristics:forService:后,

会触发CBPeripheralDelegate 中的发现特征方法:peripheral:didDiscoverCharacteristicsForService:error:

/*!
 *  @method peripheral:didDiscoverCharacteristicsForService:error:
 *
 *  @param peripheral    The peripheral providing this information.
 *  @param service        The <code>CBService</code> object containing the characteristic(s).
 *    @param error        If an error occurred, the cause of the failure.
 *
 *  @discussion            This method returns the result of a @link discoverCharacteristics:forService: @/link call. If the characteristic(s) were read successfully, 
 *                        they can be retrieved via <i>service</i>'s <code>characteristics</code> property.
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;

10、写特征、通知特征(读取)、读特征

在 CBPeripheralDelegate 中的发现特征方法:peripheral:didDiscoverCharacteristicsForService:error:中,

我们会处理特征(读、写、通知),一般会保存写入特征,方便后期写入数据,打开使能通知,方便读取数据

写:

保存写特征,方便后期写入数据。

读:

通知特征和读特征都是为了读取,一般我们使用的都是通知,

使用通知的时候,要打开使能通知(订阅),

该操作由CBPeripheral对象通过setNotifyValue:forCharacteristic:方法打开指定通知特征

通知特征发送的数据在didUpdateValueForCharacteristic方法里接受(读取)

该操作会回调CBPeripheralDelegate 中的方法peripheral:didUpdateNotificationStateForCharacteristic:error:

通过characteristic.isNotifying知晓通知状态

/*!
 *  @method peripheral:didUpdateNotificationStateForCharacteristic:error:
 *
 *  @param peripheral        The peripheral providing this information.
 *  @param characteristic    A <code>CBCharacteristic</code> object.
 *    @param error            If an error occurred, the cause of the failure.
 *
 *  @discussion                This method returns the result of a @link setNotifyValue:forCharacteristic: @/link call. 
 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
for (CBCharacteristic *characteristic in service.characteristics)
        {
//            if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:self.peripheralCharacteristicTXUUID]]) {
//                //                打开使能通知 (订阅)该特征发送的数据在didUpdateValueForCharacteristic方法里接受(读取)
//                [peripheral setNotifyValue:YES forCharacteristic:characteristic];
//            } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:self.peripheralCharacteristicRXUUID]]) {
//                //                保存外设接受特征(写入特征)
//                self.characteristicRX = characteristic;

11、写入数据

写入方法:writeValue:forCharacteristic:type:

写入操作由CBPeripheral对象通过writeValue:forCharacteristic:type:方法写入指定写入特征

[self.peripheral writeValue:peripheralRXData forCharacteristic:self.characteristicRX type:
            CBCharacteristicWriteWithResponse];
/*!
 *  @method writeValue:forCharacteristic:type:
 *
 *  @param data                The value to write.
 *  @param characteristic    The characteristic whose characteristic value will be written.
 *  @param type                The type of write to be executed.
 *
 *  @discussion                Writes <i>value</i> to <i>characteristic</i>'s characteristic value.
 *                            If the <code>CBCharacteristicWriteWithResponse</code> type is specified, {@link peripheral:didWriteValueForCharacteristic:error:}
 *                            is called with the result of the write request.
 *                            If the <code>CBCharacteristicWriteWithoutResponse</code> type is specified, and canSendWriteWithoutResponse is false, the delivery
 *                             of the data is best-effort and may not be guaranteed.
 *
 *  @see                    peripheral:didWriteValueForCharacteristic:error:
 *  @see                    peripheralIsReadyToSendWriteWithoutResponse:
 *    @see                    canSendWriteWithoutResponse
 *    @see                    CBCharacteristicWriteType
 */
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;

写入数据有两种方式:

/*
32 typedef NS_ENUM(NSInteger, CBCharacteristicWriteType) {
33     CBCharacteristicWriteWithResponse = 0,//写数据并且接收成功与否回执
34     CBCharacteristicWriteWithoutResponse,//写数据不接收回执
35 };
36 */

如果写入类型为CBCharacteristicWriteWithResponse 回调CBPeripheralDelegate 中的方法:

peripheral:didWriteValueForCharacteristic:error:,

如果写入类型为CBCharacteristicWriteWithoutResponse不回调此方法,

该方法只是告知写入数据是否成功

/*!
 *  @method peripheral:didWriteValueForCharacteristic:error:
 *
 *  @param peripheral        The peripheral providing this information.
 *  @param characteristic    A <code>CBCharacteristic</code> object.
 *    @param error            If an error occurred, the cause of the failure.
 *
 *  @discussion                This method returns the result of a {@link writeValue:forCharacteristic:type:} call, when the <code>CBCharacteristicWriteWithResponse</code> type is used.
 */
 - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;

写入数据后外设响应数据在特征值更新方法didUpdateValueForCharacteristic:error:中读取

12、读取数据

读取外设发送给中心设备的数据,

无论是read的回调,还是notify(订阅)的回调都是CBPeripheralDelegate 中的方法:

特征值更新:didUpdateValueForCharacteristic:error:

/*!
 *  @method peripheral:didUpdateValueForCharacteristic:error:
 *
 *  @param peripheral        The peripheral providing this information.
 *  @param characteristic    A <code>CBCharacteristic</code> object.
 *    @param error            If an error occurred, the cause of the failure.
 *
 *  @discussion                This method is invoked after a @link readValueForCharacteristic: @/link call, or upon receipt of a notification/indication.
 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;

13、断开链接

断开链接:cancelPeripheralConnection:

断开链接操作由CBCentralManager对象通过cancelPeripheralConnection:方法实现

该方法不会触发CBCentralManagerDelegate 中的方法:

断开外设(仅在异常断开时会触发):centralManager:didDisconnectPeripheral:error:

/*!
 *  @method cancelPeripheralConnection:
 *
 *  @param peripheral   A <code>CBPeripheral</code>.
 *
 *  @discussion         Cancels an active or pending connection to <i>peripheral</i>. Note that this is non-blocking, and any <code>CBPeripheral</code>
 *                      commands that are still pending to <i>peripheral</i> may or may not complete.
 *
 *  @see                centralManager:didDisconnectPeripheral:error:
 *
 */
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;

标签:peripheral,基本知识,iOS,类图,param,error,CBPeripheral,options,外设
From: https://www.cnblogs.com/itlover2013/p/17059026.html

相关文章

  • 【技术指北】通过SMB协议在iOS和Windows之间传输文件
    windows操作新建文件夹设置文件夹的属性,选择共享设置高级共享,权限选择完全控制选择共享,选择Everyonecmd-ipcofig,获取ipv4地址iOS操作打开文件管理器选择连接......
  • iOS APP上架流程(详细)​
    声明:本文转自​​http://www.2cto.com/kf/201512/453943.html​​,侵删。​前言:作为一名iOS开发者,把开发出来的App上传到AppStore是必须的。下面就来详细介绍下具体流程。​......
  • 一步步了解iOS APP上架流程,让你的APP顺利进入App Store的大门
    ​随着AppleStore越来越成熟,以及越来越多的开发者和公司希望在该平台上投放自己的产品,iOSAPP上架成为许多开发者和公司普遍关注的话题。但是,由于苹果AppStore的审核政策......
  • iOS Swift圆角绘制与离屏渲染优化
    iOS里面使用圆角有可能造成离屏渲染,它需要开辟一个新的内存空间,做上下文切换(状态切换),并且渲染完成后还要进行拷贝操作,因此会造成一定的性能损耗,需要进行优化。1原理http......
  • IOS中Object-C按照NSDictionary中的某个Key排序的方法
    //create_time降序NSComparisonResultsort_desc(NSDictionary*firstDict,NSDictionary*secondDict,void*context){NSDateFormatter*dateFormatter=[[NSD......
  • axios_socketio_cros_test
    D:\code_gitee\html_crostest\htmo_crostest\index.html<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><linkrel="icon"type="image/sv......
  • 新建的nagios,本地RHEL6.0报HTTP/1.1--403错误
    新建的nagios,本地RHEL6.0报HTTP/1.1--403错误 HTTPWARNING:HTTP/1.1403Forbidden-4184bytesin0.001secondresponsetime是因为nagios不断地检查apahce的根目录,......
  • iostat
    iostat命令用途报告中央处理器(CPU)统计信息和整个系统、适配器、tty设备、磁盘和CD-ROM的输入/输出统计信息。 语法 iostat[-s][-a][-d|-t][-T][-m][PhysicalVolume.......
  • axios 进行同步请求(async+await+promise)
    axios进行同步请求(async+await+promise)遇到的问题介绍将axios的异步请求改为同步请求想到了async 和await、Promiseaxios介绍Axios 是一个基于 promise 的HT......
  • ios网络协议从http变成https
    最近发了一个很蛋疼的事,iphone16.x以后的系统浏览器自动将http请求切换为https请求了工程自测1.在ihone14promax,iOS16.1的手机上用http请求是失败的,在iPhonese2,iOS16.......