2

Ok bare with me because this will be a long read. I have an app that I am trying to integrate BLE into instead of classical Bluetooth. The app was made by freelancers but I am tasked to alter it. It uses a MVVM architecture.

originally this app used classical Bluetooth. There is a Bluetooth Controller class, and it gets instantiated in a DataStore class. The DataStore has functions that use the instance made of the BluetoothController to call on functions in that class that request codes that correspond to the desired parameter. Meaning, There is an enum class that has the names of every parameter to be read or written and represents each one with a code.

for example:

    `enum class ReadRequestCodes(val value:String) {
        KEY_ADDRESS  ("08 00 00 00 20 30 05 11 00 00 00 00 00"),
        TOOL_ADDRESS ("08 00 00 00 20 30 05 27 00 00 00 00 00"),
        RPM_THRESHOLD("08 00 00 00 20 30 05 13 00 00 00 00 00"),
        BACKLASH     ("08 00 00 00 20 30 05 22 00 00 00 00 00"),
    
        POWER_SRC_TYPE     ("08 00 00 00 20 30 05 26 00 00 00 00 00"),
        BATTERY1_PERCENTAGE("08 00 00 00 20 30 11 00 00 00 00 00 00"),
        BATTERY2_PERCENTAGE("08 00 00 00 20 30 12 00 00 00 00 00 00"),
    
        HOME_POSITION     ("08 00 00 00 20 30 05 15 00 00 00 00 00"),
        BYPASS_POSITION   ("08 00 00 00 20 30 05 17 00 00 00 00 00"),
        HC_POSITION       ("08 00 00 00 20 30 05 19 00 00 00 00 00"),
        ISOLATION_POSITION("08 00 00 00 20 30 05 1B 00 00 00 00 00"),
    
        PRESSURE_SENSOR_GAIN("08 00 00 00 20 30 05 2B 00 00 00 00 00"),
        PRESSURE_SENSOR_OFFSET("08 00 00 00 20 30 05 2C 00 00 00 00 00"),
        PRESSURE_SENSOR_RANGE("08 00 00 00 20 30 05 2A 00 00 00 00 00"),
        PRESSURE_SENSOR_EXCITATION("08 00 00 00 20 30 05 29 00 00 00 00 00"),
        PRESSURE_SENSOR_SERIAL("08 00 00 00 20 30 05 2D 00 00 00 00 00"),
        GOTO_CURRENT_LIMIT("08 00 00 00 20 30 05 50 00 00 00 00 00"),
        GOTO_RPM_LIMIT("08 00 00 00 20 30 05 52 00 00 00 00 00"),
        FW_HW_VERSION("08 00 00 00 20 30 09 00 00 00 00 00 00"),
        READ_EEPROM_MEMORY("08 00 00 00 20 30 0A 00 00 00 00 00 00")

}
`

This is in the Bluetooth Controller class. The DataStore has these functions:

 fun requestKeyAddress(){
        bluetoothController.requestReadValues(ReadRequestCodes.KEY_ADDRESS.value)
    }
    fun requestToolAddress(){
        bluetoothController.requestReadValues(ReadRequestCodes.TOOL_ADDRESS.value)
    }
    fun requestRpmThreshold(){
        bluetoothController.requestReadValues(ReadRequestCodes.RPM_THRESHOLD.value)
    }
    fun requestBacklash(){
        bluetoothController.requestReadValues(ReadRequestCodes.BACKLASH.value)
    }
    fun requestPowerSrc(){
        bluetoothController.requestReadValues(ReadRequestCodes.POWER_SRC_TYPE.value)
    }
    fun requestBattery1Percentage(){
        bluetoothController.requestReadValues(ReadRequestCodes.BATTERY1_PERCENTAGE.value)
    }
    fun requestBattery2Percentage(){
        bluetoothController.requestReadValues(ReadRequestCodes.BATTERY2_PERCENTAGE.value)
    }
    fun requestHomePos(){
        bluetoothController.requestReadValues(ReadRequestCodes.HOME_POSITION.value)
    }
    fun requestBypassPos(){
        bluetoothController.requestReadValues(ReadRequestCodes.BYPASS_POSITION.value)
    }
    fun requestIsolationPos(){
        bluetoothController.requestReadValues(ReadRequestCodes.ISOLATION_POSITION.value)
    }
    fun requestHcPos(){
        bluetoothController.requestReadValues(ReadRequestCodes.HC_POSITION.value)
    }

Each fragment or activity in the app has a viewmodel, viewmodelfactory, and a repository. Let me describe the flow of things:

  1. We connect to a device via Bluetooth
  2. BluetoothController handles data exchange between the app and the device it is connected to
  3. Data is store din the DataStore
  4. UI(Fragment or activity) writes or request to read certain data
  5. ViewModel of every UI requests the data through the corresponding repository
  6. Repository of a particular fragment gets data from the DataStore
  7. And so on depending on whether we want to request data from the device or write to it

ALL of this is done via classical Bluetooth. Now I want to change that to BLE. I was following a guide and made small app(Let's call it prototype) that scans for BLE devices, connects to the selected device and displays its characteristics and their properties. I made an activity in the app at hand (the main app I want to change) that contains the part for scanning and connecting. From what I have seen there is usually a Connection Manager Class that manages the part related to connections and data transfer. I'm not sure about that one beyond some of these functions and values:

  private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
    private var pendingOperation : BleOperationType? = null      //Operations types found in the BleOperationType sealed class
    private var listeners: MutableSet<WeakReference<ConnectionEventListener>> = mutableSetOf()

    fun servicesOnDevice(device: BluetoothDevice): List<BluetoothGattService>? = deviceGattMap[device]?.services
    fun listenToBondStateChanges(context: Context) {
        context.applicationContext.registerReceiver(
            broadcastReceiver,
            IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
        )
    }

    fun registerListener(listener: ConnectionEventListener){
        if(listeners.map { it.get() }.contains(listener)){ return }
        listeners.add(WeakReference(listener))
        listeners = listeners.filter { it.get() != null }.toMutableSet()
        Log.d("RegisteredListener","Added listener $listener, ${listeners.size} listeners total")
    }
    fun unregisterListener(listener: ConnectionEventListener){
        // Removing elements while in a loop results in a java.util.ConcurrentModificationException
        var toRemove : WeakReference<ConnectionEventListener>? = null
        listeners.forEach{
            if (it.get() == listener){
                toRemove = it
            }
        }

        toRemove?.let {
           listeners.remove(it)
            Log.d("UnregisteredListener","Removed listener ${it.get()}, ${listeners.size} listeners total")
        }
    }


    fun connect(device: BluetoothDevice, context: Context){
        if(device.isConnected()){
            Log.e("CheckConnection","${device.name} - ${device.address} is already connected")
        }
        else{
            enqueueOperation(Connect(device, context.applicationContext))
        }
    }
    fun terminateConnection(device: BluetoothDevice){
        if(device.isConnected())
            enqueueOperation(Disconnect(device))
        else{
            Log.e("CheckConnection","Not connected to ${device.name} - ${device.address}, cannot teardown connection")
            }
    }
    fun characteristicWrite(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, payload: ByteArray){
        val writeType = when {

            //This is is to make sure that a characteristic can be written to,
            // and whether it has a response or not
            characteristic.isWritable() -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
            characteristic.isWritableWithoutResponse() -> {BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE}
            else -> error("Characteristic ${characteristic.uuid} cannot be written to")
        }
        if(device.isConnected()){
            enqueueOperation(CharacteristicWrite(device, characteristic.uuid, writeType, payload))
        }else{
            Log.e("Check_Writable","Not connected to ${device.address}, cannot perform characteristic write")
            return
        }

//        bluetoothGattRef?.let { gatt ->
//            characteristic.writeType = writeType
//            characteristic.value = payload
//            gatt.writeCharacteristic(characteristic)
//        } ?: error("Not connected to a BLE device!")
    }
    fun characteristicRead(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic){
         if(device.isConnected() && characteristic.isReadable()){
             enqueueOperation(CharacteristicRead(device, characteristic.uuid))
         }else if(!characteristic.isReadable()){
             Log.e("Check_Readable", "Attempting to read ${characteristic.uuid} is not readable")
         }else if(!device.isConnected()){
             Log.e("Check_Connected","Not connected to ${device.address}, cannot perform characteristic read")
         }
    }
    fun writeDescriptor(device: BluetoothDevice, descriptor:BluetoothGattDescriptor, payload: ByteArray){
         if(device.isConnected() && (descriptor.isWritable() || descriptor.isCccd())){
             enqueueOperation(DescriptorWrite(device, descriptor.uuid, payload))
         } else if (!device.isConnected()){
           Log.e("Check_Descrip_Connected","Not connected to ${device.address}, cannot perform descriptor write")
         } else if (!descriptor.isWritable() && !descriptor.isCccd()){
           Log.e("Check_Descrip_Writable","Descriptor ${descriptor.uuid} cannot be written to")
         }
//        bluetoothGattRef?.let { gatt ->
//            descriptor.value = payload
//            gatt.writeDescriptor(descriptor)
//        } ?: error("Not connected to a BLE device!")
    }
    fun readDescriptor(device: BluetoothDevice, descriptor: BluetoothGattDescriptor){
        if (device.isConnected() && descriptor.isReadable()) {
            enqueueOperation(DescriptorRead(device,descriptor.uuid))
        } else if (!descriptor.isReadable()) {
            Log.e("Check_Descrip_Readable","Attempting to read ${descriptor.uuid} that isn't readable!")
        } else if (!device.isConnected()) {
            Log.e("Check_Descrip_Connected","Not connected to ${device.address}, cannot perform descriptor read")
        }
    }

    fun enableNotifications(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic) {
        if (device.isConnected() &&
            (characteristic.isIndicatable() || characteristic.isNotifiable())
        ) {
            enqueueOperation(EnableNotifications(device, characteristic.uuid))
        } else if (!device.isConnected()) {
            Log.e("Error","Not connected to ${device.address}, cannot enable notifications")
        } else if (!characteristic.isIndicatable() && !characteristic.isNotifiable()) {
            Log.e("Error","Characteristic ${characteristic.uuid} doesn't support notifications/indications")
        }
    }

    fun disableNotifications(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic) {
        if (device.isConnected() &&
            (characteristic.isIndicatable() || characteristic.isNotifiable())
        ) {
            enqueueOperation(DisableNotifications(device, characteristic.uuid))
        } else if (!device.isConnected()) {
            Log.e("Error","Not connected to ${device.address}, cannot disable notifications")
        } else if (!characteristic.isIndicatable() && !characteristic.isNotifiable()) {
            Log.e("Error","Characteristic ${characteristic.uuid} doesn't support notifications/indications")
        }
    }

It's actually an Object since I'll only instantiate the ConnectionManager once. Now all this was in my prototype app just for building the skeleton for the BLE features that would be integrated into my Main app.

This is what I have added to my Main app so far regarding BLE integration:

  class ScanForDevices : AppCompatActivity() {

    private val bluetoothAdapter: BluetoothAdapter by lazy {
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothManager.adapter
    }

    private val bleScanner by lazy {
        bluetoothAdapter.bluetoothLeScanner
    }

    private val scanSettings = ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
        .build()

    //Check whether the app is scanning or not
    private var isScanning = false
        set(value) {
            field = value
            runOnUiThread {
                if(value) binding.scanBtn.text = "Stop Scanning" else binding.scanBtn.text = "Start Scanning"
            }
        }

    private val scanResults = mutableListOf<ScanResult>()
    private val scanResultAdapter: ScanResultAdapter by lazy {
        ScanResultAdapter(scanResults) { result ->
            // User tapped on a scan result
            if(isScanning){
                stopBleScan()
            }
            with(result.device){
                Log.w("ScanResultAdapter", "Connecting to $address")
                Toast.makeText(this@ScanForDevices,"Connecting to $address",Toast.LENGTH_SHORT).show()
                connectGatt(this@ScanForDevices, false, gattCallback)
                //////////////////////////////////////////////////////////////////////
//                ConnectionManager.connect(this, this@ScanForDevices )
            }
        }
    }

    //To check that the permission is granted
    //It is only needed when performing a BLE scan
    private val isLocationPermissionGranted
        get() = hasPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)

    private lateinit var bluetoothGattRef : BluetoothGatt


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityScanForDevicesBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)


        //Check for Bluetooth availability
        if(bluetoothAdapter == null){
            Log.w("BluetoothSupport","Device does not support Bluetooth")
        }else{
            Log.i("BluetoothSupport","Device supports Bluetooth")
        }

        binding.scanBtn.setOnClickListener {
            if(isScanning) stopBleScan() else startBleScan()
        }
        initRecyclerView()

    }

    override fun onResume() {
        super.onResume()
        if(!bluetoothAdapter.isEnabled){
            promptEnableBluetooth()
        }
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            ENABLE_BLUETOOTH_REQUEST_CODE -> {
                if (resultCode != Activity.RESULT_OK) {
                    promptEnableBluetooth()
                }
            }
        }
    }
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            LOCATION_PERMISSION_REQUEST_CODE -> {
                if (grantResults.firstOrNull() == PackageManager.PERMISSION_DENIED) {
                    requestLocationPermission()
                } else {
                    startBleScan()
                }
            }
        }
    }


    private fun promptEnableBluetooth() {
        if (!bluetoothAdapter.isEnabled) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, ENABLE_BLUETOOTH_REQUEST_CODE)
        }
    }
    //Perform BLE scan after receiving permission to do so
    private fun startBleScan() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isLocationPermissionGranted) {
            requestLocationPermission()
        } else { /* TODO: Actually perform scan */
            scanResults.clear()
            scanResultAdapter.notifyDataSetChanged()
            bleScanner.startScan(null, scanSettings, scanCallback)
            isScanning = true //Check that the app is scanning
        }
    }
    //Stop BLE scan after the corresponding button is clicked
    private fun stopBleScan(){
        bleScanner.stopScan(scanCallback)
        isScanning = false
    }
    //request location permission
    private fun requestLocationPermission() {
        if (isLocationPermissionGranted) {
            return
        }
        runOnUiThread {
            //Using "org.jetbrains.anko:anko:0.10.8"
            alert {
                title = "Location permission required"
                message = "Starting from Android M (6.0), the system requires apps to be granted " +
                        "location access in order to scan for BLE devices."
                isCancelable = false
                positiveButton(android.R.string.ok) {
                    requestPermission(
                        android.Manifest.permission.ACCESS_FINE_LOCATION,
                        LOCATION_PERMISSION_REQUEST_CODE
                    )
                }
            }.show()
        }
    }
    //For scan results
    private fun initRecyclerView(){
        binding.scanResultsRecyclerView.apply {
            adapter = scanResultAdapter
//            val topSpacingDecoration = TopSpacingItemDecoration(30)
//            addItemDecoration(topSpacingDecoration)
            layoutManager = LinearLayoutManager(
                this@ScanForDevices,
                RecyclerView.VERTICAL,
                false
            )
            isNestedScrollingEnabled = false
        }

        val animator = binding.scanResultsRecyclerView.itemAnimator
        if(animator is SimpleItemAnimator) {
            animator.supportsChangeAnimations = false
        }
    }



    /**********************/
    /**CALLBACK FUNCTIONS**/
    /*********************/

    /** Since we didn't explicitly specify CALLBACK_TYPE_FIRST_MATCH as the callback type under our ScanSettings,
     * our onScanResult callback is flooded by ScanResults belonging to the same set of devices,
     * but with updated signal strength (RSSI) readings. In order to keep the UI up to date with the latest RSSI readings,
     * we first check to see if our scanResults List already has a scan result whose MAC address is identical to the new incoming ScanResult.
     * If so, we replace the older entry with the newer one. In the event that this is a new scan result,
     * we’ll add it to our scanResults List. For both cases,
     * we’ll inform our scanResultAdapter of the updated item so that our RecyclerView can be updated accordingly.**/
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            val indexQuery = scanResults.indexOfFirst { it.device.address == result.device.address }
            if (indexQuery != -1) { // A scan result already exists with the same address
                scanResults[indexQuery] = result
//                serviceUUIDsList = getServiceUUIDsList(result)!!
//                Log.d("Service",""+ serviceUUIDsList+"\n")
                Log.d("DeviceName",""+result.device.name)
                scanResultAdapter.notifyItemChanged(indexQuery)
            } else {
                with(result.device) {
                    Log.i("ScanCallback", "Found BLE device! Name: ${name ?: "Unnamed"}, address: $address")
                }
                scanResults.add(result)
                scanResultAdapter.notifyItemInserted(scanResults.size - 1)
            }
        }

        override fun onScanFailed(errorCode: Int) {
            Log.e("ScanCallback", "onScanFailed: code $errorCode")
        }
    }

    /**we want to stop the BLE scan if it’s ongoing,
     * and we call connectGatt() on the relevant ScanResult’s BluetoothDevice handle,
     * passing in a BluetoothGattCallback object that is defined as: **/

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            val deviceAddress = gatt.device.address

            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    Log.w("BluetoothGattCallback", "Successfully connected to $deviceAddress")
                    Handler(Looper.getMainLooper()).post {
                        Toast.makeText(this@ScanForDevices, "Successfully connected to $deviceAddress", Toast.LENGTH_SHORT).show()
                    }
                    // TODO: Store a reference to BluetoothGatt
                    bluetoothGattRef = gatt
                    Log.d("ConnectedDevice","")

                    runOnUiThread {
                        bluetoothGattRef?.discoverServices()
                    }
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.w("BluetoothGattCallback", "Successfully disconnected from $deviceAddress")
                    gatt.close()
                }
            } else {
                Log.w("BluetoothGattCallback", "Error $status encountered for $deviceAddress! Disconnecting...")
                gatt.close()
            }
        }

        //On the discovery of the BLE device's services
        //We display a log message of the number of services and the device's address, then print
        //the GATT table in a recycler view beneath the one of the scanned devices -----> services and characteristics available on a BLE device.
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {

            with(bluetoothGattRef) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.d(
                        "BluetoothGattCallback",
                        "Discovered ${services.size} services for ${device.address}"
                    )
                    printGattTable()
                }
            }

        }
    }


}

let's say I already connected to the desired device, how do I exchange data of the parameters in the fragments I mentioned whether it is something to be read from the device or written to the device but with BLE instead of classical Bluetooth?

I am completely lost and don't know what to do. Any insight? Or even list me the things I should do or the path to be taken to make the necessary changes?

1 Answers1

1

There are multiple ways of achieving what you want, but for simplicity, I suggest that you start by implementing a GATT table with one service inside of which there are 20 characteristics - one for each of your parameters. In other words, this service/characteristic will be responsible for hosting your data that will be read/written to from remote devices. As such, each of these characteristics should have mainly two properties: READ and WRITE. The device that is hosting the data should be a BLE peripheral that is discoverable to remote devices.

The reason why I believe this would work is that fundamentally your classic Bluetooth application was an SPP application in that you can read from/write to a remote device, and the simplest way to achieve this in BLE is to have a characteritic that can be read from and written to.

Then this is how it should all work on your device:- 1- On launch of the app, the GATT table will be populated with your service and characteristics (i.e. the parameters). 2- Once you receive a READ REQUEST event from a remote device on one of these characteristics, you reply with the parameter value. 3- Once you receive a WRITE REQUEST event from a remote device on one of these characteristics, you update the relevant characteristic value.

For information on creating a GATT table on Android, please check the links below:-

Youssif Saeed
  • 11,789
  • 4
  • 44
  • 72
  • Okay bear with me here as this is all new to me. My prototype app had an extension function that prints the services and characteristics of a device it's connected to. Now the characteristics here as in the ones to be requested or written to correct? Which means that in the second code snippet the parameters in question here are characteristics? If so what do I do in the fragment when I want to write data? Do I check the discovered characteristics' property (Readable or Writable) first? Then take action? – Mohamed El Kayal Dec 06 '21 at 12:21
  • I want to add that the mobile app is the one that request data from the BLE device – Mohamed El Kayal Dec 06 '21 at 18:15
  • The device which will contain the parameters will be the peripheral and the GATT server, while the device that will wirelessly read/write the parameters will be the central and the GATT client. For more information on what central/client and peripheral/server mean, have a look at this link: https://stackoverflow.com/a/25067349/2215147 – Youssif Saeed Dec 07 '21 at 08:01
  • In my answer, I am assuming that you are writing the code for both the device hosting the parameters and the device reading the parameters, is this the case? If it is, then you can assign a different unique UUID to each characteristic in the GATT table, and then when you read/write to these parameters remotely, you can use the UUID to choose the specific parameter. – Youssif Saeed Dec 07 '21 at 08:05
  • If the concept of GATT and UUIDs is new to you, then I recommend spending a few days to get familiar with the different BLE terms and understand how BLE communication works. There are many good resources which you can find here:- https://interrupt.memfault.com/blog/bluetooth-low-energy-a-primer – Youssif Saeed Dec 07 '21 at 08:13
  • My app on my phone is the one that reads parameters from a tool and writes new value for parameters to that tool as well. So I suppose the central here is my mobile phone where as the tool is peripheral – Mohamed El Kayal Dec 07 '21 at 11:38
  • As for the GATT table I was able to display characteristics with their properties as in the Ultimate BLE Guide that creates a starter BLE application that scans and connects to bLE devices, Then either reads or writes to them. The code I am writing is for the mobile app only. I connect it to a BLE kit used for testing. Each parameter has a RequestRead code, ReadResponseCode and WriteCommand Code like the one I posted in the first code snippet. These frames are so that the tool that communicates with my app knows which parameter is which. – Mohamed El Kayal Dec 07 '21 at 11:43
  • I'm supposed to make a payload bytes array that contains the parsed codes to be sent. The part I am stuck in is the data transfer or exchange. The part after scanning and connecting. I have a fragment with data I will type in or select from a drop down menu and then click the set button next to each of those fields. When I click set, I'm supposed to send word to the tool I am connected to that the designated parameter has been set with a new value. – Mohamed El Kayal Dec 07 '21 at 12:12
  • Another fragment contains parameters that are to be read from the tool. So I send a readRequest code of particular parameter and then get a readResponse code for said parameter from the tool which will appear in my fragment. The whole problem for me is how to make all this but with BLE rather than classical Bluetooth. The part for data exchange whether it be Reading or Writing. – Mohamed El Kayal Dec 07 '21 at 12:16