adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.12k stars 1.22k forks source link

BLE API: returning the address for an existing connection #5881

Open ThomasAtBBTF opened 2 years ago

ThomasAtBBTF commented 2 years ago

This request is a result of a support topic in the forum under https://forums.adafruit.com/viewtopic.php?f=60&t=187178 And also relates to the issue: https://github.com/adafruit/circuitpython/issues/4639

To be able to implement a good HID-keyboard solution it is necessary to be able to pair with multiple centrals on the one hand, on the other hand, it does not make sense to send keystrokes to every connected central simultaneously. Managing the connection from the UI of the central is tedious and sometimes impossible.

So there must be a way to understand in the HID-Keyboard code to which centrals the keyboard is currently connected and disconnect from the undesired ones.

When trying to change the central, I think the keyboard shall disconnect from all centrals, then start to advertise and look if the desired central connects. After that, advertising can stop and the connection to all centrals except the desired one can be disconnected.

tannewt commented 2 years ago

BLE also has directed advertising that could be used to request a specific client connect. I think I wired it up in the code but ended up not needing it. So, it may work already.

dhalbert commented 2 years ago

@tannewt I was wondering where you were getting the addresses for the directed advertising.

tannewt commented 2 years ago

I may have had special access because I was internal in C.

ThomasAtBBTF commented 2 years ago

Yesterday it was mentioned / suggested in "Deep Dive w/Scott: #CircuitPython2022" that DeviceInfoService() could be used for identifying the "partner" in a connection. Checking this I did not find a way to obtain the DeviceInfo from a connection. It seems to be only available during the start scan phase in the result of ble.start_scan. A keyboard advertises its services and "gets connected" by one or more centrals. Because it absolutely does not make sense for a keyboard to be connected to more than one central in parallel and send same keystrokes to them, there should be a way to disconnect from all centrals except the one the user wants to focus to! So I think, more information about the central should be available in the Connection class.

tannewt commented 2 years ago

@ThomasAtBBTF What centrals are you using? I want to replicate it here and provide a DeviceInfo example for you.

ThomasAtBBTF commented 2 years ago

I would like to use an iPhone and a iPad and a Mac and a Windows PC from time to time as the user likes to. But mainly iPhones and iPads. And always only connect to one central even if the others are close and running. The peripheral shall be able to decide to whom he wants to talk to. (this seems to be hard or almost impossible) But disconnecting from a connection should be easy (and is working in my code), if I only would know which connection to disconnect.

tannewt commented 2 years ago

Ok thanks. I've got an iPhone that I tested this with.

Example code (will PR to the BLE repo too):

# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
# SPDX-License-Identifier: MIT

"""
This example does a generic connectable advertisement and prints out the
manufacturer and model number of the device(s) that connect to it.
"""

import time
import adafruit_ble
from adafruit_ble.advertising.standard import Advertisement
from adafruit_ble.services.standard.device_info import DeviceInfoService

radio = adafruit_ble.BLERadio()
a = Advertisement()
a.connectable = True
radio.start_advertising(a)

print ("advertising")

while not radio.connected:
    pass

print("connected")

while radio.connected:
    for connection in radio.connections:
        if not connection.paired:
            connection.pair()
            print("paired")
        dis = connection[DeviceInfoService]
        print(dis.manufacturer)
        print(dis.model_number)
    time.sleep(60)

print("disconnected")

Prints:

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
advertising
connected
paired
Apple Inc.
iPhone14,2

So, this will work with different devices but not identical ones. The serial number isn't made available on my iPhone.

tannewt commented 2 years ago

Here is a slightly improved version that sets values the other device can read:

# SPDX-FileCopyrightText: 2022 Scott Shawcroft for Adafruit Industries
# SPDX-License-Identifier: MIT

"""
This example does a generic connectable advertisement and prints out the
manufacturer and model number of the device(s) that connect to it.
"""

import time
import adafruit_ble
from adafruit_ble.advertising.standard import Advertisement
from adafruit_ble.services.standard.device_info import DeviceInfoService

radio = adafruit_ble.BLERadio()
a = Advertisement()
a.connectable = True
radio.start_advertising(a)

# Info that the other device can read about us.
my_info = DeviceInfoService(manufacturer="CircuitPython.org", model_number="1234")

print ("advertising")

while not radio.connected:
    pass

print("connected")

while radio.connected:
    for connection in radio.connections:
        if not connection.paired:
            connection.pair()
            print("paired")
        dis = connection[DeviceInfoService]
        print(dis.manufacturer)
        print(dis.model_number)
    time.sleep(60)

print("disconnected")
ThomasAtBBTF commented 2 years ago

Yes, maybe. But the other device is an IPhone / IPad and IOS connects to a keyboard if the user selects this in the Bluetooth Settings. The use case is: The (Blind) user has one Braille-Keyboard and an IPhone and one IPad he want's to pair his Braille keyboard with his IPhone and his IPad. The user is using Voiceover as the screen reader on both devices to get Speech feedback and maybe a Braille Display to get braille dots from Voiceover. All this works if he also has two Braille Keyboards. But now he wants to hold on only to one braille keyboard and switch by a key press between his two IOS devices. If the manufacturer info and the modelnumber is different enough between the devices he can use this method. But if they are too similar (like a work phone and a private phone) then this method fails. And what if his Windows-PC (PC's) does (do) not show manufacturer and or model info. I think this is what MAC addresses are for......

tannewt commented 2 years ago

My point is that it's possible now.

I'm not arguing we shouldn't add an accessor to the mac address of a connection. I'm arguing why it isn't a priority.

ThomasAtBBTF commented 2 years ago

Ok, I checked out the code and tested it in the CP application for our keyboards and I can use it to decide which one connection to keep and to disconnect the others.

I had hoped, that the .serial_number member of the DeviceInfoservice object would hold the serial_number of the IOS devices, but it didn't.

A quick (and dirty) fix for my problem would be to either put the mac-address string into the serial_number member if the serial number is not available for the device. Or not so quick (and dirty) to add a Mac Address member to the DeviceInfoservice class. I think this would mean very little API change, limited testing requirements, and almost no risk to break any existing code.

What do you think?

ThomasAtBBTF commented 2 years ago

May I "pull up" my desire to get the MAC-Address of an established connection here again?

Is there somewhere a "make a whish site" for features in CP.

Currently, with the example of Scott I can differentiate between different Apple IOS models, but not devices. (If a user has for example two iPhones of the same model, maybe one private and one business phone) Also so far I did not find in any Bluetooth connection to Windows PC a DeviceinfoService.

For me to have a member MAC-address of the paired partner in a connection object is an obvious necessity !

So please reconsider this feature request.

dhalbert commented 2 years ago

When you got the serial number from the DeviceInfoService, was it unique?

I am worried that the address you are going to get from the connection is going to change over time for a particular device. BLE likes to use random generated addresses for security reasons, and iOS is particularly interested in doing that.

ThomasAtBBTF commented 2 years ago

Apple IOS Phones / Pads do not respond with a serial number.

ThomasAtBBTF commented 2 years ago

When the phones are changing their MAC-Addresses, how do the "partners" are finding their pairing information when connecting again. For my purposes also "unique" pairing information of a connection would be as helpful as the MAC-address. I just want to "see who did connect to me" during advertising and disconnect from the connections "I am not interested in for the moment".

dhalbert commented 2 years ago

You mean you get no value for the serial number field, or some unknown (but possibly unique) value?

Pairing uses the IRK stuff, I forget the details. The device wishing to pair sends a key.

I did implement getting the address from a connection in _bleio, though I am having some build troubles for unrelated reasons. I can give you a test build when I get the build working. Which board do you need a build for?

dhalbert commented 2 years ago

And were you not able to use the address in the initial advertisement to identify the connection?

dhalbert commented 2 years ago

And were you not able to use the address in the initial advertisement to identify the connection?

Never mind, it's your code that's advertising, not the iPhone. etc.

dhalbert commented 2 years ago

Here are two builds, Feather nRF52840 and ItsyBitsy nRF52840, that implement _bleio.Connection.address. I did not add a corresponding change in the library yet, but you can get to the address by reaching into the Connection object. See the print statement in the test program below, which I used with the Bluefruit Connect app on an iPhone and an iPad

I did not try this with a pairing connection, and I did not try it waiting more than a few minutes between iPhone connections to see if the address eventually changes (it might change after 15 minutes, just possibly, based on my recollection).

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService

ble = BLERadio()
uart = UARTService()
advertisement = ProvideServicesAdvertisement(uart)

while True:
    ble.start_advertising(advertisement)
    while not ble.connected:
        pass
    print("peer address:", ble.connections[0]._bleio_connection.address)  # Print the address of the peer
    while ble.connected:
        # Returns b'' if nothing was read.
        one_byte = uart.read(1)
        if one_byte:
            print(one_byte)
            uart.write(one_byte)

connection-address-builds.zip

ThomasAtBBTF commented 2 years ago

Hi and thank you for looking into this!

I am using a ItsyBitsy nRF52840.

From this code:

    def connectionid(self, c):
        try:
            if DeviceInfoService in c:
                try:
                    dis = c[DeviceInfoService]
                    s = ""
                    try:
                        s += dis.manufacturer
                    except:
                        s += "-man"
                    try:
                        s += dis.model_number
                    except:
                        s += "-mod"
                    try:
                        s += dis.serial_number
                    except:
                        s += "-#"
                except Exception as e:
                    s = "-err"
            else:
                s = "?"
        except Exception as e:
            s = "!"
        return s

. . . . . 

        connections = self.ble.connections
        cns = "|"
        for c in connections:
            c.cname = self.connectionid(c)
            cns += c.cname + "|"
        print(cns)
. . . . . 

I am getting this output:

|Apple Inc.iPad5,3-#|

so accessing s += dis.serial_number above "throws" an error.

ThomasAtBBTF commented 2 years ago

Using the firmware in the zip provided. (Thank you!)

I am getting this results:

Note: This is not code, this is my debug output!
<Address 4f:15:bd:df:4b:d1>
|Apple Inc.iPad5,3-#|
set self.isconnected = True
<Address 4f:15:bd:df:4b:d1>
|Apple Inc.iPad5,3-#|
set self.isconnected = True

-> here I turned **off** bluetooth in IOS settings

playsound ((880, 0.25), 262)

-> the sound iss generated in my code if the connection "goes away"....

!!!!!!!!!!!!!!!!!!! Start advertizing: 101
hid nc. 199.104 29792   29776   -16

-> here I turned **on** bluetooth in IOS settings

playsound ((262, 0.25), 880)

-> the sound is generated in my code if the connection "comes back"....

set self.isconnected = True

hid OK 209.109 29776    29776   16 adv:False 10822 Apple Inc.iPad5,3-#
<Address 4d:c1:6b:47:bf:03>
|Apple Inc.iPad5,3-#|
set self.isconnected = True
<Address 4d:c1:6b:47:bf:03>
|Apple Inc.iPad5,3-#|
set self.isconnected = True

As we can see from this: Both of you were correct in saying the BLE MAC-Address will not help for my problem! Because after a disconnect and the a reconnect the MAC-Address reported from the same IPad is changing..

ThomasAtBBTF commented 2 years ago

But I don't want to give up... There must be a way to find out to whom a connection talks...

dhalbert commented 2 years ago

Getting the IRK (Identity Resolving Key) might help. That I think will last until you forget the device and re-pair. I don't see where to get it at the moment, but will look further later.

ThomasAtBBTF commented 2 years ago

Great, thank you.