MTWristbandKit 说明文档

本套SDK仅支持Minew公司出品的蓝牙手环设备。通过SDK可以帮助开发者处理手机和手环之间的一切工作,包括:扫描设备,广播数据、连接设备,向设备写入数据,从设备接收数据等。

前期工作

整体框架:MinewWristbandManager为设备管理类,在APP运行时始终是单例。WristbandModule是设备实例类,此套件会为每一个设备生成一个实例,在扫描和连接后都会使用,内部包含设备广播数据,该数据会随着设备不停广播而更新

MinewWristbandManager:设备管理类,可以扫描周围的手环设备,并且可以连接它们,校验它们等。

WristbandModule:扫描时获取到的设备实例,对应手环设备,在扫描和连接后都会使用

StaticInfoFrame:静态信息帧

ConfigFrame:配置帧

CombinedFrame:组合帧

TemperatureHistory:温度历史数据实体类

WristbandHistory:接触历史数据实体类

手环并不是会一直在广播状态。内部置有加速度传感器,有移动才会广播一段时间,期间可以通过sdk去扫描设备。需要注意的是,手环在广播阶段不能立即连接,需要判断是已经唤醒后才能去连接。

导入到工程

  1. 开发环境

    sdk最低支持Android 5.0,对应API Level为21。在module的build.gradle中设置minSdkVersion为21或21以上

    android {
    
        defaultConfig {
            applicationId "com.xxx.xxx"
            minSdkVersion 21
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
  2. MinewWristbandKit.jar添加到module的libs文件夹下,并在该modulebuild.gradle中添加如下语句(直接添加依赖):

    implementation files('libs/MinewWristbandKit.jar')
    implementation 'org.lucee:bcprov-jdk15on:1.52.0'
    
    1
    2

    或者右键该jar文件,选择Add as Library,添加到当前module

  3. AndroidManifest.xml需要以下权限,如果targetSdkVersion大于23,则需要做权限管理以获取权限

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    
    1
    2
    3
    4

使用

sdk分为扫描、唤醒和连接三个阶段

扫描

开始扫描

Android6.0系统以上,进行BLE扫描,需要申请到定位权限后才可以进行,并且需要打开定位开关!

MinewWristbandManager manager = MinewWristbandManager.getInstance(getApplicationContext());
manager.startScan(new OnScanWristbandResultListener() {
    @Override
    public void onScanWristbandResult(ArrayList<WristbandModule> result) {
		//scan result
    }
});
1
2
3
4
5
6
7

sdk内部并没有对蓝牙扫描时长进行处理,但是扫描是耗电操作,一般90秒就可以停止扫描了,如果还需要继续扫描,可以提供刷新等操作以便继续扫描。

取出数据

在扫描期间,sdk获取到的广播数据存储在广播帧里,部分数据会在连接后需要用到。当前需要用到的帧类型是:StaticInfoFrameConfigFrame

StaticInfoFrame

名称 类型 描述
power int 电量百分比
firmwareVersion String 固件版本

ConfigFrame

名称 类型 描述
openStorage boolean 是否已经打开存储开关,false为关,否则已经打开
recordNum int 历史记录条数
versionCode int 固件版本号。
当扫描到ConfigFrame后,可能的值为2或3。
具体将在表格下方单独说明
hasTemperatureSensor boolean 手环是否带有温度传感器,true表示有温度传感器,否则没有
temperatureRecordNum int 温度告警记录条数,仅当versionCode为3且hasTemperatureSensor为true,该值才有效

说明:在ConfigFrame中,新增versionCode属性,表示固件版本号,当扫描到ConfigFrame后,可能的值为2或3。

  • 如果为2,表示当前手环仅有:关机、存储开关关闭或打开、固件升级功能;
  • 如果为3,进一步细分是否有温度传感器(根据hasTemperatureSensor属性来判断)
    • 无温度传感器:在为2的基础功能上,增加恢复出厂设置功能
    • 有温度传感器:拥有以上所有功能,再添加:告警距离读写、报警温度读写、测温间隔时间读写等功能

CombinedFrame

名称 类型 描述
temperature float 温度,当前温度,按下按键后会广播当前温度

要想取出内部数据,首先需要先获得对应的广播帧对象,在获取固件版本等。注意,由于是存在广播帧对象中,而在扫描时,不能保证立即就能接收到指定广播帧,所以需要做非空判断!

StaticInfoFrame staticInfoFrame = (StaticInfoFrame) getMinewFrame(ScanRecordFrameType.Static_Info_Frame);
if (staticInfoFrame != null) {
    //获取固件版本
    String version =  staticInfoFrame.getFirmwareVersion();
    //获取电量百分比
    int power = staticInfoFrame.getPower();
} 

ConfigFrame configFrame = (ConfigFrame) getMinewFrame(ScanRecordFrameType.Config_Frame);
if (configFrame != null) {
    //历史记录条数
    int recordNum =  configFrame.getRecordNum();
    //是否已经开启存储开关
    boolean isOpenStorage = configFrame.isOpenStorage();
	//温度告警历史记录条数
    int tempRecordNum = configFrame.getTemperatureRecordNum();
    //是否带有温度传感器
    boolean hasTemperatureSensor = configFrame.hasTemperatureSensor()();
    //版本号
    int versionCode = configFrame.getVersionCode();
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

sdk提供了更简洁的方式来获取设备信息,但是在未接收到广播帧时,获取到的值为"Unknown"或"-1"或“false”,具体是什么值,取决于它们的类型。

//获取固件版本
String version =  wristbandModule.getFirmwareVersion();
//获取电量百分比
int power = wristbandModule.getPower();
//获取历史记录条数
int recordNum =  wristbandModule.getTotalRecord();
//是否已经开启存储开关
boolean isOpenStorage = wristbandModule.isOpenStorage();
//获取温度告警历史记录条数
int tempRecordNum = wristbandModule.getTotalTemperatureRecord();
//获取设备的实时温度
float temperature = wristbandModule.getTemperature();
//手环是否带有温度传感器
boolean hasTemperaturSensor = wristbandModule.hasTemperatureSensor();
//设备版本号。这个值的作用请看扫描——取出数据——ConfigFrame部分
int versionCode = wristbandModule.getFirmwareVersionCode();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

唤醒

设备在扫描到手环设备后,需要去唤醒后才能去连接。唤醒后,一分钟以内,可以去连接;过了一分钟,需要重新唤醒。可通过WristbandModule.isAwakened()来判断指定手环是否已经被唤醒。

//当前手机系统是否支持唤醒手环,如果为false,就不去唤醒
boolean isSupport = manager.isSupportAdvertisement();
//activateState为true表示手环已唤醒,否则为未唤醒
boolean activateState = wristbandModule.isAwakened();
if (isSupport && !activateState) {
    //手机支持唤醒手环,且当前手环未被唤醒,就去唤醒手环
    manager.startAwaken(macAddress);
}
1
2
3
4
5
6
7
8

需要注意:

  1. 在被唤醒前,不能停止扫描。否则将无法获取到唤醒状态!
  2. 在激活-未激活状态转变。唤醒后如果1分钟以内没有去连接导致变为未激活状态,那么只能通过重新启动扫描。

SDK通过startAwaken(String macAddress)来唤醒指定设备,现在增加主动停止唤醒设备的机制,这与以前不同,以前是直到唤醒成功后才会自动停止,现在是在开始唤醒后,设置唤醒时长,在唤醒时长过后就会自动停止唤醒,且停止唤醒不会发出通知。

SDK增加了设置唤醒时长的方法:setAwakenTime(long timeMillis),默认是10秒,可设置范围是5秒到15秒。由于SDK缺少在唤醒时长范围内主动停止唤醒的功能,所以APP需要自己在发现设备已经被唤醒的情况下停止唤醒,即stopAwaken()

连接

连接前一般需要先停止扫描

if (!device.isAwakened()) {
    //设备未唤醒不可连接
    Toast.makeText(this,"not_activate",Toast.LENGTH_SHORT).show();
    return
}
//停止扫描
manager.stopScan();
//连接
manager.connect(context, module);
//断开连接
manager.disConnect(macAddress);
1
2
3
4
5
6
7
8
9
10
11

sdk中会对连接过程和设备断开连接等方式会有状态监听

manager.setOnConnStateListener(new OnConnStateListener() {
    
    /*
     * 连接过程中的状态回调
     *
     * @param macAddress      设备mac
     * @param connectionState 状态
     */
    @Override
    public void onUpdateConnState(String address, ConnectionState connectionState) {
        switch (connectionState) {
            case Disconnect:
				//连接失败或者设备断开连接会回调,主动断开不会回调该状态
                break;
            case Power_Off:
				//关机,设备发送关机指令,成功后会回调该状态,不会回调Disconnect状态
                break;
            case Inactivated:
				//未被唤醒状态。如上文所示,如果未检测设备已经唤醒就去连接,会回调该状态
                break;
            case Verify_Password:
				//密码验证,回调该状态后,设备需要写入密码,通过sendPassword写入
                manager.sendPassword(address, "minew123")
                break;
            case PasswordError:
				//密码错误,设备会断开连接,并且不会回调Disconnect状态
                break;
            
            case Firmware_Upgrade_Successfully:
				//固件升级成功,不会回调Disconnect状态
                break;

            case Connect_Complete:
				//连接完成,此时设备可进行读写操作
                break;
                
            case Reset_Device:
                //恢复出厂设置,成功后会回调该状态,不会回调Disconnect状态
                break;
            default:
                break;
        }
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

在连接过程中,sdk会返回多个连接状态到app中,app需要做好处理

  • ConnectionState.Inactivated: 如果在连接前后去判断手环是否已经激活,一般不会回调该状态。但是手环激活时间只有一分钟,一分钟后如果才连接,这时候sdk虽然返回是已经激活状态,但实际上激活状态不会再更新。需要重新开始扫描才会变化;
  • ConnectionState.Verify_Password: 手环连接需要验证密码,一旦回调该状态,那么就需要通过manager.sendPassword(address, "minew123")来写入密码。注意,密码长度为8位
  • ConnectionState.PasswordError: 密码错误,手环会断开连接,并且不会返回ConnectionState.Disconnect状态;
  • ConnectionState.Connect_Complete: 返回这个状态,说明手环已经连接成功,可以进行读写操作,比如读取历史数据等;
  • ConnectionState.Power_Off: 关机。通过关机指令可使手环关机,并回调该状态。另外,连接也会断开,且不会回调ConnectionState.Disconnect状态;
  • ConnectionState.Firmware_Upgrade_Successfully: 进行固件升级,sdk会给出另一个回调方法,并且升级成功后会回调该状态,连接会断开,且不会回调ConnectionState.Disconnect状态;
  • ConnectionState.Disconnect: 连接失败或者设备断开连接会回调,主动断开不会回调该状态。即调用manager.disConnect(macAddress);不会回调该状态。注意:升级失败是会回调该状态的;
  • ConnectionState.Reset_Device: 恢复出厂设置成功,并回调该状态。另外,连接也会断开,且不会回调ConnectionState.Disconnect状态。

API汇总

连接前

  1. startScan(): 开始扫描。每次开始扫描都会清除上一次扫描的所有结果

  2. stopScan(): 停止扫描

  3. setAwakenTime(long timeMillis): 设置唤醒时长,默认是10秒,在startAwaken(String macAddress)调用前设置才会生效

  4. getAwakenTime(): 获取此前设置的唤醒时长

  5. startAwaken(String macAddress): 开始唤醒设备

  6. stopAwaken(): 停止去唤醒

  7. isAwakened(): 是否在唤醒设备

  8. connect(Context context, WristbandModule module): 连接设备

  9. disConnect(String macAddress): 断开连接

  10. setOnConnStateListener(OnConnStateListener listener): 设置连接状态,这是必须调用的

  11. sendPassword(String macAddress, String password): 写入密码

    //macAddress为手环mac,password为要写入的密码
    manager.sendPassword(macAddress, password);
    
    1
    2

连接后

注意:连接后,在连接期间,对于同一设备,不管是读取还是写入,都需要等到上一操作完成,才能继续下一操作,否则程序会出现问题。 比如手环有20000条历史记录,读取需要几分钟时间,这时候又去关闭存储开关,就会导致程序出现问题,可能是数据错乱,或者更严重点程序闪退。

另外还请注意:读取历史记录,在通信过程中可能会丢包!如果对历史数据较为敏感,那么可以在读取到历史数据后对其条数进行判断,如果与预期结果条数不一致,可重新读取。

连接后可用方法如下:

  1. 读取历史数据;

    读取历史数据是通过索引来读取的,索引从0开始,如读取前七条,则传入0和6。需要注意的是每次读取不能超过7条。如果传入0和25,默认读取0到6,即读取前7条。

    //分别传入手环对象,开始索引和结束索引以及监听器
    manager.readHistoryData(module, 0, 6, 
                            new OnReadHistoryDataListener<WristbandHistory>() {
        /**
         * mac为设备mac
         * data为这次读取操作得到的历史数据
         */                        
        @Override
        public void receiverDataCompletely(String macAddress, 
                                           ArrayList<WristbandHistory> list) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  2. 读取全部历史数据;

    manager.readAllHistoryData(module, 
                               new OnReadAllHistoryDataListener<WristbandHistory>() {
        
        /**
         * mac为设备mac
         * process为读取全部历史数据的进度
         */  
        @Override
        public void receiverDataProgress(String macAddress, float process) {
    
        }
    
        /**
         * mac为设备mac
         * data为这次读取操作得到的历史数据
         */                             
        @Override
        public void receiverDataCompletely(String macAddress, 
                                           ArrayList<WristbandHistory> allData) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  3. 读取温度告警历史数据;

    读取温度告警历史数据是通过索引来读取的,索引从0开始,如读取前七条,则传入0和6。需要注意的是每次读取不能超过14条。如果传入0和25,默认读取0到13,即读取前14条。

    //分别传入手环对象,开始索引和结束索引以及监听器
    manager.readTemperatureHistory(module, 0, 6, 
                                   new OnReadHistoryDataListener<TemperatureHistory>() {
        /**
         * mac为设备mac
         * data为这次读取操作得到的历史数据
         */                                
        @Override
        public void receiverDataCompletely(String macAddress, 
                                           ArrayList<TemperatureHistory> allData) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  4. 读取全部温度告警历史数据;

    manager.readAllTemperatureHistory(
        module, 
        new OnReadAllHistoryDataListener<TemperatureHistory>() {
            @Override
            public void receiverDataProgress(String macAddress, float process) {
    
            }
    
            @Override
            public void receiverDataCompletely(String macAddress, 
                                               ArrayList<TemperatureHistory> allData) {
    
            }
        });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  5. 存储开关设置;

    /**
     * 存储开关设置
     *
     * @param macAddress 设备mac
     * @param isOpen     是否要打开
     * @param listener   监听器
     */
    manager.storageSwitch(mac, true, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean b) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  6. 关机;

    /**
     * 关机
     *
     * @param macAddress 设备mac
     */
    manager.powerOff(mac, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean b) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  7. 固件升级;

    /**
     * 固件升级
     *
     * @param macAddress  设备mac
     * @param upgradeData 升级包数据
     * @param listener    监听器
     */
    manager.firmwareUpgrade(mac, upgradeData, 
                            new OnFirmwareUpgradeListener() {
        
        /**
         * 升级包数据写入进度
         */
        @Override
        public void updateProgress(int progress) {
    
        }
    
        /**
         * 升级成功会触发OnConnStateListener回调,返回
         * ConnectionState.Firmware_Upgrade_Successfully状态
         */
        @Override
        public void upgradeSuccess() {
    
        }
        
        /**
         * 升级失败。升级失败会断开连接,会触发OnConnStateListener回调,返回
         * ConnectionState.Disconnect状态
         */
        @Override
        public void upgradeFailed() {
    
     }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
  8. 恢复出厂设置。在 ConfigFrame.versionCode 为3的情况下可使用;

    manager.resetDevice(module, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  9. 读取温度测量间隔。在 ConfigFrame.versionCode 为3且WristbandModule.hasTemperatureSensor() 返回true的情况下可使用。读取到的测量间隔范围在0到7200秒,注意,单位是秒;

    manager.readTempeMeasureInterval(module, new OnReadValueListener<Integer>() {
        @Override
        public void onReadValue(Integer value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  10. 设置温度测量间隔。在 ConfigFrame.versionCode 为3且WristbandModule.hasTemperatureSensor() 返回true的情况下可使用。手环支持的测量间隔范围在0到7200秒,注意,单位是秒;

    manager.setTempMeasureInterval(module, 8, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean result) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  11. 读取温度告警值。在 ConfigFrame.versionCode 为3且WristbandModule.hasTemperatureSensor() 返回true的情况下可使用。读取到的值范围在设置范围应在 30.0℃ ~ 42.0℃之间;

    manager.readTempAlarmValue(module, new OnReadValueListener<Float>() {
        @Override
        public void onReadValue(Float value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  12. 设置温度告警值。在 ConfigFrame.versionCode 为3且WristbandModule.hasTemperatureSensor() 返回true的情况下可使用。写入的值范围在设置范围应在 30.0℃ ~ 42.0℃之间;

    manager.setTempAlarmValue(module, 20, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean result) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  13. 读取告警距离档位。在 WristbandModule.getFirmwareVersionCode() 返回3的情况下可使用。读取到的值为0到4;

    manager.readAlarmGear(module, new OnReadValueListener<Integer>() {
        @Override
        public void onReadValue(Integer value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  14. 写入告警距离档位。在 WristbandModule.getFirmwareVersionCode() 返回3的情况下可使用。写入的值为0到4。

    manager.setAlarmGear(module, 2, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean result) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  15. 读取温度报警振动值。写入的值范围在设置范围应在 30.0℃ ~ 42.0℃之间。需满足以下条件才可使用:

    • WristbandModule.getFirmwareVersionCode() 返回3
    • WristbandModule.hasTemperatureSensor() 返回true
    • 固件版本是3.2.5及以上。
    manager.readAlarmVibrationValue(module, new OnReadValueListener<Integer>() {
        @Override
        public void onReadValue(Integer value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  16. 设置温度报警振动值,写入的值范围在设置范围应在 30.0℃ ~ 42.0℃之间。需满足以下条件才可使用:

    • WristbandModule.getFirmwareVersionCode() 返回3
    • WristbandModule.hasTemperatureSensor() 返回true
    • 固件版本是3.2.5及以上。
    manager.setAlarmVibrationValue(module, 33F, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean result) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  17. 读取温度报警振动开关值,需满足以下条件才可使用:

    • WristbandModule.getFirmwareVersionCode() 返回3
    • WristbandModule.hasTemperatureSensor() 返回true
    • 固件版本是3.2.5及以上。
    manager.readVibrationSwitch(module, new OnReadValueListener<Boolean>() {
        @Override
        public void onReadValue(Boolean value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  18. 设置温度报警振动开关值。需满足以下条件才可使用:

    • WristbandModule.getFirmwareVersionCode() 返回3
    • WristbandModule.hasTemperatureSensor() 返回true
    • 固件版本是3.2.5及以上。
    manager.setVibrationSwitch(module, true, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean result) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  19. 读取近距离报警震动开关值。需满足以下条件才可使用:

    • WristbandModule.getFirmwareVersionCode() 返回2或3
    • 固件版本是3.2.5及以上。
    manager.readCrAlarmVibrationSwitch(module, new OnReadValueListener<Boolean>() {
        @Override
        public void onReadValue(Boolean value) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6
  20. 设置近距离报警震动开关值。需满足以下条件才可使用:

    • WristbandModule.getFirmwareVersionCode() 返回2或3
    • 固件版本是3.2.5及以上。
    manager.setCrAlarmVibrationSwitch(module, true, new OnChangeListener() {
        @Override
        public void onModifyResult(boolean result) {
    
        }
    });
    
    1
    2
    3
    4
    5
    6

附表

在这里对一些类的属性进行说明。

TemperatureHistory(温度历史数据实体类):

名称 类型 描述
macAddress String 当前设备mac
time long 时间戳,表示记录时间
temperature float 温度

WristbandHistory(接触历史数据实体类):

名称 类型 描述
macAddress String 设备mac
time long 时间戳,表示记录时间
rssi float 信号强度

更新历史

2020/07/06

  1. 增加sdk支持系统版本说明;
  2. 增加读取历史数据中,单次读取长度不能超过7,索引从0开始。

2020/08/11

  1. 修改sdk支持最低版本为21,唤醒手环增加isSupportAdvertisement()方法判断系统是否支持唤醒手环;
  2. 增加resetDevice()readTempeMeasureInterval()setTempMeasureInterval()readTempAlarmValue()setTempAlarmValue()readAlarmGear()setAlarmGear()等方法。由于sdk兼容以前的手环设备,所以在调用这些方法时需要做些判断。具体查看本文档。

2020/08/26

  1. 修改设置档位为0到4;
  2. 增加如下方法:
    • setAwakenTime()
    • getAwakenTime()

2020/09/01

修改单次最多读取温度历史数据条数从7改为14,影响到方法为:readTemperatureHistory()

2020/09/16

设置和读取温度告警值范围修改为30.0℃ ~ 42.0℃

2020/10/10

  1. 新增对TemperatureHistoryWristbandHistory属性的说明;
  2. 在读取历史数据过程中(接触数据和温度数据),可能会产生丢包。如果对历史数据较为敏感,那么可以在读取到历史数据后对其条数进行判断,如果与预期结果条数不一致,可重新读取。

2021/04/02

增加读取和写入数据功能,这些功能位于连接后功能列表的第15至第20条。另外,请务必注意,部分新功能是在新固件上增加,在使用这些功能之前,需要满足固件和硬件的基础上使用。 具体看demo。

另外,在连接后的功能列表,补充了这些功能可以在哪些版本下可用的条件判断

上次更新:: 2021/4/6 下午5:09:38