Android Question How do i scan for multiple BLE UUIDs?

ema01

Member
Licensed User
Longtime User
Hi,
I have to scan for different devices that report different UUIDs
The problem is, passing a list of UUIDs to BLEManager.Scan/BLEManager.Scan2 doesn't work. It works if i pass a list containing exactly one UUID, if two UUIDs are present in the list no BLE device is ever found (the callback is never called).

Passing Null is not possible because unfiltered scans won't happen if the screen is turned off, and that is another requirement.

I'm looking at the compiled java and it seems correct, other than the fact that Google declared the startLeScan method deprecated in API 21, and to use BluetoothLEScanner.startscan instead (see https://developer.android.com/refer...id.bluetooth.BluetoothAdapter.LeScanCallback))

but that is over my ability in integrating java in B4X so i don't know how to proceed.

Can somebody help?
 

ema01

Member
Licensed User
Longtime User
I see.
That sounds completely counterintuitive to me from the descritpion of the Scan Sub
Devices that don't advertise these uuids will not be discovered

may i suggest you change it to
"Devices that don't advertise ALL these uuids will not be discovered"
?

So, to achieve what i want to do i need to create a scan filter with uuid mask
 
Last edited:
Upvote 0

ema01

Member
Licensed User
Longtime User
I'm playing with the code in that example
I actually have the BLE code inside a class, not inside the starter so this line
B4X:
processBA.raiseEvent(this, "scan_result", result);

emitted an error.
I figured that i could do this
B4X:
starter.processBA.raiseEvent(this, "scan_result", result);
and it compiles,
however Scan_Result is never called.
I get inside the callback (i tried to put a BA.Log("Test") inside the callback and that function gets called, repeatedly

(All code was copy/pasted, just inside a class instead of the starter service)
 
Upvote 0

ema01

Member
Licensed User
Longtime User
You're right :)

So: created a new application.
This in Main
B4X:
#Region  Project Attributes 
    #ApplicationLabel: B4A Example
    #VersionCode: 1
    #VersionName: 
    'SupportedOrientations possible values: unspecified, landscape or portrait.
    #SupportedOrientations: unspecified
    #CanInstallToExternalStorage: False
#End Region

#Region  Activity Attributes 
    #FullScreen: False
    #IncludeTitle: True
#End Region

Sub Process_Globals
    'These global variables will be declared once when the application starts.
    'These variables can be accessed from all modules.
    Private xui As XUI
End Sub

Sub Globals
    'These global variables will be redeclared each time the activity is created.
End Sub

Sub Activity_Create(FirstTime As Boolean)
    Activity.LoadLayout("Layout")
    
    Wait For (GetPermission) complete (Result As Boolean)
    If Result = False Then
        ExitApplication
    End If
End Sub

Sub Activity_Resume

End Sub

Sub Activity_Pause (UserClosed As Boolean)

End Sub

Private Sub GetPermission As ResumableSub
    If HasPermission = False Then
        'Android requires an explanation for location permission use
        For Each permission As String In PermissionList
            Starter.rp.CheckAndRequest(permission)
            Wait For Activity_PermissionResult (permission As String, result As Boolean)
            If result = False Then Return False
        Next
    End If
    
    Return True
End Sub
Private Sub HasPermission As Boolean
    For Each permission As String In PermissionList
        If Starter.rp.Check(permission) = False Then Return False
    Next
    Return True
End Sub
Private Sub PermissionList As String()
    Dim p As Phone
    If p.SdkVersion < 31 Then
        Return Array As String(Starter.rp.PERMISSION_ACCESS_FINE_LOCATION)
    Else If p.SdkVersion < 34 Then
        'B4A doesn't have the strings implemented
        Return Array As String (Starter.rp.PERMISSION_ACCESS_FINE_LOCATION, "android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT")
    Else
        'B4A doesn't have the strings implemented
        Return Array As String (Starter.rp.PERMISSION_ACCESS_FINE_LOCATION, "android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT")
    End If
End Sub

Sub Button1_Click
    'xui.MsgboxAsync("Hello world!", "B4X")
    Starter.StartScan
End Sub

Public Sub StateChanged
    Log("Ble Manager State Changed")
End Sub

Public Sub DataAvailable (ServiceId As String, Characteristics As Map)
    Log("Ble Data Available")
End Sub

This in Starter
B4X:
#Region  Service Attributes
   #StartAtBoot: False
   #ExcludeFromLibrary: True
#End Region

Sub Process_Globals
    Public manager As BleManager2
    Public currentStateText As String = "UNKNOWN"
    Public currentState As Int
    Public connected As Boolean = False
    Public ConnectedName As String
    Private ConnectedServices As List
    Public rp As RuntimePermissions
    Private ManagerJO As JavaObject
    Private Adapter As JavaObject
    Private Scanner As JavaObject
    Private ScanCallback As JavaObject
End Sub

Sub Service_Create
    manager.Initialize("manager")
    ManagerJO = manager
    Adapter = ManagerJO.GetField("blueAdapter")
    Scanner = Adapter.RunMethod("getBluetoothLeScanner", Null)
    ScanCallback.InitializeNewInstance(Application.PackageName & ".starter$MyScanCallback", Null)
   
End Sub

Sub Service_Start (StartingIntent As Intent)

End Sub

Public Sub ReadData
    For Each s As String In ConnectedServices
        manager.ReadData(s)
    Next
End Sub

Public Sub Disconnect
    manager.Disconnect
    Manager_Disconnected
End Sub

Sub Manager_StateChanged (State As Int)
    Select State
        Case manager.STATE_POWERED_OFF
            currentStateText = "POWERED OFF"
        Case manager.STATE_POWERED_ON
            currentStateText = "POWERED ON"
        Case manager.STATE_UNSUPPORTED
            currentStateText = "UNSUPPORTED"
    End Select
    currentState = State
    CallSub(Main, "StateChanged")
End Sub

Sub Manager_DeviceFound (Name As String, Id As String, AdvertisingData As Map, RSSI As Double)
    Log("Found: " & Name & ", " & Id & ", RSSI = " & RSSI & ", " & AdvertisingData)
    ConnectedName = Name
    StopScanWithLeScanner
    manager.Connect2(Id, False) 'disabling auto connect can make the connection quicker
End Sub

Public Sub StartScan
    If manager.State <> manager.STATE_POWERED_ON Then
        Log("Not powered on.")
    Else If rp.Check(rp.PERMISSION_ACCESS_COARSE_LOCATION) = False Then
        Log("No location permission.")
    Else
        ScanWithLeScanner
    End If
End Sub


Private Sub Scan_Result (Result As Object)
    Log("Scan_Result")
    Dim ScanResult As JavaObject = Result
    Dim device As JavaObject = ScanResult.RunMethod("getDevice", Null)
    Dim address As String = device.RunMethod("getAddress", Null)
    ManagerJO.GetFieldJO("devices").RunMethod("put", Array(address, device))
    Dim name As String
    Dim o As Object = device.RunMethod("getName", Null)
    If o = Null Then name = "" Else name = o
    Dim ScanRecord As JavaObject = ScanResult.RunMethod("getScanRecord", Null)
    Log(ScanRecord) 'you can extend the code to access the data.
    'https://developer.android.com/reference/android/bluetooth/le/ScanRecord
    Dim rssi As Double = ScanResult.RunMethod("getRssi", Null)
    Manager_DeviceFound(name, address, CreateMap(), rssi)
End Sub

Private Sub ScanWithLeScanner
    Dim ScanSettingsStatic As JavaObject
    ScanSettingsStatic.InitializeStatic("android.bluetooth.le.ScanSettings")
    Dim ScanSettingsBuilder As JavaObject
    ScanSettingsBuilder.InitializeNewInstance("android.bluetooth.le.ScanSettings.Builder", Null)
    'https://developer.android.com/reference/android/bluetooth/le/ScanSettings.Builder
    ScanSettingsBuilder.RunMethod("setScanMode", Array(ScanSettingsStatic.GetField("SCAN_MODE_LOW_LATENCY")))
    
    
    Dim ScanFilterStatic As JavaObject
    ScanFilterStatic.InitializeStatic("android.bluetooth.le.ScanFilter")
    Dim ScanFilterBuilder As JavaObject
    ScanFilterBuilder.InitializeNewInstance("android.bluetooth.le.ScanFilter.Builder", Null)
    
    'Test with name <<<<---- ok!!!!
'    ScanFilterBuilder.RunMethod("setDeviceName", Array ("DeviceName"))

    Dim ParcelUuidStatic As JavaObject
    ParcelUuidStatic.InitializeStatic("android.os.ParcelUuid")
    Dim ParcelUuid As JavaObject
    ParcelUuid = ParcelUuidStatic.RunMethod("fromString", Array(GetUUID("ffe0")))
    Dim ParcelMask As JavaObject
    ParcelMask = ParcelUuidStatic.RunMethod("fromString", Array(GetUUIDMask))
    
    ScanFilterBuilder.RunMethod("setServiceUuid", Array (ParcelUuid, ParcelMask))

    Dim Filters As List = Array(ScanFilterBuilder.RunMethod("build", Null))
   
    Scanner.RunMethod("startScan", Array(Filters, ScanSettingsBuilder.RunMethod("build", Null), ScanCallback))
End Sub

Private Sub StopScanWithLeScanner
    Scanner.RunMethod("stopScan", Array(ScanCallback))
End Sub

Sub Manager_DataAvailable (ServiceId As String, Characteristics As Map)
    CallSub3(Main, "DataAvailable", ServiceId, Characteristics)
End Sub

Sub Manager_Disconnected
    Log("Disconnected")
    connected = False
    CallSub(Main, "StateChanged")
End Sub

Sub Manager_Connected (services As List)
    Log("Connected")
    connected = True
    ConnectedServices = services
    CallSub(Main, "StateChanged")
End Sub

'Return true to allow the OS default exceptions handler to handle the uncaught exception.
Sub Application_Error (Error As Exception, StackTrace As String) As Boolean
    Return True
End Sub

Sub Service_Destroy

End Sub

#if Java
import android.bluetooth.le.*;
public static class MyScanCallback extends ScanCallback {
 public void onScanResult(int callbackType, ScanResult result) {
         processBA.raiseEvent(this, "scan_result", result);
    }

   

    /**
     * Callback when scan could not be started.
     *
     * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure.
     */
    public void onScanFailed(int errorCode) {
       BA.Log("Error: " + errorCode);
    }
}
#End If

Private Sub GetUUID(uuid As String) As String
    Return $"0000${uuid.ToLowerCase}-0000-1000-8000-00805f9b34fb"$
End Sub
Private Sub GetUUIDMask As String
    Return "FFFF0000-FFFF-FFFF-FFFF-FFFFFFFFFFFF"
End Sub

and these permissions added in the manifst
B4X:
AddPermission(android.permission.ACCESS_FINE_LOCATION)
'Required for SDK31 onwards
AddPermission(android.permission.BLUETOOTH_SCAN)
AddPermission(android.permission.BLUETOOTH_CONNECT)

I can confirm that scan works in the background AND only the required UUIDs are picked up
 
Upvote 0

ema01

Member
Licensed User
Longtime User
Start with the old example and put it in the starter service. If you find that it can actually help in this case then I'll explain how to make it work in a class.
Bumping to see if there is any news!

EDIT: For the time being i've moved on as in my HM1X class (abstract HM11 and clones/derivatives) declares and controls the manager, and an instance lives inside a service.
The service implements StartScan/StopScan and gathers the data which is then passed to the faux _DeviceFound

by the way: in device found i do the actual filtering by UUID, in order to mantain the same API i have to create a MAP with the advertising data, i only care about key (2) which contains the service id, two bytes so Scan_Result becomes this
B4X:
Private Sub Scan_Result (Result As Object)
    Dim ScanResult As JavaObject = Result
    Dim device As JavaObject = ScanResult.RunMethod("getDevice", Null)
    Dim address As String = device.RunMethod("getAddress", Null)
    mManagerJO.GetFieldJO("devices").RunMethod("put", Array(address, device))
    Dim name As String
    Dim o As Object = device.RunMethod("getName", Null)
    If o = Null Then name = "" Else name = o
    Dim ScanRecord As JavaObject = ScanResult.RunMethod("getScanRecord", Null)
    'https://developer.android.com/reference/android/bluetooth/le/ScanRecord
    Dim rssi As Double = ScanResult.RunMethod("getRssi", Null)
    Dim uuidList As List = ScanRecord.RunMethod("getServiceUuids", Null)
    Dim uuid As String = uuidList.Get(0).As(JavaObject).RunMethod("toString", Null).As(String).SubString2(4,8)
    Dim bc As ByteConverter
    hm11.mManager_DeviceFound(name, address, CreateMap(2:Array As Byte(bc.HexToBytes(uuid)(1), bc.HexToBytes(uuid)(0))), rssi)
End Sub

All seems to be working well, though i would really keep everything inside the class
 
Last edited:
Upvote 0
Top