bluer icon indicating copy to clipboard operation
bluer copied to clipboard

Add support for passive BLE scanning

Open Redrield opened this issue 2 years ago • 9 comments

I'm trying to use bluer on a raspberry pi to scan for advertising BLE devices. In doing some research, I'm fairly certain that bluer only supports active (ie. scan request) discovery, and that there is code in BlueZ to support passive scanning (used in hcitool lescan, for example), but there isn't anything in bluer to give access to passive scanning.

Redrield avatar Mar 11 '22 23:03 Redrield

This would probably be provided by the advertisement monitoring api which is indeed not yet implemented in BlueR.

surban avatar Mar 11 '22 23:03 surban

I'm working on this for a few days now, it's not working yet. It should, but bluez is not calling the callback functions.

Maybe something with bluez?

I compiled the latest version manually since monitoring API is something new.

I forked bluer: https://github.com/otaviojr/bluer/tree/monitor

The code is simple:

callbacks:

    pub fn activate_fn() -> Pin<Box<dyn Future<Output = bluer::monitor::ReqResult<()>> + Send>> {
        println!("Activate funcion called(1)");
        Box::pin(async {
            println!("Activate funcion called(2)");
            Ok(())
        })
    }

    pub fn release_fn() -> Pin<Box<dyn Future<Output = bluer::monitor::ReqResult<()>> + Send>> {
        println!("Release funcion called(1)");
        Box::pin(async {
            println!("Release funcion called(2)");
            Ok(())
        })
    }

    pub fn device_found_fn(device: DeviceFound) -> Pin<Box<dyn Future<Output = bluer::monitor::ReqResult<()>> + Send>> {
        println!("DeviceFound funcion called(1)");
        Box::pin(async {
            println!("DeviceFound funcion called(2)");
            Ok(())
        })
    }

code:

if let Ok(monitor_handle) = adapter.register_monitor(Monitor {
                        activate: Some(Box::new(BlueZ::activate_fn)),
                        release: Some(Box::new(BlueZ::release_fn)),
                        device_found: Some(Box::new(BlueZ::device_found_fn)),
                        patterns: Some((0x00, 0xff,vec![0xbe,0xac])),
                        ..Default::default()
                    }).await { ...

Now, a few things on bluer may complicate things further.

BlueR changes some filters on discover_devices on the user's behalf, it doesn't allow duplicates, which could be a problem with beacons, and it does not allow us to choose a LE scan only.

I think discover_devices is not necessary with passive scan, but if they work simultaneously, it could be a problem with beacons. I don't know.

And sorry if my Rust code is not state of the art. I'm relatively new to Rust.. just trying to do my best here.

regards, Otávio Ribeiro

otaviojr avatar Sep 22 '22 12:09 otaviojr

Oh, and I'm having a hard time with those or_patterns as well... but I will figure them out eventually.

otaviojr avatar Sep 22 '22 12:09 otaviojr

I also started a prototype a while ago, but forgot to post it here: https://github.com/lopsided98/bluer/compare/master...adv-mon

It works, but currently only supports one monitor at a time. The monitor root can only contain a single monitor right now because dbus-crossroads seems to only support sending added/removed signals for the root ObjectManager, so I can't tell BlueZ when a new monitor was added.

I also found that adjusting any of the RSSI settings broke the monitor (I forget the exact details, but most of the time I would just get no notifications), and I was never able to get the DeviceLost callback to work reliably. These both appear to be BlueZ issues.

Despite these issues, I was to get it working well for my application. To reliably get notifications when a device starts reappears after disappearing for a while, I have to re-register the monitor. My application code can be found here: https://github.com/lopsided98/WaterLevelMonitor/blob/54e8bc2219d0ef33d84bb9c4fb57ed287cb848a3/base_station/src/sensor.rs#L185

lopsided98 avatar Sep 22 '22 19:09 lopsided98

I managed to make it to work. At least receiving the Activate/Release callbacks.

It was the object manager indeed. Once I implemented it on the monitor interface things started to work.

I managed to implement object manager at the monitor path, so, in my version, you can have more than one monitor, as many as bluez allows. At least, in theory, I will make more tests on it. If you want to get a look at it, maybe you could allow more than one monitor on your version as well.

Now, I need to finish the DeviceFound and DeviceLost endpoints and I think it will be good to go.

regards, Otávio Ribeiro

otaviojr avatar Sep 22 '22 22:09 otaviojr

It looks like you are still calling RegisterMonitor and UnregisterMonitor for each monitor, right? If the object manager signals are working, you should be able to register the root once (with RegisterMonitor) and then just add monitors under that root through the object manager.

lopsided98 avatar Sep 22 '22 23:09 lopsided98

I made the last commit with those changes....

I'm implementing the object manager at the root level, but my root level is the uuid. The AdvertisementMonitor1 interface is one level above the uuid...

that allows you to have multiple monitors.

object manager -> /org/bluez/bluer/monitor/4e7adc5601864010bf393486740bbe92 AdvertisementMonitor1 -> /org/bluez/bluer/monitor/4e7adc5601864010bf393486740bbe92/app

    pub(crate) async fn register(self, inner: Arc<SessionInner>, adapter_name: &str) -> Result<MonitorHandle> {
        let manager_path = dbus::Path::new(format!("{}/{}", MANAGER_PATH, adapter_name)).unwrap();
        let uuid = Uuid::new_v4().as_simple().to_string();
        let root = dbus::Path::new(format!("{}/{}",MONITOR_PREFIX,uuid)).unwrap();
        let name = dbus::Path::new(format!("{}/{}/app",MONITOR_PREFIX,uuid)).unwrap();

        log::trace!("Publishing monitor at {}", &name);

        {
            let mut cr = inner.crossroads.lock().await;
            let object_manager_token = cr.object_manager();
            let introspectable_token = cr.introspectable();
            let properties_token = cr.properties();
            cr.insert(root.clone(), [&object_manager_token, &introspectable_token, &properties_token], {});
            cr.insert(name.clone(), [&inner.monitor_token], Arc::new(self));
        }

        log::trace!("Registering monitor at {}", &name);
        let proxy = Proxy::new(SERVICE_NAME, manager_path, TIMEOUT, inner.connection.clone());
        proxy.method_call(MANAGER_INTERFACE, "RegisterMonitor", (root.clone(),)).await?;

        let (drop_tx, drop_rx) = oneshot::channel();
        let unreg_name = root.clone();
        tokio::spawn(async move {
            let _ = drop_rx.await;

            log::trace!("Unregistering monitor at {}", &unreg_name);
            let _: std::result::Result<(), dbus::Error> =
                proxy.method_call(MANAGER_INTERFACE, "UnregisterMonitor", (unreg_name.clone(),)).await;

            log::trace!("Unpublishing monitor at {}", &unreg_name);
            let mut cr = inner.crossroads.lock().await;
            let _: Option<Self> = cr.remove(&unreg_name);
        });

        Ok(MonitorHandle { name, _drop_tx: drop_tx })
    }

regards, Otávio Ribeiro

otaviojr avatar Sep 23 '22 01:09 otaviojr

Yeah, I guess that works, but it doesn't seem to be the how the advertisement monitor API is intended to be used. The documentation implies that you are supposed to create a single root object manager per application and then create multiple monitors underneath that, using the object manager signals to tell BlueZ that they were added/removed.

lopsided98 avatar Sep 23 '22 02:09 lopsided98

You are right, and you made me see things from a different perspective.

Looking at the API documentation, we must register the root endpoint once and add the AdvertisementMonitor1 interface underneath it multiple times.

Instead, we are adding the root endpoint for every monitor we register.

And this is also the problem with the crossroad. Every time we add the same path again, it replaces the old one and loses the previous child structure. In the end, it will have only the last monitor as a child, and the previous ones will stop working.

With that in mind, I made another proposal, one where we register the monitor once and then add as many rules as we want.

I created a new branch for it: https://github.com/otaviojr/bluer/tree/monitor1

And here is the code I used to try it out.

if let Ok(mut monitor_handle) = adapter.register_monitor().await {
    monitor_handle.add_monitor(Monitor {
        activate: Some(Box::new(move || {
            Box::pin(async {
                println!("Monitor 1: Activate funcion called");
                Ok(())
            })
        })),
        release: Some(Box::new(move || {
            Box::pin(async {
                println!("Monitor 1: Release funcion called");
                Ok(())
            })
        })),
        device_found: Some(Box::new(move |device| {
            Box::pin(async move {
                println!("Monitor 1: DeviceFound funcion called: {}",device.addr);
                Ok(())
            })
        })),
        patterns: Some(vec!(Pattern {
            start_position: 2,
            ad_data_type: 0xff,
            content_of_pattern: vec!(190, 172, 57, 237, 152, 255, 41, 0, 68, 26, 128, 47, 156, 57, 143, 193, 153, 210, 0, 1, 0, 100, 197, 100)
        })),
        rssi_low_threshold: Some(127),
        rssi_high_threshold: Some(127),
        rssi_low_timeout: Some(0),
        rssi_high_timeout: Some(0),
        ..Default::default()
    }).await;
    monitor_handle.add_monitor(Monitor {
        activate: Some(Box::new( move || {
            Box::pin(async {
                println!("Monitor 2: Activate funcion called");
                Ok(())
            })
        })),
        release: Some(Box::new( move || {
            Box::pin(async {
                println!("Monitor 2: Release funcion called");
                Ok(())
            })
        })),
        device_found: Some(Box::new(move |device| {
            Box::pin(async move {
                println!("Monitor 2: DeviceFound funcion called: {}",device.addr);
                Ok(())
            })
        })),
        patterns: Some(vec!(Pattern {
            start_position: 2,
            ad_data_type: 0xff,
            content_of_pattern: vec!(190, 172, 57, 237, 152, 255, 41, 0, 68, 26, 128, 47, 156, 57, 143, 193, 153, 210, 0, 1, 0, 100, 197, 100)
        })),
        rssi_low_threshold: Some(127),
        rssi_high_threshold: Some(127),
        rssi_low_timeout: Some(0),
        rssi_high_timeout: Some(0),
        ..Default::default()
    }).await;
}

With this code, I have been able to receive events for both monitors.

Monitor 1: DeviceFound funcion called: 60:68:4E:15:18:25
Monitor 2: DeviceFound funcion called: 60:68:4E:15:18:25

And the dbus objects's paths are now following what has been suggested in the API documentation.

PS: I borrowed some pieces of your code, especially the one that handles the or_patterns, which are much better than mine. Thanks for that.

regards, Otávio Ribeiro

otaviojr avatar Sep 23 '22 15:09 otaviojr

Is this issue still open? I'm hoping to use this lib for essentially beacon advertisements, so it seems like this issue might be impactful for me?

hpux735 avatar Mar 13 '23 17:03 hpux735

I believe it is still open.

I am still waiting for feedback.

You could get my branch, update it and use it.

I've been using it for a while now, and it works well.

Regards

otaviojr avatar Mar 13 '23 18:03 otaviojr

@otaviojr I got feedback pretty quickly for my (unrelated) change once I actually made a MR. Would you be willing to make a MR of your branch? If not, I can make an MR from a copy of your branch on my fork, so I can handle the feedback and make appropriate updates.

jhartzell42 avatar Mar 28 '23 14:03 jhartzell42

Could you please send a PR if you would like a review? This makes it easier for everyone.

surban avatar Mar 28 '23 14:03 surban

OK, I made an MR on @otaviojr 's behalf...

jhartzell42 avatar May 10 '23 20:05 jhartzell42

If @surban would be interested, I can move this MR to my own fork and clean up the commit log as well :-)

jhartzell42 avatar May 10 '23 20:05 jhartzell42

OK, I made an MR on @otaviojr 's behalf...

Sorry for the delay, but thanks. If you need something, let me know...

About the commit logs, I developed them on a remote host and used git to sync code... :-(

Can be cleaned

otaviojr avatar May 10 '23 22:05 otaviojr

@jhartzell42 I've cleaned all commit logs at https://github.com/otaviojr/bluer/tree/monitor1 branch

Can you change your pull request?

otaviojr avatar May 11 '23 00:05 otaviojr

I can't retarget it to a different branch, and I can't modify the branch I'm using, as it's a pull request literally from your fork. Could you possibly make your own pull request from monitor1?? If not, I'll delete the one I have, make my own fork, migrate your new branch over, and make a new pull request.

jhartzell42 avatar May 11 '23 00:05 jhartzell42

@jhartzell42 done

otaviojr avatar May 11 '23 00:05 otaviojr

PR merged.

surban avatar Jun 13 '23 12:06 surban