# MinewMTB13Kit Documentation

This SDK only supports Bluetooth devices manufactured by Minew.

The SDK helps developers handle all interactions between the phone and Bluetooth devices, including: scanning for devices, advertising data, connecting to devices, writing data to devices, and receiving data from devices, etc.

Currently, the SDK only supports the MTB13 industrial high-protection asset tag device.

# Preparations

Overall architecture: MTB13BleDevicesManager is the device management class, which remains a singleton for the lifetime of the APP. MTB13Entity is the device entity class; the SDK generates one instance for each device, used both after scanning and after connection. It internally contains the device's advertising data, which is updated continuously as the device broadcasts during scanning.

MTB13BleDevicesManager : the device management class, which can scan for nearby devices, connect to them, verify them, and more.

MTB13Entity : the device instance obtained during scanning, which inherits from BaseBleDeviceEntity

# I. Import into the Project

# 1. Development Environment

The SDK requires a minimum of Android 7.0, corresponding to API Level 24. Set minSdkVersion to 24 or higher in the module's build.gradle:

   android {
       defaultConfig {
           applicationId "com.xxx.xxx"
           minSdkVersion 24
       }
   }
1
2
3
4
5
6

# 2. Add the Library

Add the AAR package to the module's libs folder, and add the following statements to the module's build.gradle (add the dependency directly):


   implementation files('libs\\SDK_MTB13-release.aar')
   api 'androidx.appcompat:appcompat:1.5.0'
   api 'org.lucee:bcprov-jdk15on:1.52.0'
   api 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.16.0'
   api 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
1
2
3
4
5
6

To add the .so library files, add the following configuration to the App directory's build.gradle:

   android {
       defaultConfig {
           ndk {
               abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
           }
       }
       sourceSets {
           main {
               jniLibs.srcDirs = ['libs']
           }
       }
   }
1
2
3
4
5
6
7
8
9
10
11
12

The following permissions are required in AndroidManifest.xml. If targetSdkVersion is greater than 23, you need to implement permission management to obtain these permissions:

       <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" tools:node="replace" />
       <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" tools:node="replace" />
       <uses-permission android:name="android.permission.BLUETOOTH_SCAN" tools:remove="android:usesPermissionFlags" />
       <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" tools:remove="android:usesPermissionFlags" />
       <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:remove="android:usesPermissionFlags" />
       <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
       <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
       <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
       <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
       <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
1
2
3
4
5
6
7
8
9
10

# II. Usage

The SDK is divided into three phases: scanning, connection, and read/write.

# 1. Start Scanning

On Android 6.0 and above, before performing a BLE scan, you must first obtain Bluetooth permissions and enable the location service.

To start Bluetooth scanning, Bluetooth must be enabled first. If you initiate a scan without enabling Bluetooth, the APP will crash. You can use BLETool.checkBluetooth(this) to check whether Bluetooth is enabled. If it is not enabled, enable Bluetooth first.

    private fun checkBlePermissions() {
        val requestPermissionList = listOf(
            Manifest.permission.BLUETOOTH_SCAN,
            Manifest.permission.BLUETOOTH_CONNECT,
            Manifest.permission.BLUETOOTH_ADVERTISE,
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_FINE_LOCATION
        )
        PermissionConverter.putPermissionDescriptionMap(
            R.string.common_permission_nearby_devices,
            R.string.common_request_gps_nearby_devices_message_permission
        )
        PermissionConverter.putPermissionDescriptionMap(
            R.string.common_permission_location,
            R.string.common_request_gps_message_permission
        )
        if (!XXPermissions.isGrantedPermissions(this,requestPermissionList)){
            XXPermissions.with(this)
                .permission(requestPermissionList)
                .interceptor(PermissionInterceptor())
                .description(PermissionDescription())
                .request(object : OnPermissionCallback {
                    override fun onGranted(
                        permissions: MutableList<String>,
                        allGranted: Boolean
                    ) {
                        if (!allGranted) {
                            return
                        }else{
                            checkBluetooth()
                        }
                    }
                })
        }else{
            checkBluetooth()
        }
    }



	fun checkBluetooth(){
         when (BLETool.checkBluetooth(this@ScanMtb13DevicesActivity)) {
            BluetoothState.BLE_NOT_SUPPORT -> {
                Toast.makeText(this@ScanMtb13DevicesActivity, "Not Support BLE", Toast.LENGTH_SHORT).show()
            }
            BluetoothState.BLUETOOTH_OFF -> {
                val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
                launcherActivityResultForBle.launch(enableIntent)
            }
            BluetoothState.BLUETOOTH_ON -> {
                startScan()

            }
            else -> {
            }
        }
    }



	 /**
     * Start scanning
     */
	fun startScan(){
        val manager = MTB13BleDevicesManager.getInstance()
        manager.startScan(ModuleMTB13Application.getApplication().mApplication, 5*60*1000,object :
            OnScanDevicesResultListener<MTB13Entity> {
            override fun onScanResult(scanList: MutableList<MTB13Entity>) {

            }


            override fun onStopScan(scanList: MutableList<MTB13Entity>) {

            }
        })
	}

    /**
     * Stop scanning
     */
    private fun stopScan() {
       val manager = MTB13BleDevicesManager.getInstance()
       manager.stopScan(ModuleMTB13Application.getApplication().mApplication)
    }
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

Note: During scanning, do not frequently call the manager.startScan() and manager.stopScan() methods. Set a reasonable scan duration. Avoid calling manager.startScan() and manager.stopScan() multiple times within one minute, otherwise the Bluetooth stack may malfunction. If problems occur, go to the phone's system settings → Bluetooth → manually toggle Bluetooth off and then on.

During scanning, the APP can obtain some of the device's current data through the SDK. As shown below, the device data is obtained via MTB13Entity and stored in the advertising frame object.

The SDK provides BaseBleDeviceEntity as the base class of MTB13Entity for storing the device's common data, as shown in the table below:

Name Type Description
macAddress String Device MAC address
name String Device name
rssi int RSSI (signal strength)

BaseBleDeviceEntity also holds a BaseNanoLinkFrame, which is used internally to store the device's advertising data frames captured during scanning. You can retrieve it as follows:

val mtb13AdvFrame : Mtb13AdvFrame? = item.nanoLinkFrame?.let {it as Mtb13AdvFrame}
    // MAC address
    val mac = mtb13AdvFrame?.macAddress
    // deviceName
    String deviceName = mtb13AdvFrame?.deviceName
    // battery percentage; default: Integer.MIN_VALUE indicates battery level not yet obtained
    val battery = mtb13AdvFrame?.battery
    // firmware version
    val firmwareVersion =   mtb13AdvFrame?.firmwareVersion
    // temperature data
        mtb13AdvFrame?.temperatureHumidityList?.let { htList ->
           val temperature = htList[0].temperature
     }
    // motion state
	mtb13AdvFrame?.motionState?.let { motion ->
     val state = motion.motionState
     }
    // door/window (contact sensor) state
	mtb13AdvFrame?.motionState?.let { contactSensorState ->
     val state = contactSensorState.state
     }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

The MTB13 industrial high-protection asset tag device has 1 advertising frame type.

  1. Device frame

    • Mtb13AdvFrame
Name Type Description
mac String Firmware MAC address
deviceName String Device name
firmwareVersion String Version number
battery int Battery percentage; default: Integer.MIN_VALUE indicates battery level not obtained
motionState MotionState ACC motion state; motionState=0 stationary, motionState=1 moving
contactSensorState ContactSensorState Door/window state; contactSensorState=0 closed, contactSensorState=1 open
temperatureHumidityList List Temperature data; temperature=-128f is invalid. Any other value is valid

# 2. Connection

Before connecting, you generally need to stop scanning first. The SDK provides methods for connecting and disconnecting.

val mBleManager = MTB13BleDevicesManager.getInstance()
// Stop scanning
mBleManager.stopScan(context);
// Connect: module is the device to be connected
val module:MTB13Entity;
mBleManager.connect(context,module)
// Disconnect: macAddress is the device MAC
mBleManager.disConnect(macAddress)
1
2
3
4
5
6
7
8

Note: Before connecting to a device, make sure the device has been scanned. If the device's advertisement has not been scanned, calling the connect method will fail.

After calling connect(), the SDK monitors the connection process via state callbacks.

// Set the listener
mBleManager.setOnConnStateListener {macAddress, connectionState ->

                when (it) {
                BleConnectionState.Connecting -> {
                    // This state is reported after calling connect()

                }
                BleConnectionState.Connected -> {
                    // Initial connection succeeded as a transitional phase; the connection is not truly established yet
                }
                BleConnectionState.EnterAuthenticatePassword -> {

                }
                BleConnectionState.AuthenticateSuccess ->{

                }
                BleConnectionState.AuthenticateFail ->{

                }
                BleConnectionState.ConnectComplete -> {
                    // Device connection is complete; only then can you operate the device or navigate to another screen via the methods provided by MTB13BleDevicesManager

                }
                BleConnectionState.Bond_None -> {
                    // Bonding callback: this is the invalid bonding state
                }
                BleConnectionState.Bond_Bonding -> {
                    // Bonding callback: this is the bonding-in-progress state
                }
                BleConnectionState.Bond_Bonded -> {
                 // Bonding callback: this is the bonded (completed) state
                // Only after bonding is complete can you operate the device via the methods provided by MTB13BleDevicesManager

                }
                BleConnectionState.Disconnect -> {
                    LogUtil.d("connectionListener", "ConnectionState.Disconnect")
                    LoadingDialogUtil.dismissLoadingDialog()
//                    ToastUtils.showLong(getString(R.string.conn_failure))
                }
                else -> {}
            }
}
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

During the connection process, the SDK returns multiple connection states to the APP, and the APP must handle them properly.

  • BleConnectionState.Connecting, BleConnectionState.Connected: The device is connecting. Do not perform time-consuming operations in this state, because the SDK is discovering services and sending authentication data, etc.
  • BleConnectionState.Bond_None: Invalid bonding state.
  • BleConnectionState.Bond_Bonding: Device is bonding.
  • BleConnectionState.Bond_Bonded: Bonding completed; only then can you operate the device via the methods provided by MTB13BleDevicesManager.
  • BleConnectionState.ConnectComplete: The device is fully connected; you can now perform read/write operations, such as configuring advertising parameters, reading historical data, etc.
  • BleConnectionState.Disconnect: Callback triggered when connection fails or the device disconnects.

# 3. Device Configuration Read/Write Operations

The APIs for reading and writing device configuration are as follows, invoked via the MTB13BleDevicesManager.getInstance() object:

# 1. Synchronize Device Time with Phone's Current Time

By default, the SDK will automatically grant the device the phone's time during the connection process.


   	// First, check whether Bluetooth is enabled. The phone's CurrentTimeService can only be started when Bluetooth is on. If Bluetooth is off and you try to enable it, some Android systems may throw errors. You must also monitor the Bluetooth state: start the service via startSyncTimeServer when Bluetooth turns on, and stop it via closeSyncTimeServer when Bluetooth turns off. Note: when the Activity's onDestroy is called, you must also stop the service via closeSyncTimeServer.
   	val mBleManager = MTB13BleDevicesManager.getInstance()
   	// Start the service
   	mBleManager.startSyncTimeServer(context)
       // Stop the service
       mBleManager.closeSyncTimeServer(context)


   	// Some phones may occasionally fail at time synchronization, so you need to check whether time synchronization succeeded. It is recommended to check 3 seconds after the device is connected.
   	mBleManager.isDeviceReadSyncTime(context);
   	// If the time synchronization did not succeed, you need to proactively send a time-sync command to the device.
   	mBleManager.writeTimeToDevice(macAddress, onWriteTimeToDeviceListener);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2. Get Firmware Version

FirmwareVersionModel

Name Type Description
versionInfoList List Collection of version information

VersionInfo

Name Type Description
firmwareName String Firmware name
firmwareType int Default value -1
firmwareVersion String Firmware version number
slot String Slot
methods ArrayList
       /**
        * Get firmware version
        * @param macAddress Device MAC address
        * @param listener   Callback listener
        */
       void getFirmwareVersion(@NonNull String macAddress, OnQueryResultListener<FirmwareVersionModel> listener);


           /**
        * Get firmware version info
        * @return FirmwareVersionModel?
        */
       suspend fun getFirmwareInfo(): FirmwareVersionModel? = withContext(Dispatchers.Default){
           if(connectMacAddress == null){
               return@withContext null
           }
           return@withContext suspendCancellableCoroutine<FirmwareVersionModel?>  { continuation ->

               manager.getFirmwareVersion(connectMacAddress!!,
                   OnQueryResultListener<FirmwareVersionModel> { _, queryInfo ->
                       continuation.resume(queryInfo, null)
                   })
           }
       }
           /**
        * Get version information
        */
       private fun getFirmwareInfo(){
           lifecycleScope.launch(Dispatchers.Main){
               LoadingDialogUtil.showLoadingDialog()
               val result = mConnectedViewModel.getFirmwareInfo()
               LoadingDialogUtil.dismissLoadingDialog()
               result?.let {
                   val firmwareVersion = it.versionInfoList.first()
                   binding.tvFirmwareVersion.text = "V${firmwareVersion.firmwareVersion}"
               }
           }

       }
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

# 3. Factory Reset


       /**
        * String macAddress
        * Factory reset
        *
        * @param macAddress Device MAC address
        * @param listener   Callback listener
        */
       void reset(@NonNull String macAddress, OnModifyConfigurationListener listener);

           /**
        * Factory reset
        * @return Boolean
        */
       suspend fun reset(): Boolean = withContext(Dispatchers.Default) {
           if (connectMacAddress == null) {
               return@withContext false
           }
           return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
               manager.reset(connectMacAddress!!) {
                   continuation.resume(it, null)
               }
           }
       }

        /**
        * Factory reset
        */
       private fun reset(){
           lifecycleScope.launch(Dispatchers.Main){
               LoadingDialogUtil.showLoadingDialog()
               val result = mConnectedViewModel.reset()
               LoadingDialogUtil.dismissLoadingDialog()
               ToastUtils.showShort(when(result){
                   true -> R.string.common_reset_success
                   else -> R.string.common_reset_failure
               })
               mConnectedViewModel.disconnect(mConnectedViewModel.connectMacAddress!!)
               finish()
           }
       }

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

# 4. Power Off

       /**
        * Power off
        *
        * @param macAddress Device MAC address
        * @param listener   Callback listener
        */
       void powerOff(@NonNull String macAddress, OnModifyConfigurationListener listener);
           /**
        * Power off
        * @return Boolean
        */
       suspend fun shutdown(): Boolean = withContext(Dispatchers.Default) {
           if (connectMacAddress == null) {
               return@withContext false
           }
           return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
               manager.powerOff(connectMacAddress!!) {
                   continuation.resume(it, null)
               }
           }
       }

        /**
        * Power off
        */
       private fun powerOff(){
           lifecycleScope.launch(Dispatchers.Main){
               LoadingDialogUtil.showLoadingDialog()
               val result = mConnectedViewModel.shutdown()
               LoadingDialogUtil.dismissLoadingDialog()
               ToastUtils.showShort(when(result){
                   true -> R.string.common_power_off_success
                   else -> R.string.common_power_off_failure
               })
               mConnectedViewModel.disconnect(mConnectedViewModel.connectMacAddress!!)
               finish()
           }

       }
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

# 5. Reboot

    /**
     * Reboot
     * @param macAddress Device MAC address
     */
    void reboot( String macAddress, OnModifyConfigurationListener listener);

        /**
     * Reboot
     * @return Boolean
     */
    suspend fun reboot(): Boolean = withContext(Dispatchers.Default) {
        if (connectMacAddress == null) {
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.reboot(connectMacAddress!!) {
                continuation.resume(it, null)
            }
        }
    }

        /**
     * Reboot device
     */
    private fun reboot(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.reboot()
            LoadingDialogUtil.dismissLoadingDialog()
            ToastUtils.showShort(when(result){
                true -> R.string.common_reboot_success
                else -> R.string.common_reboot_failure
            })
            mConnectedViewModel.disconnect(mConnectedViewModel.connectMacAddress!!)
            finish()
        }

    }
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

# 6. Set Static Passkey

      /**
        * Set static passkey
        * @param macAddress Device MAC address
        * @param passkey Password, 6-digit, range 000000 - 999999
        * @param listener Callback listener
        */
       void setStaticPassKey(@NonNull String macAddress, int passkey, OnModifyConfigurationListener listener);

       /**
        * Change device password
        * @return Boolean
        */
       suspend fun setPasskey(password:Int): Boolean = withContext(Dispatchers.Default) {
           if (connectMacAddress == null) {
               return@withContext false
           }
           return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
               manager.setStaticPassKey(connectMacAddress!!,password) {
                   continuation.resume(it, null)
               }
           }
       }

       /**
        * Set device password
        * @param password String
        */
       private fun setDevicePassword(password:Int){
           lifecycleScope.launch(Dispatchers.Main) {
               LoadingDialogUtil.showLoadingDialog()
               val result = mConnectedViewModel.setPasskey(password)
               LoadingDialogUtil.dismissLoadingDialog()
               ToastUtils.showShort(when(result){
                   true -> R.string.common_set_device_password_success_message
                   else -> R.string.common_set_device_password_fail_message
               })
           }
       }
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

# 7. Query and Set Device Name

    /**
     * Get device name
     * @param macAddress Device MAC address
     * @param listener Callback listener
     */
    void getDeviceName( String macAddress, OnQueryResultListener<String> listener);

    /**
     * Set device name
     * @param macAddress Device MAC address
     * @param name Device name, max length 9 characters
     * @param listener Callback listener
     */
    void setDeviceName( String macAddress, String name, OnModifyConfigurationListener listener);

        /**
     * Get device name
     * @return String?
     */
    suspend fun getDeviceName(): String? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<String?>  { continuation ->

            manager.getDeviceName(connectMacAddress!!,{ _, name ->
                continuation.resume(name, null)
            })
        }
    }

    /**
     * Set device name
     * @return String?
     */
    suspend fun setDeviceName(name:String):Boolean = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.setDeviceName(connectMacAddress!!,name) {
                continuation.resume(it, null)
            }
        }
    }

    /**
     * Get device name
     */
    private fun getDeviceName(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.getDeviceName()
            LoadingDialogUtil.dismissLoadingDialog()
            result?.let {
                binding.tvDeviceName.text = it
            }
        }
    }

    /**
     * Set device name; name length limited to within 9 characters
     */
    private fun setDeviceName(name:String){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.setDeviceName(name)
            LoadingDialogUtil.dismissLoadingDialog()
            ToastUtils.showShort(when(result){
                true -> R.string.common_set_device_name_success_message
                else -> R.string.common_set_device_name_fail_message
            })
            if(result){
                binding.tvDeviceName.text = name
            }
        }

    }

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

# 8. Query HT Sensor Parameters

TemperatureHumidityConfiguration

Name Type Description
intvl int T&H sampling interval, in seconds
        /**
         * Query HT sensor parameters
         * @param macAddress
         * @param listener
         */
        void getTemperatureHumidityConfig(String macAddress, OnQueryResultListener<TemperatureHumidityConfiguration> listener);

        /**
         * Set HT sensor parameters
         * @param macAddress
         * @param htSensorConfig
         * @param listener
         */
        void setTemperatureHumidityConfig(String macAddress, TemperatureHumidityConfiguration temperatureHumidityConfiguration, OnModifyConfigurationListener listener);

    /**
     * Get T&H parameters
     * @return AdvConfigEntity?
     */
    suspend fun getTemperatureHumidityConfig(): TemperatureHumidityConfiguration? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<TemperatureHumidityConfiguration?>  { continuation ->

            manager.getTemperatureHumidityConfig(connectMacAddress!!,
                OnQueryResultListener<TemperatureHumidityConfiguration> { _, queryInfo ->
                    continuation.resume(queryInfo, null)
                })
        }
    }

    /**
     * Set T&H parameters
     * @return Boolean
     */
    suspend fun setTemperatureHumidityConfig(temperatureHumidityConfiguration: TemperatureHumidityConfiguration): Boolean = withContext(Dispatchers.Default) {
        if (connectMacAddress == null) {
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.setTemperatureHumidityConfig(connectMacAddress!!,temperatureHumidityConfiguration) {
                continuation.resume(it, null)
            }
        }
    }


   /**
     * Get T&H configuration parameters
     */
    private fun getTemperatureHumidityConfig(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.getTemperatureHumidityConfig()
            LoadingDialogUtil.dismissLoadingDialog()
            result?.let {
                binding.htSamplingIntervalTv.text = "${DataUtil.getHour(it.intvl)}h ${DataUtil.getMinute(it.intvl)}m ${DataUtil.getSecond(it.intvl)}s"
            }
        }

    }

    /**
     * Set T&H configuration parameters
     */
    private fun setTemperatureHumidityConfig(temperatureHumidityConfiguration: TemperatureHumidityConfiguration){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.setTemperatureHumidityConfig(temperatureHumidityConfiguration)
            LoadingDialogUtil.dismissLoadingDialog()
            when(result){
                true -> ToastUtils.showShort(getString(R.string.common_config_success))
                else -> ToastUtils.showShort(getString(R.string.common_config_fail))
            }

        }

    }

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

# 9. Query and Set ACC

AccSensorConfiguration

Name Type Description
sens int Trigger sensitivity; levels: 1,2,3,4. Higher value means more sensitive
window int Detection duration; levels: 200,500,1000,2000.
     /**
      * Query ACC sensor parameters
      * @param macAddress
      * @param listener
    */
    void getAccSensorConfig(String macAddress, OnQueryResultListener<AccSensorConfiguration> listener);
      /**
       * Set ACC sensor parameters
       * @param macAddress
       * @param listener
      */
    void setAccSensorConfig(String macAddress, AccSensorConfiguration accSensorConfiguration, OnModifyConfigurationListener listener);

    /**
     * Get ACC sensor parameters
     * @return AdvConfigEntity?
     */
    suspend fun getAccConfig(): AccSensorConfiguration? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<AccSensorConfiguration?>  { continuation ->

            manager.getAccSensorConfig(connectMacAddress!!,
                OnQueryResultListener<AccSensorConfiguration> { _, queryInfo ->
                    continuation.resume(queryInfo, null)
                })
        }
    }

    /**
     * Set ACC sensor parameters
     * @return Boolean
     */
    suspend fun setAccConfig(accSensorConfiguration: AccSensorConfiguration): Boolean = withContext(Dispatchers.Default) {
        if (connectMacAddress == null) {
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.setAccSensorConfig(connectMacAddress!!,accSensorConfiguration) {
                continuation.resume(it, null)
            }
        }
    }


    /**
     * Get ACC configuration parameters
     */
    private fun getAccConfig(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.getAccConfig()
            result?.let {
                mAccConfig = it
                when(it.sens){
                    1 -> binding.accAccTriggerSensitivitySelectorview.setSelectedIndex(3)
                    2 -> binding.accAccTriggerSensitivitySelectorview.setSelectedIndex(2)
                    3 -> binding.accAccTriggerSensitivitySelectorview.setSelectedIndex(1)
                    4 -> binding.accAccTriggerSensitivitySelectorview.setSelectedIndex(0)
                    else -> binding.accAccTriggerSensitivitySelectorview.setSelectedIndex(1)
                }
                when(it.window){
                    200 -> binding.accAccTestingDurationSelectorview.setSelectedIndex(0)
                    500 -> binding.accAccTestingDurationSelectorview.setSelectedIndex(1)
                    1000 -> binding.accAccTestingDurationSelectorview.setSelectedIndex(2)
                    2000 -> binding.accAccTestingDurationSelectorview.setSelectedIndex(3)
                    else -> binding.accAccTestingDurationSelectorview.setSelectedIndex(0)
                }
            }
            LoadingDialogUtil.dismissLoadingDialog()

        }

    }

    /**
     * Get ACC configuration parameters
     */
    private fun setAccConfig(accSensorConfiguration: AccSensorConfiguration){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.setAccConfig(accSensorConfiguration)
            LoadingDialogUtil.dismissLoadingDialog()
            when(result){
                true -> ToastUtils.showShort(getString(R.string.common_config_success))
                else -> ToastUtils.showShort(getString(R.string.common_config_fail))
            }
        }

    }

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 10. Query and Set Advertising Parameters

BroadcastConfigEntity

Name Type Description
param List Multiple advertising parameter configurations

BroadcastParamItem

Name Type Description
en boolean Whether advertising is enabled; true=on, false=off
m String Advertising mode. m="norm" normal mode, m="trig" ACC trigger mode
f String Advertising frame type; f="dtlm" Combination frame, f="macr" Repeater frame, f="ibeacon" iBeacon frame
p BroadcastParamDetail Advertising parameter configuration details

BroadcastParamDetail

Name Type Description
it Integer Advertising interval, range 100ms ~ 10000ms, step 100ms
tp Integer Advertising TX power; 8 levels available: -40dBm, -20dBm, -16dBm, -12dBm, -8dBm, -4dBm, 0dBm, 4dBm, 8dBm
phy Integer Advertising PHY rate. phy=0 1Mbps, phy=2 125Kbps
        /**
         * Read advertising parameters
         * @param macAddress
         * @param listener
         */
         void getBroadcastConfig(String macAddress, OnQueryResultListener<BroadcastConfigEntity> listener);

        /**
         * Set advertising parameters
         * @param macAddress
         * @param advConfigEntity
         * @param listener
         */
        void setBroadcastConfig(String macAddress, BroadcastConfigEntity broadcastConfigEntity, OnModifyConfigurationListener listener);



    /**
     * Get advertising parameters
     * @return AdvConfigEntity?
     */
    suspend fun getAdvParams(): BroadcastConfigEntity? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<BroadcastConfigEntity?>  { continuation ->

            manager.getBroadcastConfig(connectMacAddress!!,
                OnQueryResultListener<BroadcastConfigEntity> { _, queryInfo ->
                    continuation.resume(queryInfo, null)
                })
        }
    }


    /**
     * Set advertising parameters
     * @return Boolean
     */
    suspend fun setAdvParams(advConfigEntity: BroadcastConfigEntity): Boolean = withContext(Dispatchers.Default) {
        if (connectMacAddress == null) {
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.setBroadcastConfig(connectMacAddress!!,advConfigEntity) {
                continuation.resume(it, null)
            }
        }
    }



    /**
     * Get T&H configuration parameters
     */
    private fun getBroadcastConfig(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectViewModel.getAdvParams()
            LoadingDialogUtil.dismissLoadingDialog()
            result?.let {
                mBroadcastParametersConfiguration = it
                bindCombinateViewData(it)
                bindRepeaterViewData(it)

            }

        }
    }


        private fun bindCombinateViewData(broadcastConfigEntity: BroadcastConfigEntity){
        broadcastConfigEntity.let {
            val combinationConfig = it.param?.firstOrNull { item -> item.f == BroadcastFrameType.BROADCAST_FRAME_COMBINATION.getCode() && item.m == currentMode }
            // Advertising interval
            when(currentMode){
                DeviceModel.TRIGGER_MODEL.getCode() -> {
                    binding.includeCombinationFrameConfig.combinationFrameToggleCheckbox.isChecked = combinationConfig?.isEn?:false
                    binding.includeCombinationFrameConfig.combinationFrameSettingContentLayout.visibility = when(binding.includeCombinationFrameConfig.combinationFrameToggleCheckbox.isChecked ){
                        true -> View.VISIBLE
                        false -> View.GONE
                    }
                }
                DeviceModel.NORMAL_MODEL.getCode() ->{
                    binding.includeCombinationFrameConfig.combinationFrameToggleCheckbox.visibility = View.GONE
                    binding.includeCombinationFrameConfig.combinationFrameSettingContentLayout.visibility = View.VISIBLE
                }
                else ->{}
            }
            binding.includeCombinationFrameConfig.advIntervalEdit.setText("${combinationConfig?.p?.it?:3000}")
            binding.includeCombinationFrameConfig.advIntervalEdit.setSelection(binding.includeCombinationFrameConfig.advIntervalEdit.text.toString().length)
            binding.includeCombinationFrameConfig.advIntervalSeekbar.progress = intervalRange.indexOf((combinationConfig?.p?.it?:3000)/intervalStep)
            // Advertising TX power
            binding.includeCombinationFrameConfig.advTxPowerSeekbar.progress = powerRange.indexOf(combinationConfig?.p?.tp?:0)
            binding.includeCombinationFrameConfig.advTxPowerTv.setText("${combinationConfig?.p?.tp?:0}")
            // Advertising PHY rate
            binding.includeCombinationFrameConfig.advRateRadioGroup.check(when(combinationConfig?.p?.phy){
                0 -> R.id.rate_1mbps_radioBtn
                2 -> R.id.rate_125kbps_radioBtn
                else -> R.id.rate_1mbps_radioBtn
            })
        }
    }

    private fun bindRepeaterViewData(broadcastConfigEntity: BroadcastConfigEntity){
        broadcastConfigEntity.let {
            val repeaterConfig = it.param?.firstOrNull { item -> item.f == BroadcastFrameType.BROADCAST_FRAME_REPEATER.getCode() && item.m == currentMode }
            binding.includeRepeaterFrameConfig.repeaterFrameToggleCheckbox.isChecked = repeaterConfig?.isEn?:false
            binding.includeRepeaterFrameConfig.repeaterFrameSettingContentLayout.visibility = when(binding.includeRepeaterFrameConfig.repeaterFrameToggleCheckbox.isChecked ){
                true -> View.VISIBLE
                false -> View.GONE
            }
            // Advertising interval
            binding.includeRepeaterFrameConfig.advIntervalEdit.setText("${repeaterConfig?.p?.it?:3000}")
            binding.includeRepeaterFrameConfig.advIntervalEdit.setSelection(binding.includeRepeaterFrameConfig.advIntervalEdit.text.toString().length)
            binding.includeRepeaterFrameConfig.advIntervalSeekbar.progress = intervalRange.indexOf((repeaterConfig?.p?.it?:3000)/intervalStep)
            // Advertising TX power
            binding.includeRepeaterFrameConfig.advTxPowerSeekbar.progress = powerRange.indexOf(repeaterConfig?.p?.tp?:0)
            binding.includeRepeaterFrameConfig.advTxPowerTv.setText("${repeaterConfig?.p?.tp?:0}")

        }
    }


    /**
     * Set advertising configuration parameters
     */
    private inline fun setBroadcastConfig(broadcastConfigEntity: BroadcastConfigEntity){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectViewModel.setAdvParams(broadcastConfigEntity)
            LoadingDialogUtil.dismissLoadingDialog()
            when(result){
                true -> ToastUtils.showShort(getString(R.string.common_config_success))
                else -> ToastUtils.showShort(getString(R.string.common_config_fail))
            }
        }
    }
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

# 11. Read and Configure iBeacon Advertising Parameters

AppleIBeaconParamConfiguration

Name Type Description
data List Multiple advertising parameter configurations

AppleIBeaconParamItem

Name Type Description
m String Advertising mode. m="norm" normal mode, m="trig" ACC trigger mode
d AppleIBeaconParamDetail Advertising parameter configuration details

AppleIBeaconParamDetail

Name Type Description
uuid byte[] UUID
major int Major
minor int Minor
cpwr int RSSI (measured power)

    /**
     * Get iBeacon advertising parameters
     * @param macAddress
     * @param listener
     */
    void getAppleIBeaconParamsConfig(String macAddress, OnQueryResultListener<AppleIBeaconParamConfiguration> listener);

    /**
     * Set iBeacon advertising parameters
     * @param macAddress
     * @param appleIBeaconParamConfiguration
     * @param listener
     */
    void setAppleIBeaconParamsConfig(String macAddress, AppleIBeaconParamConfiguration appleIBeaconParamConfiguration, OnModifyConfigurationListener listener);

        /**
     * Read iBeacon advertising parameters
     * @return Boolean
     */
    suspend fun getIBeaconParamsConfig(): AppleIBeaconParamConfiguration? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<AppleIBeaconParamConfiguration?>  { continuation ->

            manager.getAppleIBeaconParamsConfig(connectMacAddress!!,
                OnQueryResultListener<AppleIBeaconParamConfiguration> { _, queryInfo ->
                    continuation.resume(queryInfo, null)
                })
        }
    }

    /**
     * Set iBeacon advertising parameters
     * @return Boolean
     */
    suspend fun setIBeaconParamsConfig(advConfigEntity: AppleIBeaconParamConfiguration): Boolean = withContext(Dispatchers.Default) {
        if (connectMacAddress == null) {
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.setAppleIBeaconParamsConfig(connectMacAddress!!,advConfigEntity) {
                continuation.resume(it, null)
            }
        }
    }


    /**
     * Get iBeacon configuration parameters
     */
    private fun getIBeaconConfig(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectViewModel.getIBeaconParamsConfig()
            LoadingDialogUtil.dismissLoadingDialog()
            result?.let {
                bindIBeaconViewData(mBroadcastParametersConfiguration!!,it)
            }
        }
    }
    private fun bindIBeaconViewData(broadcastConfigEntity: BroadcastConfigEntity,appleIBeaconParamConfiguration: AppleIBeaconParamConfiguration){
        broadcastConfigEntity.let {
            val iBeaconConfig = it.param?.firstOrNull { item -> item.f == BroadcastFrameType.BROADCAST_FRAME_IBEACON.getCode() && item.m == currentMode }
            val appleIBeaconContent = appleIBeaconParamConfiguration.data?.firstOrNull { item -> item.m == currentMode}
            // Switch (enable/disable)
            binding.includeIbeaconFrameConfig.ibeaconFrameToggleCheckbox.isChecked = iBeaconConfig?.isEn?:false
            binding.includeIbeaconFrameConfig.ibeaconParamsSettingContentLayout.visibility = when(binding.includeIbeaconFrameConfig.ibeaconFrameToggleCheckbox.isChecked ){
                true -> View.VISIBLE
                false -> View.GONE
            }
            // Advertising interval
            binding.includeIbeaconFrameConfig.advIntervalEdit.setText("${iBeaconConfig?.p?.it?:3000}")
            binding.includeIbeaconFrameConfig.advIntervalEdit.setSelection(binding.includeIbeaconFrameConfig.advIntervalEdit.text.toString().length)
            binding.includeIbeaconFrameConfig.advIntervalSeekbar.progress = intervalRange.indexOf((iBeaconConfig?.p?.it?:3000)/intervalStep)
            // Advertising TX power
            binding.includeIbeaconFrameConfig.advTxPowerSeekbar.progress = powerRange.indexOf(iBeaconConfig?.p?.tp?:0)
            binding.includeIbeaconFrameConfig.advTxPowerTv.setText("${iBeaconConfig?.p?.tp?:0}")
            // RSSI
            binding.includeIbeaconFrameConfig.advRssiEdit.setText("${appleIBeaconContent?.d?.cpwr?:0}")
            binding.includeIbeaconFrameConfig.advRssiEdit.setSelection(binding.includeIbeaconFrameConfig.advRssiEdit.text.toString().length)
            binding.includeIbeaconFrameConfig.advRssiSeekbar.progress = rssiRange.indexOf(appleIBeaconContent?.d?.cpwr?:0)
            // iBeacon UUID
            if(appleIBeaconContent?.d?.uuid !=null){
                binding.includeIbeaconFrameConfig.uuidEdit.setText(BytesOptUtil.bytesToUUID(appleIBeaconContent?.d?.uuid).toString().replace("-",""))
            }

            binding.includeIbeaconFrameConfig.uuidEdit.setSelection(binding.includeIbeaconFrameConfig.uuidEdit.text.toString().length)
            // iBeacon major
            binding.includeIbeaconFrameConfig.majorEdit.setText("${appleIBeaconContent?.d?.major?:""}")
            binding.includeIbeaconFrameConfig.majorEdit.setSelection(binding.includeIbeaconFrameConfig.majorEdit.text.toString().length)
            // iBeacon minor
            binding.includeIbeaconFrameConfig.minorEdit.setText("${appleIBeaconContent?.d?.minor?:""}")
            binding.includeIbeaconFrameConfig.minorEdit.setSelection(binding.includeIbeaconFrameConfig.minorEdit.text.toString().length)

            // Advertising PHY rate
            binding.includeIbeaconFrameConfig.advRateRadioGroup.check(when(iBeaconConfig?.p?.phy){
                0 -> R.id.rate_1mbps_radioBtn
                2 -> R.id.rate_125kbps_radioBtn
                else -> R.id.rate_1mbps_radioBtn
            })
        }
    }
    /**
     * Set iBeacon configuration parameters
     */
    private fun setIBeaconConfig(appleIBeaconParamConfiguration: AppleIBeaconParamConfiguration){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectViewModel.setIBeaconParamsConfig(appleIBeaconParamConfiguration)
            LoadingDialogUtil.dismissLoadingDialog()
            when(result){
                true -> ToastUtils.showShort(getString(R.string.common_config_success))
                else -> ToastUtils.showShort(getString(R.string.common_config_fail))
            }
        }
    }
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

# 12. Read and Set Scan Filter Configuration

ScanParamsConfig

Name Type Description
param List Scan parameters
filter List<TypeMap<String, Object>> Filter parameters

ScanParamItem

Name Type Description
en boolean Whether enabled; true=on, false=off
m String Mode. m="norm" normal mode, m="trig" ACC trigger mode
p ScanParamDetail Scan parameter configuration details

ScanParamDetail

Name Type Description
it float Scan interval
wd float Scan window
to int Scan timeout
gap int Scan restart interval
at boolean Scan type; true=active scanning, false=passive scanning
    /**
     * Get scan parameters
     * @param macAddress
     * @param listener
     */
    void getScanConfig(String macAddress, OnQueryResultListener<ScanParamsConfig> listener);

    /**
     * Set scan parameters
     * @param macAddress
     * @param scanParamsConfig
     * @param listener
     */
    void setScanConfig(String macAddress, ScanParamsConfig scanParamsConfig, OnModifyConfigurationListener listener);


    /**
     * Get scan filter parameters
     */
    suspend fun getScanFilter(): ScanParamsConfig? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<ScanParamsConfig?>  { continuation ->
            manager.getScanConfig(connectMacAddress!!,
                OnQueryResultListener<ScanParamsConfig> { _, queryInfo ->
                    try {
                        continuation.resume(queryInfo,null)
                    }catch (e: Exception){
                        LogUtil.e(e.message)
                    }
                })
        }
    }
    /**
     * Set scan filter parameters
     */
    suspend fun setScanFilter(scanFilter: ScanParamsConfig):Boolean = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean>  { continuation ->
            manager.setScanConfig(connectMacAddress!!,scanFilter){
                try {
                    continuation.resume(it, null)
                }catch (e: Exception){
                    LogUtil.e(e.message)
                }
            }
        }
    }
    /**
     * Get scan filter configuration parameters
     */
    private fun getScanFilterConfig(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectViewModel.getScanFilter()
            LoadingDialogUtil.dismissLoadingDialog()
            result?.let {
                val targetItem = it.param?.firstOrNull { paramItem -> paramItem.m == currentMode }
                targetItem?.let { paramItem ->
                    binding.includeScanParamsConfig.scanTimeoutEdit.setText("${paramItem.p.to}")
                    binding.includeScanParamsConfig.scanRestartIntervalEdit.setText("${paramItem.p.gap}")
                    binding.includeScanParamsConfig.scanMethodRadioGroup.check(when(paramItem.p.isAt){
                        true -> R.id.active_scan_radioBtn
                        else -> R.id.passive_scan_radioBtn
                    })
                    binding.includeScanParamsConfig.scanParametersToggleCheckbox.isChecked = paramItem.isEn
                    binding.includeScanParamsConfig.scanParamsSettingContentLayout.visibility = when(binding.includeScanParamsConfig.scanParametersToggleCheckbox.isChecked){
                        true -> View.VISIBLE
                        else -> View.GONE
                    }
                }
                if(targetItem == null){
                    binding.includeScanParamsConfig.scanParametersToggleCheckbox.isChecked = false
                    binding.includeScanParamsConfig.scanParamsSettingContentLayout.visibility = when(binding.includeScanParamsConfig.scanParametersToggleCheckbox.isChecked){
                        true -> View.VISIBLE
                        else -> View.GONE
                    }
                }
                // Filter parameters
                it.filter?.forEachIndexed { index, map ->
                    val filterConditions:MutableList<FilterCondition> = mutableListOf()
                    for (one in FilterConditionTypeForRegular.entries){
                        map.forEach { (key, value) ->
                            if (one.typeName.equals(key, true)) {
                                if (value!=null){
                                    if (value is ByteArray){
                                        filterConditions.add(FilterCondition(one.typeName, one,BytesOptUtil.byteArrayToHex(value)))
                                    }else{
                                        filterConditions.add(FilterCondition(one.typeName, one,value))
                                    }
                                }
                                return@forEach
                            }
                        }
                    }
                    if (filterConditions!=null&&filterConditions.isNotEmpty()){
                        mFilterConditionGroupList.add(FilterConditionGroup(filterConditions,initFilterConditionSelects()))
                    }
                }


            }
        }

    }
    /**
     * Get scan filter configuration parameters
     */
    private fun setScanFilterConfig(scanParamsConfig: ScanParamsConfig){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectViewModel.setScanFilter(scanParamsConfig)
            LoadingDialogUtil.dismissLoadingDialog()
            when(result){
                true -> ToastUtils.showShort(getString(R.string.common_config_success))
                else -> ToastUtils.showShort(getString(R.string.common_config_fail))
            }

        }

    }

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

# 13. Query and Configure Relay Control Data Reporting

RelayControl

Name Type Description
type String Data reporting method; "mac"=MAC reporting, "imm"=major&minor reporting
aln Integer For MAC reporting, the length of the MAC. Range {2,3,4,6}
max Integer Maximum number of reported devices; range 1~100
cnt Long
    /**
     * Get Relay Control configuration
     * @param macAddress  MAC address
     * @param listener Callback for the query result
     */
    void getRelayControl(String macAddress, OnQueryResultListener<RelayControl> listener);

    /**
     * Set Relay Control
     * @param macAddress  MAC address
     * @param relayControl Relay Control parameters
     * @param listener Callback for the set result
     */
    void setRelayControl(String macAddress, RelayControl relayControl, OnModifyConfigurationListener listener);

    /**
     * Get Relay Control parameters
     * @return RelayControl?
     */
    suspend fun getRelayControl(): RelayControl? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<RelayControl?>  { continuation ->
            manager.getRelayControl(connectMacAddress!!,
                OnQueryResultListener<RelayControl> { _, queryInfo ->
                    try {
                        continuation.resume(queryInfo,null)
                    }catch (e: Exception){
                        LogUtil.e(e.message)
                    }
                })
        }
    }
    /**
     * Set Relay Control parameters
     * @return Boolean
     */
    suspend fun setRelayControl(relayControl: RelayControl):Boolean = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean>  { continuation ->
            manager.setRelayControl(connectMacAddress!!,relayControl){
                try {
                    continuation.resume(it,null)
                }catch (e: Exception){
                    LogUtil.e(e.message)
                }
            }
        }
    }


    /**
     * Get RelayControl parameters
     */
    private fun getRelayControl() {
        lifecycleScope.launch(Dispatchers.Main) {
            LoadingDialogUtil.showLoadingDialog()
            relayControl = mConnectedViewModel.getRelayControl()
            LoadingDialogUtil.dismissLoadingDialog()
            relayControl?.let {
                binding.dataReportRadioGroup.check(when(it.type){
                    "mac" -> R.id.mac_radioBtn
                    "imm" -> R.id.major_minor_radioBtn
                    else -> R.id.mac_radioBtn
                })
                binding.reportMacByteCountLayout.visibility = when(it.type){
                    "mac" -> View.VISIBLE
                    "imm" -> View.GONE
                    else -> View.VISIBLE
                }
                if (it.aln!=null&&it.aln!=Int.MIN_VALUE){
                    binding.reportMacByteCountValueTv.text = it.aln.toString()
                    dataReportMacBytesCheckedIndex = dataArray.indexOf(it.aln.toString())
                }
                if (it.max!=null&&it.max!=Int.MIN_VALUE){
                    binding.reportDeviceCountValueEv.setText(it.max.toString())
                }

            }
        }
    }

    /**
     * Set RelayControl parameters
     */
    private fun setRelayControl(relayControl: RelayControl) {
        lifecycleScope.launch(Dispatchers.Main) {
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.setRelayControl(relayControl)
            LoadingDialogUtil.dismissLoadingDialog()
            when(result){
                true -> ToastUtils.showShort(getString(R.string.common_config_success))
                else -> ToastUtils.showShort(getString(R.string.common_config_fail))
            }
        }
    }
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

#

# 14. Firmware Upgrade


        /**
         * Firmware upgrade
         * @param context Context
         * @param macAddress Device MAC address
         * @param isLinkUpgrade Whether to upgrade via connection or OTA; true: URL upgrade, false: OTA upgrade
         * @param filePath OTA file path; can be null when upgrading via URL
         * @param upgradeData OTA data; can be null when upgrading via URL
         * @param target Upgrade target firmware; OTA data, default target="main", target="app" when upgrading via URL
         * @param listener
         */
        void firmwareUpgrade(@NonNull Context context,@NonNull String macAddress, boolean isLinkUpgrade, String filePath, @NonNull byte[] upgradeData, String target, OnFirmwareUpgradeListener listener) ;



        /**
         * Navigate to the system file picker
         */
        private fun gotoSystemFilePage() {
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
            intent.type = "*/*"
            launcherActivityResult.launch(intent)
        }

        // Firmware upgrade file-selection callback
        private val launcherActivityResult =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (it.resultCode == RESULT_OK) {
                    val fileUri = it.data?.data
                    LogUtil.e("fileUri","fileUri=${fileUri.toString()}")
                    fileUri?.let { uri ->
                        handleFile(uri)
                    }
                }
            }
        private fun handleFile(fileUri: Uri) {
            // Get the compressed package file name
            var fileName = ""
            // Get the compressed package path (convert URI to path)
            val cursor =this.contentResolver.query(fileUri, null, null, null, null, null)
            try {
                if (cursor != null && cursor.moveToFirst()) {
                    fileName = cursor.getString(
                        cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                    )
                } else {
                }
            } finally {
                cursor?.close()
            }

            val zipFilePath = FileUtil.getFileAbsolutePath(this, fileUri)
            toFirmwareUpgradePage(zipFilePath)
        }



        /**
         * Parse the upgrade package
         * @param filePath String
         */
        private fun dealFileData(filePath: String) {
            MinewExecutors.getInstance().getDiskIO().execute {
                try {
                    val bininputstream = ZipUtil.readZipFile(filePath, ".bin")
                    val bufferedInputStream = BufferedInputStream(bininputstream)
                    val allData = ArrayList<Byte>()
                    // Read 4 KB each time
                    val buffer = ByteArray(1024 * 4)
                    var bytesRead = 0
                    var tempLength = 0
                    while (bufferedInputStream.read(buffer).also { bytesRead = it } != -1) {
                        for (i in 0 until bytesRead) {
                            val by = buffer[i]
                            allData.add(by)
                        }
                        tempLength += bytesRead
                    }
                    val fileData = ByteArray(allData.size)
                    for (i in allData.indices) {
                        fileData[i] = allData[i]
                    }
                    bufferedInputStream.close()
                    // Invoke the upgrade command
                    firmwareUpgrade(this@FirmwareUpgradeActivity,filePath,fileData, "main")
                } catch (e: FileNotFoundException) {
                    e.printStackTrace()
                    chooseFileError(R.string.common_file_select_error)
                } catch (e: IOException) {
                    e.printStackTrace()
                    chooseFileError(R.string.common_file_select_error)
                }
            }
        }

        /**
         * Firmware upgrade
         */
        private fun firmwareUpgrade(
            context: Context,
            filePath:String,
            fileByte: ByteArray,
            target: String
        ) {
            mConnectedViewModel.firmwareUpgrade(context,filePath, fileByte,target,
                progressCallBack = { progress ->
                    WaitDialog.show(getString(R.string.common_upgrading), (progress / 100f))
                    binding.progressBarSimpleCustom.progress = progress.toFloat()
                },
                successCallBack = {
                    WaitDialog.dismiss()
                    ToastUtils.showShort(R.string.common_firmware_upgrade_successfully)

                },
                failCallBack = {
                    WaitDialog.dismiss()
                    ToastUtils.showShort(R.string.common_firmware_upgrade_failure2)

                }
            )
        }



        /**
         * Firmware upgrade
         */
        fun firmwareUpgrade(
            context: Context,
            filePath:String,
            fileByte: ByteArray,
            target:String,
            progressCallBack:(progress:Int) -> Unit,
            successCallBack:() -> Unit,
            failCallBack:() -> Unit,
        ){
            manager.firmwareUpgrade(context,connectMacAddress!!,false,filePath,fileByte,target,
                object : OnFirmwareUpgradeListener {

                    override fun updateProgress(progress: Int) {
                        // Upgrade package data write progress
                        progressCallBack(progress)
                    }

                    override fun upgradeSuccess() {
                        // Upgrade success callback; at this point the device will actively disconnect from the phone, which triggers the OnConnStateListener callback
                        successCallBack()
                    }

                    override fun upgradeFailed() {
                        // Upgrade failed
                        failCallBack()
                    }
                })
        }



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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158

# 15. Query Historical Records

Historical data can be queried by type, including contact sensor, ACC, temperature & humidity, and scan data, etc.

        /**
         * Get device stored data
         * @param queryAllData true=query all, false=query by time range. Note: querying by time range is recommended; querying all may fail due to the phone's limited RAM.
         * @param startTime Long Start query time, in seconds
         * @param endTime Long   End query time, in seconds
         * @param systemTime Long Phone's current system time, in seconds. It is recommended to pass: System.currentTimeMillis()/1000
         */
        private fun getDeviceStorageData(queryAllData:Boolean, sensorType: StorageDataBlockType, startTime:Long, endTime:Long, systemTime: Long){
            lifecycleScope.launch(Dispatchers.Main) {
                LoadingDialogUtil.showLoadingDialog()
                lifecycleScope.launch(Dispatchers.IO) {
                    val storageSearch = when(sensorType){
                        StorageDataBlockType.Temperature_V2_x100) ->  StorageIndexRecordSearcher<StorageActualDataHumidityAndTemperatureX100>()
                        StorageDataBlockType.Contact_Sensor_State ->  StorageIndexRecordSearcher<StorageActualDataContactSensorState>()
                        StorageDataBlockType.Motion ->  StorageIndexRecordSearcher<StorageActualDataMotion>()
                        StorageDataBlockType.Advertising_Report ->  StorageIndexRecordSearcher<StorageActualDataScanResultAdvertisingReportData>()
                        else -> StorageIndexRecordSearcher<StorageActualDataHumidityAndTemperatureX100>()
                    }

                    storageSearch.getStorageIndexRecordsNeedSearchFile(
                        this@HistoryDataTimePickerActivity,
                        mConnectedViewModel.connectMacAddress,
                        false,
                        StorageIndexRecordCondition()
                            .setStorageDataBlockType(sensorType)
                            .setInstance(StorageIndexInstance.INSTANCE_DEFAULT)
                            .setStartTime(when (queryAllData){
                                true ->0
                                else  ->startTime
                            })
                            .setEndTime(when (queryAllData){
                                true ->systemTime
                                else  ->endTime
                            })
                    ) { resultList,exception ->
                        lifecycleScope.launch (Dispatchers.Main){
                            LoadingDialogUtil.dismissLoadingDialog()
                            if (exception==null
                                &&resultList!=null
                                &&!resultList.isEmpty()
                            ){
                                when(sensorType){
                                    StorageDataBlockType.Temperature_V2_x100 ->  {

                                    }
                                    StorageDataBlockType.Contact_Sensor_State->  {

                                    }
                                    StorageDataBlockType.Motion->  {

                                    }
                                    StorageDataBlockType.Advertising_Report->  {

                                    }
                                    else -> {}
                                }
                            }else{

                            }
                        }
                    }
                }
            }
        }
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# 16. Clear Historical Data

    /**
     *  Clear historical data
     * @param macAddress
     * @param force Whether to force-clear data; force=true
     * @param listener
     */
    void clearHistory(@NonNull String macAddress, boolean force, OnModifyConfigurationListener listener);

     /**
     * Clear historical data
     * @return force
     */
    suspend fun clearHistory(force:Boolean):Boolean = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext false
        }
        return@withContext suspendCancellableCoroutine<Boolean> { continuation ->
            manager.clearHistory(connectMacAddress!!,force) {
                continuation.resume(it, null)
            }
        }
    }

     /**
     * Clear device historical data
     */
    private fun cleanHistoryData(){
        lifecycleScope.launch(Dispatchers.Main){
            LoadingDialogUtil.showLoadingDialog()
            val result = mConnectedViewModel.clearHistory( true)
            LoadingDialogUtil.dismissLoadingDialog()
            ToastUtils.showShort(when(result){
                true -> R.string.common_clear_successfully
                else -> R.string.common_clear_failed
            })
        }

    }
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

# 17. Get the List of Commands Supported by the Device

CommandListEntity

Name Type Description
groupId int Corresponds to the groupId of the nanolink protocol
commandIds List Corresponds to the commandId(s) of the nanolink protocol
    /**
     * Get the list of commands supported by the device
     * @param macAddress Device MAC address
     * @param listener Callback listener
     */
    void getDeviceSupportCommandList(@NonNull String macAddress, OnQueryResultListener<List<CommandListEntity>> listener);

        /**
     * Query the list of commands supported by the device
     * @return CommandListEntity?
     */
    suspend fun getDeviceSupportCommandList(): List<CommandListEntity>? = withContext(Dispatchers.Default){
        if(connectMacAddress == null){
            return@withContext null
        }
        return@withContext suspendCancellableCoroutine<List<CommandListEntity>?>  { continuation ->

            manager.getDeviceSupportCommandList(connectMacAddress!!,
                OnQueryResultListener<List<CommandListEntity>> { _, queryInfo ->
                    continuation.resume(queryInfo, null)
                })
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Document Update Log

  • 2026/09/20 Added basic MTB13 device operation APIs
Last Updated:: 9/24/2026, 4:17:45 PM