spacecheese / bluez_peripheral

A library for building BLE peripherals using GATT and bluez
MIT License
42 stars 8 forks source link

Connection from multiple apps #31

Open targettadams opened 1 year ago

targettadams commented 1 year ago

I've had great fun with this library so far. Compared to other libraries I've used, it is super simple to create a peripheral and works very reliably.

I've come across some behaviour which I just wanted to run past you, just in case there is an issue in the library.

I have a peripheral written using bluez-peripheral. It is a scoreboard which displays numbers sent to it from either A) a third-party (natively written) scoring app or B) a PWA which I've written that utilizes Chrome's Web Bluetooth API.

Ideally, I wanted to be able to connect both apps at the same time to the device (my app provides some complimentary functionality). My understanding of BLE is that this is not possible. (Although, I don't quite understand how multiple audio-related apps can then connect to a bluetooth speaker, unless bluetooth classic works differently or some other solution is employed in those cases).

From my testing, I initially confirmed that it was not possible to connect both apps simultaneously. When I have connected app A, say, app B does not list the device in its device search. If I disconnect app A, the device comes up straight away in B's device search. And vice versa.

Fine.

But then I found a way of 'hacking' things to allow both devices to be connected. If I initiate app A's device search, I see the device listed. HOWEVER - before I actually connect to it, I switch over to app B, search for the device, and connect to it. With app B connected, I go back to the app A device search window, which still shows the device as available, and I can connect to it. Bingo - I have two apps connected simultaneously.

Apologies if that was convoluted, and I hope it made sense. Is this indeed 'hacky' behaviour, or should two apps be able to simultaneously connect to one bluetooth peripheral, and if so, is there an issue in the library preventing this?

spacecheese commented 1 year ago

I believe this should be legal for Bluetooth v4.1+ and assuming your device supports it. From your description it sounds like the central is removing the advert on connection which I think makes sense as a default behaviour but possibly I could modify the advertising code to make this behaviour more intuitive for multi connection scenarios like this.

For now if you're able to use the master branch I recently merged #22 which adds a Advertisement.__init__.releaseCallback that should allow you register to advert again once it's been removed by the connection. I think possibly there might be some way of polling the advert but I'm not completely sure.

targettadams commented 1 year ago

Thanks. So, I grabbed the code. I put a print statement in the Release method of the Advertisement class, but it never seems to be invoked, and so the callback never gets invoked ...

spacecheese commented 1 year ago

Ah sorry I guessed wrong. Does btmon tell you anything about the advert while you do the connection.

targettadams commented 1 year ago

I've got a steep learning curve ahead of me understanding this output ... So here is the output of btmon which covers the following steps: 1) start running peripheral code 2) connect device. I can indeed see both the 'Add Advertising' command and 'Remove Advertising' command being called. btmon.dump.log

targettadams commented 1 year ago

Actually - scratch that. Those commands to Add and Remove Advertising are present even if I don't connect the device.

targettadams commented 1 year ago

The answer to this (and the link) might be relevant: https://stackoverflow.com/questions/56236749/continue-advertising-after-connection-bluez

targettadams commented 1 year ago

I think I've come to the conclusion that what I am trying to do here is is just not possible. The fundamental issue seems that once a peripheral is in the connected state, it no longer advertises. I don't think it is impossible (as the 'hacky' behaviour I documented in my original question illustrates) for two apps to share a connection. But the issue is - once one app has initiated the connection, which triggers the peripheral to stop advertising, how does the other app get the device information it requires to access the connection if there are no advertisements. If I had authored both apps, and, likely, was not using the Web Bluetooth API (which feels quite restrictive) perhaps I could have done something ... but then I wouldn't have had this use case (as I would just create one app that did everything I required rather than having to write an additional app to supplement the incomplete functionality of a 3rd party one!).

spacecheese commented 1 year ago

Apologies that I haven't had much time to look into this recently. I'll try and do some testing later today.

I think that this stack overflow answer is wrong. The spec doesn't actually say that a device can't advertise and connect at the same time in fact it says "any combination of states and roles may be supported" (across multiple state machines). I think the confusion here is that a single device may support multiple state machines each of which can only be in a single state.

In particular, the Link Layer may be in the Connection State multiple times with any mix of Central Role and Peripheral Role. (v5.4 Vol 6, Part B, Section 1.1.1)

The Link Layer in the Peripheral Role will communicate with a single device in the Central Role. (v5.4 Vol 6, Part B, Section 1.1)

In fairness the spec does seem to occasionally conflate the concept of a link layer state machine instance and the link layer in general. Since you're able to connect multiple Centrals to your Peripheral I think your device must implement multiple state machines though unfortunately there doesn't seem to be anything specific in the bluez documentation about multiple connections.

I believe there is a command in btmgmt that allows you to turn advertising back on (something like advertisement on but unfortunately I don't have access to a system I can test on right now) though I'm not sure if this will work in this instance. As an aside could you confirm that the releaseCallback is invoked if your advert times out? I haven't yet added unit tests for this feature so it's possible its broken somewhere along the way.

targettadams commented 1 year ago

No need to apologise. Once I'm up to speed, I can probably help out a bit.

So ... yes, if I put a timeout (60 secs, say), I do indeed see the Release method called. In bluetoothctl I can see the number of active (advertising) instances go from 1 to 0 when it stops advertising. However, although issuing the command advertise on in bluetoothctl raises the number of active instances back to 1, I cannot see the advert in my devices.

targettadams commented 1 year ago

Ok. Sorry - got that wrong. This stupid Web Bluetooth API I'm using is leading me astray.

Re the above test, I can indeed see the adverts again, once I do advertise on following a timeout (as long as I'm not using my web app which uses said API).

Ok - this is good. I see my controller has 4 supported instances. It looks like I have to kick off 2 instances for my purposes.

Funny enough, Web Bluetooth API will ONLY show the adverts if there are no other connected instances. That's outside the current scope though. Could be a security thing. Once I have two instances, I just need to make sure I connect the webapp first.

targettadams commented 1 year ago

So this is what I did:

    # Start 2 instances of an advert that will last forever.
    advert = Advertisement("CricNet Scoreboard", ["5a0d6a15-b664-4304-8530-3a0ec53e5bc1"], 0x0140, 0)
    await advert.register(bus, adapter)
    await advert.register(bus, adapter)

i.e. register the advert multiple times. And the good news is that when I kick-off the instances from my code (instead of doing one in code, one in bluetoothctl as in the above test), even the Web Bluetooth API behaves itself. So problem solved.

Do you think we should encapsulate this ability to register multiple instances in the register method itself? I can have a go if you think that's sensible.

One remaining thing to report, re the invocation of the releaseCallback. So, as reported, this is indeed fired when you put a timeout on your advert. But it is not being fired when a connection is made. When a connection is made, it seems that the number of active advertising instances does not change (I can't see any advertising related output in bluetoothctl when a connection is made). But when the advert times out, the active instances are actually decremented; it is this decrementation which seems to invoke the Release method on the Advertisement class.