← index #686Issue #866
Off-topic · high · value 2.291
QUERY · ISSUE

aioble: Connecting to the Apple Media Service (Pico-W)

openby neil-morrison44opened 2023-06-18updated 2024-02-11

Hi,

I'm trying to use a Pi Pico W to connect to the Apple Media Service, but I'm running into a few issues.

async def connectBLE():
    connection = await aioble.advertise(
        _ADV_INTERVAL_MS,
        name="AMS",
        services=[_AMS_UUID],
        appearance=0,
        manufacturer=(0xABCD, b"1234"),
    )
    print("Connection from", connection.device)
    return connection

_AMS_UUID = bluetooth.UUID("89D3502B-0F36-433A-8EF4-C502AD55F8DC")
_AMS_ENTITY_UPDATE = bluetooth.UUID("2F7CABCE-808D-411F-9A0C-BB92BA96C102")
_ADV_INTERVAL_MS = const(250000)

async def main():
    ams_service = aioble.Service(_AMS_UUID)
    aioble.register_services(ams_service)

    connection = await connectBLE()
    ams_service = await connection.service(_AMS_UUID)
    ams_entity_update_characteristic = await ams_service.characteristic(
        _AMS_ENTITY_UPDATE
    )
    await ams_entity_update_characteristic.subscribe(notify=True)

This'll connect, find the services, characteristics but then throws:

Traceback (most recent call last):
  File "<stdin>", line 160, in <module>
  File "uasyncio/core.py", line 1, in run
  File "uasyncio/core.py", line 1, in run_until_complete
  File "uasyncio/core.py", line 1, in run_until_complete
  File "<stdin>", line 139, in main
  File "aioble/client.py", line 430, in subscribe
  File "aioble/client.py", line 295, in write
GattError: 5

On the .subscribe(notify=True) bit, not sure how / if I can look up the GattError: 5 bit for more info?

There is a CircuitPython implementation here, https://docs.circuitpython.org/projects/ble_apple_media/en/latest/index.html

Which uses a "Service Solicitation" advertisement - what's the method for constructing that in aioble? I'm guessing custom adv_data & resp_data but I'm unsure of the format, etc.

That implementation also pairs to the connecting device, but when I insert a await connection.pair() I get

Traceback (most recent call last):
  File "<stdin>", line 161, in <module>
  File "uasyncio/core.py", line 1, in run
  File "uasyncio/core.py", line 1, in run_until_complete
  File "uasyncio/core.py", line 1, in run_until_complete
  File "<stdin>", line 124, in main
  File "<stdin>", line 99, in connectBLE
  File "aioble/device.py", line 256, in pair
  File "aioble/security.py", line 171, in pair
ValueError: unknown config param

Which I guess is due to the Pico-W implementation not (currently?) supporting something in ble.config(bond=bond, le_secure=le_secure, mitm=mitm, io=io)

Thanks

16 comments
sebastian-blohm · 2023-08-12

Hi!
Were you able to find out what GattError: 5 means? I'm seeing the same in a similar context.

neil-morrison44 · 2023-08-12

No I couldn’t work it out, dug into the code a bit to see if I could find it’s origin but even that didn’t help much (though I might not have gone to a deep enough layer)

puppet13th · 2023-09-20

@neil-morrison44 if you want to pico-w to connect to anything you should use central role( scan and connect ) like this aioble example. the code that you're using now is for peripheral mode( aioble.advertise )

neil-morrison44 · 2023-09-20

@puppet13th peripheral mode is correct for the Apple Media Service, the phone is the central device.

puppet13th · 2023-09-20

from this picture there is MS and MR. what is pico role there?

puppet13th · 2023-09-20

@neil-morrison44 after connection try add these line:

await connection.exchange_mtu()
await connection.pair( bond=False, le_secure=False, mitm=False )

if those config not implemented for pico-w yet you can edit security.py comment out 171 line:
ble.config(bond=bond, le_secure=le_secure, mitm=mitm, io=io)

neil-morrison44 · 2023-09-20

@puppet13th puppet13th it'd be the MR device - I believe that's roughly what I tried back in June which threw the error, but it may well be supported now if there's been updates so I'll have another go on the latest MicroPython at some point & update this

puppet13th · 2023-09-21

@neil-morrison44 i have not use any pico-w board yet, have you check if ble pair implemented in pico-w port? on esp32 port pair only implemented since firmware version 1.20

foxt · 2023-11-13

Yeah - it seems like BLE.gap_pair is not present on the Pico W, so aioble.pair will fail with AttributeError: 'BLE' object has no attribute 'gap_pair'

hoihu · 2024-02-04

I'm facing the same issue with my raspberry pico w. I'm trying to read the current time service characteristic from an iPhone.

I guess the gatt error 5 is the GATTS_ERROR_INSUFFICIENT_AUTHENTICATION as specified in https://docs.micropython.org/en/latest/library/bluetooth.html#event-handling

That makes somewhat sense when the peripheral is not paired / bonded to the iPhone. However, when adding the await connection.pair() to the code, it fails with ValueError: unknown config param .

As I interepret the code in modbluetooth.c this could only be case when the bonding support is not compiled in. But MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING seems to be defined in the the make file (?)

Also note that the example from the official repo https://github.com/micropython/micropython/blob/master/examples/bluetooth/ble_bonding_peripheral.py does not seem to work anymore, giving the same fault at https://github.com/micropython/micropython/blob/master/examples/bluetooth/ble_bonding_peripheral.py#L70

(using the nightly build version as of 3.2.2024)

hoihu · 2024-02-04

But MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING seems to be defined in the the make file (?)

huh looking into it it may not be... since it uses btstack not nimble (at least from the build output)? Means bonding support is not enabled on the rp2 port?

andrewleech · 2024-02-04

Yeah I'm pretty sure bonding is currently only supported on stm32

hoihu · 2024-02-04

Yeah I'm pretty sure bonding is currently only supported on stm32

thanks for the feedback. Yeah I feared that a bit... pitty since bonding is sometimes required to readout characteristics.

studying the release notes: https://micropython.org/resources/micropython-ChangeLog.txt it was mentioned that the esp32 port is also supporting bonding from 1.20 onward... but haven't tested that. I'll give it a shot with a pyboard d.

In general, it would be good to have an overview with a list of ports: <ble_features>.

hoihu · 2024-02-05

Ok so the bonding support is not enabled on the pico w, but it can be done via

diff --git a/ports/rp2/CMakeLists.txt b/ports/rp2/CMakeLists.txt
index 3a46fdaa7..2ced4e86e 100644
--- a/ports/rp2/CMakeLists.txt
+++ b/ports/rp2/CMakeLists.txt
@@ -284,6 +284,7 @@ if (MICROPY_BLUETOOTH_BTSTACK)
 
     target_compile_definitions(${MICROPY_TARGET} PRIVATE
         MICROPY_BLUETOOTH_BTSTACK=1
+        MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING=1
         MICROPY_BLUETOOTH_BTSTACK_CONFIG_FILE=\"btstack_inc/btstack_config.h\"
     )

recompiling and then the ValueError: unknown config param faults are disappearing. On the mobile device, a pairing request pops up (depending on the selected pairing option). So far so good.

However it doesn't continue any further from there. If I confirm it on the mobile it doesn't seem to continue the bonding process.

I then enabled the logs via aioble.core.log_level = 3 and checked if there are any incoming events that are not handled, but nope.

andrewleech · 2024-02-05

Yeah the problem is a bunch of C functions would need to be written to recieve any pairing and bonding events from btstack and convert those events into the form that aioble expects. I wrote some of the initial stm versions of these a few years ago for nimble, but btstack was not supported on my hardware (stm32wb55) so I wasn't able to prepare the code for that stack.

If you take a look in https://github.com/micropython/micropython/blob/master/extmod/nimble/modbluetooth_nimble.c at everywhere enabled by MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING you'll see the bulk of the code supporting this feature, there probably needs to be more of this for btstack in https://github.com/micropython/micropython/blob/master/extmod/btstack/modbluetooth_btstack.c (though some support is already there from https://github.com/micropython/micropython/commit/a1fcf301217b34ae74fbb937a9488be5bc6e61a5 )

hoihu · 2024-02-11

I'll give it a shot with a pyboard d.

so I tried - but faced isses... see. https://github.com/micropython/micropython/issues/13639

@andrewleech thanks for the pointers! I had a look around those files and I also think it should be doable. But what I really need is a working (uPy-) example of a platform where bonding is known to work, to get a feeling of how the interactions works during the bonding process. For example, it's still unclear to me if addr_mode set to 2 is a requirement for bonding.

CANDIDATE · ISSUE

'TypeError: can't convert NoneType to int' using aioble on Pico W

openby cchanthompalitopened 2024-05-28updated 2024-11-05

Hi all,

I am trying to get two Raspberry Pi Pico W's to communicate. The Pico that transmits the data is hooked up to an accelerometer (MPU6050). As it takes the x, y, and z values from the accelerometer, it then attempts to send the data to the receiving Pico. The example code, temp_client.py and temp_sensor.py, was used as a baseline for my code. As for setup, I am using the IDE, Thonny, which is currently running MicroPython v1.22.2.

When I run the program, the two devices connect successfully, and the transmitter appears to successfully retrieve and send the data to the receiver. The problem I'm having is at the receiver end -- when I attempt to read the data using data = accel_char.read() , I get an error that says "TypeError: can't convert NoneType to int". I am unsure why this error is appearing, since it is written the exact way the examples have it (and I tested the example numerous times, along with using a MAX31855 breakout board to receive temperature data.) Here is the code for my receiver which I am having issues for.

import sys

# ruff: noqa: E402
sys.path.append("")

from micropython import const
from machine import RTC

import uasyncio as asyncio
import aioble
import bluetooth

import random
import struct

# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)

# Experiment ID (arbitrary for now), for file name
Exp_ID = "Acc" + str(24)

# Row ID for file
global rowID
rowID = 1

# Initialize RTC
rtc = machine.RTC()

# Write CSV header
def start_csv(timestamp):
    global accelFile
    accelFile = Exp_ID + '-' + timestamp + '.csv'
    
    with open(accelFile, 'a') as f:
        f.write("Row ID" + ",")
        f.write("X Accel")
        f.write(",")
        f.write("Y Accel")
        f.write(",")
        f.write("Z Accel")
        f.write("\r\n")
                
# Get current time to attach to file name
def get_current_time():
    current_time = rtc.datetime()

    curr_year = current_time[0]		# '2024' in 5/20/2024
    curr_month = current_time[1]	# '5' in 5/20/2024
    curr_day = current_time[2]		# '20' in 5/20/2024

    curr_hour = current_time[4] 	# '10' in '10:25:56'
    curr_min = current_time[5] 		# '25' in '10:25:56'
    curr_sec = current_time[6] 		# '56' in '10:25:56'

    # Convert to string to maintain double-digit format during transfer
    year = '{:02d}'.format(curr_year)
    month = '{:02d}'.format(curr_month)
    day = '{:02d}'.format(curr_day)
    hour = '{:02d}'.format(curr_hour)
    minute = '{:02d}'.format(curr_min)
    sec = '{:02d}'.format(curr_sec)

    return year, month, day, hour, minute, sec


# Write to CSV file
def write2file(rowID:int, x_accel:int, y_accel:int, z_accel:int):
    
    with open(accelFile, 'a') as f:
        f.write(str(rowID))
        f.write(",")
        f.write(str(x_accel))
        f.write(",")
        f.write(str(y_accel))
        f.write(",")
        f.write(str(z_accel))
        f.write("\r\n")
        
    rowID += 1
        
# Scan for devices to connect to   
async def find_temp_sensor():
    # Scan for 5 seconds, in active mode, with very low interval/window (to
    # maximise detection rate).
    async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
        async for result in scanner:
            # See if it matches our name and the environmental sensing service.
            if result.name() == "mpy-accel" and _ENV_SENSE_UUID in result.services():
                # print(result.services())
                return result.device
    return None

    
# Helper to decode the temperature characteristic encoding (sint16, hundredths of a degree).
def decode_data(data):
    
    return struct.unpack("8s8s8s", data)


# Reconstruct packets, receive data and append current time to CSV entry
async def receive_data(data):
    
    recv_data = b''
    unpacked_data = False
    recv_data += data
    
    if len(recv_data) >= 24:
        unpacked_data = True
        
        data = decode_data(recv_data[:24])
        x, y, z = data[0], data[1], data[2]
        
        decoded_x = x.decode('utf-8')
        decoded_y = y.decode('utf-8')
        decoded_z = z.decode('utf-8')
        
        write2file(rowID, decoded_x, decoded_y, decoded_z)

        print(f"rowID: {rowID}, X Accel: {decoded_x}, Y Accel: {decoded_y}, Z Accel: {decoded_z}") # Print received data to central output
        
        recv_data = b''      # Reset the received data buffer
        

async def main():
    connected = False
    
    device = await find_temp_sensor()
    if not device:
        print("Temperature sensor not found")
        return

    try:
        connection = await device.connect()
        print("Connecting to", device)
        connected = True
        
    except asyncio.TimeoutError:
        print("Timeout during connection")
        return

    async with connection:
        try:
            accel_service = await connection.service(_ENV_SENSE_UUID)
            print("Accel service: ", accel_service)
            if accel_service is None:
                print("Environmental sensing service not found")
                return
            
            accel_char = await accel_service.characteristic(_ENV_SENSE_TEMP_UUID)
            print("Accel characteristic: ", accel_char)
            if accel_char is None:
                print("Characteristic not found")
                return
            
            # await asyncio.sleep(1)
        
        
        except asyncio.TimeoutError as e:
            print("Error registering services/characteristics: ", e)
            
            # Start CSV file
            global accelFile
            year, month, day, hour, minute, sec = get_current_time()
            
            timestamp = f"{year:04}_{month:02}_{day:02}_{hour:02}-{minute:02}-{sec:02}"
            start_csv(timestamp)
                    
        while connected:
            data = b''
            while data is b'':
                data = await accel_char.read()
                print(type(data))
                print("Waiting until data is not None")
                
            if data is None:
                print("No more data to receive.")
                break

            print("Received data type: ", type(data))
            print(data)
            await receive_data(data)
            
            '''
            except Exception as e:
                print("Error receiving data: ", e)
            '''
            
            await asyncio.sleep_ms(1000)
            
        print("Disconnected")


asyncio.run(main())

Receiver Output:
MPY: soft reboot
Connecting to Device(ADDR_PUBLIC, 28:cd:c1:0b:30:b9, CONNECTED)
Accel service: Service: 7 9 UUID(0x181a)
Accel characteristic: Characteristic: 11 9 10 UUID(0x2a6e)
<class 'bytes'>
Waiting until data is not None
Received data type: <class 'bytes'>
b''
Traceback (most recent call last):
File "<stdin>", line 195, in <module>
File "asyncio/core.py", line 1, in run
File "asyncio/core.py", line 1, in run_until_complete
File "asyncio/core.py", line 1, in run_until_complete
File "<stdin>", line 173, in main
File "aioble/client.py", line 251, in read
TypeError: can't convert NoneType to int

The receiver initially does not receive any data (as marked by b''), but I figure this is due to a timing issue. To my understanding, this error should still not be happening despite this since the datatype of the received data is 'bytes.'

Any help would be greatly appreciated! I've been stuck on this problem for a couple weeks now and have not found a solution despite scrounging through numerous discussion posts and comparing my code to the original example code multiple times. I originally thought it was because the service and characteristic was not being registered properly, but I added a short delay to ensure that it is and the problem is still occurring. I've also added my transmitter code below just in case.

# BLE Transmitter: attached to accelerometer (MPU6050) and an external power supply

import sys

# ruff: noqa: E402
sys.path.append("")

import machine
import micropython
from micropython import const
from machine import Pin
# For MPU6050
import time
import MPU6050
# For BLE
import uasyncio as asyncio
import aioble
import bluetooth
import struct

# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
# org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_CYCLING_SPEED_SENSOR = const(1154)

# How frequently to send advertising beacons.
_ADV_INTERVAL_MS = 250_000

# Register GATT server.
accel_service = aioble.Service(_ENV_SENSE_UUID)
accel_char = aioble.Characteristic(accel_service, _ENV_SENSE_TEMP_UUID, read=True, write=True)
aioble.register_services(accel_service)

# Set GPIO pins
start_pin = machine.Pin(5, Pin.IN)
sda = machine.Pin(2)
scl = machine.Pin(3)

# Initialize I2C ([group I2C0 or I2C1], [sda pin], [scl pin], [frequency])
i2c = machine.I2C(1, sda=sda, scl=scl, freq=400000)

# Setup readings (note: transmitter only needs to know duration, receiver will handle
# time and writing/naming file
def setup_readings():
    
    # Set duration (how long are we measuring? (ms))
    duration = 1000
    
    return duration


# Measure data from accelerometer
async def get_measuring():
    
    setup = setup_readings()
    duration = setup
    #print(f"Duration: {duration}")
    
    # Set up read period (how often accelerometer reads (sec))
    read_period = 1
    
    # Set up measurement duration
    end_time = time.ticks_add(time.ticks_ms(), duration)
    #print(f"End time: {end_time}")
    
    # Loop continuously to print data
    while time.ticks_diff(end_time, time.ticks_ms()) > 0:
        # 'Accel' is tuple of 3 that contains x, y, andz data
        accel = mpu.read_accel_data()
        x_accel = accel[0]
        y_accel = accel[1]
        z_accel = accel[2]
        
        print("Accel data type: ", type(x_accel))
        
        print("Accel X: " + str(x_accel))
        print("Accel Y: " + str(y_accel))
        print("Accel Z: " + str(z_accel))
        
        encoded_data = encode_data(x_accel, y_accel, z_accel)
        await send_data(encoded_data)

        time.sleep(read_period)
        
        if time.ticks_diff(end_time, time.ticks_ms()) <= 0:
            break
    
    print("Completed measuring for given duration")
    return

# Helper to encode the temperature characteristic encoding (sint16, hundredths of a degree).
def encode_data(x_accel, y_accel, z_accel):
    
    # Convert 'float' data to strings
    str_x = str(x_accel)
    str_y = str(y_accel)
    str_z = str(z_accel)
    
    # Encode to bytes for transfer / data consistency
    encode_x = str_x.encode('utf-8')
    encode_y = str_y.encode('utf-8')
    encode_z = str_z.encode('utf-8')
    print("Encoded data type: ", type(encode_x))
    
    '''
    print(len(encode_x))
    print(len(encode_y))
    print(len(encode_z))
    '''
    
    return struct.pack("8s8s8s", encode_x, encode_y, encode_z)


# Break data into packets to send
async def send_data(data):
    
    accel_char.write(data)
    print("Data type of data: ", type(data))

    
# Scan for I2C devices
async def scan_I2C():
    print('Scanning I2C bus...')
    devices = i2c.scan() # returns all viable I2C devices on bus
    
    if len(devices) == 0: # if devices list is length 0, report no devices
        print('No I2C device!')
    else:
        print('I2C devices found: ', len(devices))
        
        for device in devices: # Run loop for every device in list, print corresponding address
            print('Decimal address: ', device, ' | Hexa address: ', hex(device))
        
        # Set up MPU6050 object
        global mpu
        mpu = MPU6050.MPU6050(i2c)
        
        # Wake up MPU6050 in case it was sleeping
        mpu.wake()


# Serially wait for connections. Don't advertise while a central is
# connected.
async def peripheral_task():
    connected = False
    
    while True:
        # Advertise device
        print("Advertising device, waiting for connection...")
        
        try:
            async with await aioble.advertise(
                _ADV_INTERVAL_MS,
                name="mpy-accel",
                services=[_ENV_SENSE_UUID],
                appearance=_ADV_APPEARANCE_CYCLING_SPEED_SENSOR,
            ) as connection:
                
                # Print connected device
                print("Connection from ", connection.device)
                connected = True
                
                await asyncio.sleep(1)

        except Exception as e:
            print("Error while advertising: ", e)
                
        while connected:
            # await asyncio.sleep(0.5)
            
            # Scan I2C devices
            await scan_I2C()

            # Retrieve measurements from accelerometer, pack and send
            await get_measuring()
        
            # Disconnect after measuring interval has finished
            # await connection.disconnect()
        
        print("Disconnected")
            
            #return

async def main():
    await peripheral_task()


asyncio.run(main())

Thank you!

2 comments
brianreinhold · 2024-05-30

Personally I have always been frustrated with being able to tell what is in scope and what is not in scope in python/micropython.

accel_service and accel_char are both first defined in a try - except clause and then they are accessed later outside of that clause. Now in C and in Java, those variables would be out of scope once outside of the try clause.

Try something simple like placing them at the top of the file along with the connected = False (I type everything I can not only for my benefit but for a future reader of my code's benefit....

async def main():
    connected: bool = False
    accel_service: ClientService | None = None 
    accel_char: ClientCharacteristic | None = None

then check that they are not None before accessing them. Just because you are getting the error that what you are accessing is of NoneType suggests that they are out of scope. I recall being instructed that variables defined in a try clause should not go out of scope but that has not always been my experience.

In any case, if you have not found a solution this is an easy thing to try.

brianreinhold · 2024-11-05

I have also gotten this error (client side) because the device has disconnected and aioble does not get that information to the client application or does not get that information to the client application in a timely manner. In the mean time the client application does something that uses components of the aioble library assuming the device is still connected and that causes the type error.

In the end this was so common when working with real (market) devices I had the application set its own variables directly in the aioble disconnect event handler and used that (instead of the DeviceConnection apis) to ascertain if the device was still connected. That solved a LOT of problems!

For example here:

    elif event == _IRQ_PERIPHERAL_DISCONNECT:
        
        from bt_layouts import set_phdinfo_connected
        set_phdinfo_connected(False)

        conn_handle, _, _ = data
        print(f'LNI: AIOBLE: Peripheral disconnect. Handle {conn_handle}')
        if connection := DeviceConnection._connected.get(conn_handle, None):
            # Tell the device_task that it should terminate.
            connection._event.set()

The first two lines set the connection state in the application variable.

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied