Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report all Advertisements #20

Open
Fungusware opened this issue Mar 17, 2021 · 13 comments
Open

Report all Advertisements #20

Fungusware opened this issue Mar 17, 2021 · 13 comments

Comments

@Fungusware
Copy link

Does anyone have an idea how to listen to ALL advertisements from all devices, not just the first time it is discovered?

@wazzamatazz
Copy link

@Fungusware I encountered this same situation. The solution is to call WatchPropertiesAsync on each device you want to monitor to register a callback that will be invoked when any of the device properties (including manufacturer data) change from one broadcast to the next. The callback receives information about which properties have changed and what their new values are.

using (var adapter = await BlueZManager.GetAdapterAsync("hci0")) {
  adapter.DeviceFound += async (sender, args) => {
    var device = args.Device;
    var props = await device.GetAllAsync();

    // Do whatever you need to do with the device properties.
    DoStuffWithProps(device, props);

    // Create a watcher so that you can be notified when the device's properties change.
    var watcher = await device.WatchPropertiesAsync(changes => {
      // Call a method that will update your existing properties object with the changes.
      UpdateDeviceProperties(props, changes);
      // Do whatever you need to do with the updated device properties.
      DoStuffWithProps(device, props);
    });

    // TODO: watcher is an IDisposable, so keep it somewhere so that it can be disposed when you cancel discovery!
  };

  // Start discovery.
  await adapter.StartDiscoveryAsync();
  try {
    // Wait until discovery is cancelled.
    await Task.Delay(-1, someCancellationToken);
  }
  finally {
    // TODO: dispose of any watchers that have been created.
  }
}

@Fungusware
Copy link
Author

Special thanks!

This has solved my issue, I'm checking stability as I had been getting crashes after a few minutes with my previous method. Will update once the tests have completed.

@wazzamatazz
Copy link

Something to consider as well: I don't know if it is at all possible for the DeviceFound event to be raised more than once for a given device, but just in case you might want to use an IDictionary<string, IDisposable> to hold the watcher IDisposable instances, and only create a new watcher if there is not already an existing watcher for the discovered device's address.

@Fungusware
Copy link
Author

Indeed it is possible, I dealt with it as mentioned.

One thing I noticed, is that after a few hours of running, the change notification stop coming through and I had to restart my console app host. I'm guessing just restarting the listener would be enough. I'll try that out.

@wazzamatazz
Copy link

Yeah I saw the same thing - I get some sort of DBus exception about too many connections. My workaround has just been to run the app as a service with systemd and have it restart automatically when it crashes 🤷

@walkerlaron
Copy link

I think another solution would be to set the correct property in the await adapter.SetDiscoveryFilterAsync(properties) function. According to the API https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt#n107, you can set this parameter.

bool Discoverable (Default: false)

			Make adapter discoverable while discovering,
			if the adapter is already discoverable setting
			this filter won't do anything.

I'm looking now to see how to pass these properties in without causing an error.

I've had success with a node.js library that does this. So i'm sure it works.

@walkerlaron
Copy link

walkerlaron commented Jun 20, 2021

Does anyone know how to properly format and add properties that are passed into the Start adapter.SetDiscoveryFilterAsync(Properties) method? Here is a post showing the api implemented in C. I'm not sure how to port it C#. https://stackoverflow.com/questions/50675797/bluez-d-bus-c-application-ble

Data is passed in via a dictionary, but i'm not sure how the object should be formed.
IDictionary<string, object> Properties = new Dictionary<string, object>();

Since i couldn't figure this out, I just removed the device on add each time and that works using adapter.RemoveDeviceAsync. since the device is removed, the adapter has to rescan.

using (await adapter.WatchDevicesAddedAsync(async device =>
{
newDevices++;
// Write a message when we detect new devices during the scan.
tring deviceDescription = await GetDeviceDescriptionAsync(device);
Console.WriteLine($"[NEW] {deviceDescription}");
await adapter.RemoveDeviceAsync(device.ObjectPath);
}))

I did the same here

using (await adapter.WatchDevicesAddedAsync(async device =>
{
newDevices++;
// Write a message when we detect new devices during the scan.
string deviceDescription = await GetDeviceDescriptionAsync(device);
Console.WriteLine($"[NEW] {deviceDescription}");
await adapter.RemoveDeviceAsync(device.ObjectPath);
}))

Now, I get every advertising packet when it posts.

@Fungusware
Copy link
Author

@walkerlaron would you be able to post the full source code (or at least more of it), I cannot get any results using this method.

@walkerlaron
Copy link

e

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HashtagChris.DotNetBlueZ;
using HashtagChris.DotNetBlueZ.Extensions;

namespace Scan
{
class Program
{
static TimeSpan timeout = TimeSpan.FromSeconds(15);

    static async Task Main(string[] args)
    {
        //if (args.Length < 1 || args.Length > 2 || args[0].ToLowerInvariant() == "-h" || !int.TryParse(args[0], out int scanSeconds))
        //{
        //    Console.WriteLine("Usage: scan <SecondsToScan> [adapterName]");
        //    Console.WriteLine("Example: scan 15 hci0");
        //    return;
        //}
        


       

        int scanSeconds = 60 * 3;  //3 mins

        IAdapter1 adapter;
        if (args.Length > 1)
        {
            adapter = await BlueZManager.GetAdapterAsync(args[1]);
        }
        else
        {
            var adapters = await BlueZManager.GetAdaptersAsync();
            if (adapters.Count == 0)
            {
                throw new Exception("No Bluetooth adapters found.");
            }

            adapter = adapters.First();
        }

        var adapterPath = adapter.ObjectPath.ToString();
        var adapterName = adapterPath.Substring(adapterPath.LastIndexOf("/") + 1);
        Console.WriteLine($"Using Bluetooth adapter {adapterName}");

        // Print out the devices we already know about.
        var devices = await adapter.GetDevicesAsync();

        foreach (var device in devices)
        {
            string deviceDescription = await GetDeviceDescriptionAsync(device);

            if (deviceDescription != "")
            {
                Console.WriteLine($"{deviceDescription}");
            }
            //   Console.WriteLine(deviceDescription);
            await adapter.RemoveDeviceAsync(device.ObjectPath);
        }
        Console.WriteLine($"{devices.Count} device(s) found ahead of scan.");

     


        int newDevices = 0;


        using (await adapter.WatchDevicesAddedAsync(async device =>
        {
            newDevices++;
            // Write a message when we detect new devices during the scan.
            string deviceDescription = await GetDeviceDescriptionAsync(device);
            if (deviceDescription != "")
            {
                Console.WriteLine($"[NEW] {deviceDescription}");
            }
            await adapter.RemoveDeviceAsync(device.ObjectPath);
        }))
        {
            
                Console.WriteLine($"Start discovery for {scanSeconds} seconds...");
                await adapter.StartDiscoveryAsync();
                await Task.Delay(TimeSpan.FromSeconds(scanSeconds));
                Console.WriteLine($"Stop Discovery");                  
                await adapter.StopDiscoveryAsync();
           
           

        }
        Console.WriteLine($"Scan complete. {newDevices} new device(s) found.");

    }

    private static async Task<string> GetDeviceDescriptionAsync(IDevice1 device)
    {
        var deviceProperties = await device.GetAllAsync();


        byte[] mdata = (byte[])deviceProperties.ManufacturerData[deviceProperties.ManufacturerData.Keys.First()];

    
         return $"{deviceProperties.Alias} (Address1: {deviceProperties.Address}, RSSI: {deviceProperties.RSSI}}";

    }
}

}

@Fungusware
Copy link
Author

Yeah still failing with the exception.

Connection ":1.18" is not allowed to add more match rules (increase limits in configuration file if required; max_match_rules_per_connection=512)

@Fungusware
Copy link
Author

Fungusware commented Jun 24, 2021

I managed to pass some Discovery Filters using the following :

                var props = new Dictionary<string, object>();
                props.Add("Transport", "le");
                props.Add("UUIDs", new string[] { "0000fe95-0000-1000-8000-00805f9b34fb" });
                await adapter.SetDiscoveryFilterAsync(props); 

These options worked nicely and reduced the amount non-useful advertiements I was recieving.

I haven't tried all of the properties yet though.

@walkerlaron
Copy link

Odd, i tried similar code before and received errors. I tried this again today with your code and didn't get errors. So there must be something that I'm missing from my old code version. I didn't see where the DuplicateData Tag worked, but i'll debug a bit more later today when I have time. I'm not getting the error you posted regardding max connections. I've been runnning for several days now with a version of the snippet i posted earlier the only change i have is...

while (true)
{

                Console.WriteLine($"Start discovery for {scanSeconds} seconds...");
                await adapter.StartDiscoveryAsync();
                await Task.Delay(TimeSpan.FromSeconds(scanSeconds));
                Console.WriteLine($"Stop Discovery (save latest value to sql DB here)  Restart at {DateTime.Now.AddSeconds(sleepSeconds).ToString("yyyy-MM-dd hh:mm:ss")} ");
                await adapter.StopDiscoveryAsync();
                await Task.Delay(TimeSpan.FromSeconds(sleepSeconds));
            }

so I scan for three minutes, stop scanning, sleep for 10 minutes and start over. In my case, working on a Beacon like use case for my device.

@aolszowka
Copy link

I know this is an older post but I wanted to chime in to say that this worked for me.

I think the proper way to do this though is to use the experimental AdvertisementMonitor API (https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/advertisement-monitor-api.txt) however this is not exposed in this library and I can't seem to get it working on my RBPi4B under Ubuntu (uname -a - Linux rbpi4b 5.15.0-1025-raspi #27-Ubuntu SMP PREEMPT Thu Feb 16 17:09:55 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux; bluetoothctl -v - bluetoothctl: 5.64). Even with the --experimental flag on the service; I see the AdvertisementMonitorManager1 but no AdvertisementMonitor1 which I believe I need in conjunction.

$ dotnet dbus list objects --bus system --service org.bluez
/ : org.freedesktop.DBus.ObjectManager
/org/bluez : org.bluez.AgentManager1 org.bluez.ProfileManager1
/org/bluez/hci0 : org.bluez.Adapter1 org.bluez.AdvertisementMonitorManager1 org.bluez.BatteryProviderManager1 org.bluez.GattManager1 org.bluez.LEAdvertisingManager1 org.bluez.Media1 org.bluez.NetworkServer1

I'll keep hacking away at it on my end. I am using a branch for which I've upgraded this library (hashtagchris/DotNet-BlueZ) to the latest version of Tmds (0.14.0).

I am trying to write a .NET Version of ble_monitor to allow me to ingest the events of a ThermPro TP357 (Cheap Bluetooth Enabled Temperature and Humidity Monitor [Hygrometer] ~$8 on AliExpress when bought in quantity) into an InfluxDB instance I have running.

This sensor works by exporting the Temperature and Humidity as ManufacturerData in an advertisement. This issue on the ble_monitor project page was invaluable: custom-components/ble_monitor#961

Here's a copy of BT Snoop:

@ MGMT Event: Device Found (0x0012) plen 53            {0x0001} [hci0] 41.615913
        LE Address: BC:C7:DA:6D:8D:12 (OUI BC-C7-DA)
        RSSI: -72 dBm (0xb8)
        Flags: 0x00000000
        Data length: 39
        Name (complete): TP357 (8D12)
        Flags: 0x05
          LE Limited Discoverable Mode
          BR/EDR Not Supported
        Company: not assigned (51650)
          Data: 0021022c
        Name (complete): TP357 (8D12)

Its a bit confusing because apparently btsnoop wants to assume that the first 16-bytes of the ManufacturerData are the company. But the TP357 uses these bits to have at least part of the temperature.

If you convert the "Company" into Hex you get c9c2, you need to flip these around due to endianness (https://en.wikipedia.org/wiki/Endianness?useskin=vector) and combine it with the remaining data to give you:

c2c90021022c

The string can be decoded in the following way:

Unknown Temperature Humidity Unknown
c2 c900 21 022c

The Unknown values have not changed since I've started monitoring this; and the linked blue_monitor issue didn't seem to have an idea what they were for either.

The Temperature is a 16bit Little-endian value that contains the current reading multiplied by 10 in C; so the above example:

Step Result
Raw Value c900
Correct for LE 00c9
Convert To Decimal 201
Divide By 10 20.1

The Humidity % is represented with 8bits above and can be simply converted:

0x21 -> 33%

In a perfect world I'd love to have the library just take the MAC Address as an argument and spit me out the raw Advertisement Data (ideally filtered down to just ManufacturerData). I would then pipe this information into a post processing pipeline to decode. This is more or less how ble_monitor works (but in Python). My intent would be to then take that decoded processing pipeline and then ingest it into the InfluxDB.

Historically many of these applications (including ble_monitor) used hcidump but reading online it seems like they really want you to use the DBus, hence why I'm writing this program.

Here's what I've got so far and am continuing to hack on it:

using Tmds.DBus;
using HashtagChris.DotNetBlueZ;

namespace dotnet_ble_monitor
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using (var adapter = (await BlueZManager.GetAdaptersAsync()).FirstOrDefault())
            {
                try
                {
                    Console.WriteLine("Bluez DBUS Examples");
                    adapter.DeviceFound += adapter_DeviceFoundAsync;

                    var props = new Dictionary<string, object>();
                    props.Add("Pattern", "BC:C7:DA:6D:8D:12");
                    await adapter.SetDiscoveryFilterAsync(props);
                    await adapter.StartDiscoveryAsync();
                    Console.WriteLine("Waiting for events. Use Control-C to quit.");
                    Console.WriteLine();
                    await Task.Delay(-1);
                }
                catch (Exception ex)
                {
                    Console.Error.WriteLine(ex);
                }
                finally
                {
                    await adapter.StopDiscoveryAsync();
                }
            }
        }

        private static async Task adapter_DeviceFoundAsync(Adapter sender, DeviceFoundEventArgs eventArgs)
        {
            try
            {
                Console.WriteLine("A device was found!");
                var deviceProperties = await eventArgs.Device.GetAllAsync();
                Console.WriteLine($"{deviceProperties.Name} ({deviceProperties.Address}) was the device");
                var watcher = eventArgs.Device.WatchPropertiesAsync(device_PropertiesChangedAsync);
                return;
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }
        }

        private static void device_PropertiesChangedAsync(PropertyChanges obj)
        {
            Console.WriteLine($"A Data Change was Encountered");
            Dictionary<System.UInt16, System.Object> manufacturerData = (Dictionary<System.UInt16, System.Object>)obj.Changed.Where(kvp => kvp.Key.Equals("ManufacturerData")).First().Value;
            foreach (var kvp in manufacturerData)
            {
                Console.WriteLine($"Manufacturer Data Changed: {kvp.Key}, {kvp.Value}");
            }
        }
    }
}

I've noticed that behind the scenes Discovery stops after awhile (I believe 60 seconds). Is the intent really to have it continue to try and perform discovery? Is there a way to continue to monitor without having to continually re-enable discovery?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants