Asyncio、Arduino BLE 和不读取特性更新

Asyncio, Arduino BLE, and not reading characteristic updates

我有一个 Arduino 33 BLE,它正在使用 BNO055 传感器校准和四元数数据的字符串表示来更新一些蓝牙特性。在 Arduino 方面,我看到校准和四元数数据按预期的有序顺序更新。

我在 Windows 10 上有一个 Python (3.9) 程序 运行,它使用 asyncio 订阅 Arduino 上的特性以读取更新。当我在 Arduino 上的更新率为 1/秒时,一切正常。 “工作正常”我的意思是我看到更新的有序序列:四元数、校准、四元数、校准……我遇到的问题是我将更新速率更改为 10/秒(在 Arduino 中延迟 100 毫秒)并且例如,现在我得到 100 次四元数数据更新,但只有 50 次校准数据更新,而更新次数应该相等。不知何故,我没有在 python 方面正确处理更新。

python代码如下:

import asyncio
import pandas as pd

from bleak import BleakClient
from bleak import BleakScanner

ardAddress = ''
found = ''
exit_flag = False

temperaturedata = []
timedata = []
calibrationdata=[]
quaterniondata=[]

# loop: asyncio.AbstractEventLoop

tempServiceUUID = '0000290c-0000-1000-8000-00805f9b34fb'  # Temperature Service UUID on Arduino 33 BLE

stringUUID = '00002a56-0000-1000-8000-00805f9b34fb'  # Characteristic of type String [Write to Arduino]
inttempUUID = '00002a1c-0000-1000-8000-00805f9b34fb'  # Characteristic of type Int [Temperature]
longdateUUID = '00002a08-0000-1000-8000-00805f9b34fb'  # Characteristic of type Long [datetime millis]

strCalibrationUUID = '00002a57-0000-1000-8000-00805f9b34fb'  # Characteristic of type String [BNO055 Calibration]
strQuaternionUUID = '9e6c967a-5a87-49a1-a13f-5a0f96188552'  # Characteristic of type Long [BNO055 Quaternion]


async def scanfordevices():
    devices = await BleakScanner.discover()
    for d in devices:
        print(d)
        if (d.name == 'TemperatureMonitor'):
            global found, ardAddress
            found = True
            print(f'{d.name=}')
            print(f'{d.address=}')
            ardAddress = d.address
            print(f'{d.rssi=}')
            return d.address


async def readtemperaturecharacteristic(client, uuid: str):
    val = await client.read_gatt_char(uuid)
    intval = int.from_bytes(val, byteorder='little')
    print(f'readtemperaturecharacteristic:  Value read from: {uuid} is:  {val} | as int={intval}')


async def readdatetimecharacteristic(client, uuid: str):
    val = await client.read_gatt_char(uuid)
    intval = int.from_bytes(val, byteorder='little')
    print(f'readdatetimecharacteristic:  Value read from: {uuid} is:  {val} | as int={intval}')


async def readcalibrationcharacteristic(client, uuid: str):
    # Calibration characteristic is a string
    val = await client.read_gatt_char(uuid)
    strval = val.decode('UTF-8')
    print(f'readcalibrationcharacteristic:  Value read from: {uuid} is:  {val} | as string={strval}')


async def getservices(client):
    svcs = await client.get_services()
    print("Services:")
    for service in svcs:
        print(service)

        ch = service.characteristics
        for c in ch:
            print(f'\tCharacteristic Desc:{c.description} | UUID:{c.uuid}')


def notification_temperature_handler(sender, data):
    """Simple notification handler which prints the data received."""
    intval = int.from_bytes(data, byteorder='little')
    # TODO:  review speed of append vs extend.  Extend using iterable but is faster
    temperaturedata.append(intval)
    #print(f'Temperature:  Sender: {sender}, and byte data= {data} as an Int={intval}')


def notification_datetime_handler(sender, data):
    """Simple notification handler which prints the data received."""
    intval = int.from_bytes(data, byteorder='little')
    timedata.append(intval)
    #print(f'Datetime: Sender: {sender}, and byte data= {data} as an Int={intval}')


def notification_calibration_handler(sender, data):
    """Simple notification handler which prints the data received."""
    strval = data.decode('UTF-8')
    numlist=extractvaluesaslist(strval,':')
    #Save to list for processing later
    calibrationdata.append(numlist)

    print(f'Calibration Data: {sender}, and byte data= {data} as a List={numlist}')


def notification_quaternion_handler(sender, data):
    """Simple notification handler which prints the data received."""
    strval = data.decode('UTF-8')
    numlist=extractvaluesaslist(strval,':')

    #Save to list for processing later
    quaterniondata.append(numlist)

    print(f'Quaternion Data: {sender}, and byte data= {data} as a List={numlist}')


def extractvaluesaslist(raw, separator=':'):
    # Get everything after separator
    s1 = raw.split(sep=separator)[1]
    s2 = s1.split(sep=',')
    return list(map(float, s2))


async def runmain():
    # Based on code from: https://github.com/hbldh/bleak/issues/254
    global exit_flag

    print('runmain: Starting Main Device Scan')

    await scanfordevices()

    print('runmain: Scan is done, checking if found Arduino')

    if found:
        async with BleakClient(ardAddress) as client:

            print('runmain: Getting Service Info')
            await getservices(client)

            # print('runmain: Reading from Characteristics Arduino')
            # await readdatetimecharacteristic(client, uuid=inttempUUID)
            # await readcalibrationcharacteristic(client, uuid=strCalibrationUUID)

            print('runmain: Assign notification callbacks')
            await client.start_notify(inttempUUID, notification_temperature_handler)
            await client.start_notify(longdateUUID, notification_datetime_handler)
            await client.start_notify(strCalibrationUUID, notification_calibration_handler)
            await client.start_notify(strQuaternionUUID, notification_quaternion_handler)

            while not exit_flag:
                await asyncio.sleep(1)
            # TODO:  This does nothing.  Understand why?
            print('runmain: Stopping notifications.')
            await client.stop_notify(inttempUUID)
            print('runmain: Write to characteristic to let it know we plan to quit.')
            await client.write_gatt_char(stringUUID, 'Stopping'.encode('ascii'))
    else:
        print('runmain: Arduino not found.  Check that its on')

    print('runmain: Done.')


def main():
    # get main event loop
    loop = asyncio.get_event_loop()

    try:
        loop.run_until_complete(runmain())
    except KeyboardInterrupt:
        global exit_flag
        print('\tmain: Caught keyboard interrupt in main')
        exit_flag = True
    finally:
        pass

    print('main: Getting all pending tasks')

    # From book Pg 26.
    pending = asyncio.all_tasks(loop=loop)
    print(f'\tmain: number of tasks={len(pending)}')
    for task in pending:
        task.cancel()
    group = asyncio.gather(*pending, return_exceptions=True)
    print('main: Waiting for tasks to complete')
    loop.run_until_complete(group)
    loop.close()

    # Display data recorded in Dataframe
    if len(temperaturedata)==len(timedata):
        print(f'Temperature data len={len(temperaturedata)}, and len of timedata={len(timedata)}')

        df = pd.DataFrame({'datetime': timedata,
                           'temperature': temperaturedata})
        #print(f'dataframe shape={df.shape}')
        #print(df)
        df.to_csv('temperaturedata.csv')
    else:
        print(f'No data or lengths different: temp={len(temperaturedata)}, time={len(timedata)}')

    if len(quaterniondata)==len(calibrationdata):
        print('Processing Quaternion and Calibration Data')
        #Load quaternion data
        dfq=pd.DataFrame(quaterniondata,columns=['time','qw','qx','qy','qz'])
        print(f'Quaternion dataframe shape={dfq.shape}')
        #Add datetime millis data
        #dfq.insert(0,'Time',timedata)
        #Load calibration data
        dfcal=pd.DataFrame(calibrationdata,columns=['time','syscal','gyrocal','accelcal','magcal'])
        print(f'Calibration dataframe shape={dfcal.shape}')
        #Merge two dataframes together
        dffinal=pd.concat([dfq,dfcal],axis=1)
        dffinal.to_csv('quaternion_and_cal_data.csv')
    else:
        print(f'No data or lengths different. Quat={len(quaterniondata)}, Cal={len(calibrationdata)}')
        if len(quaterniondata)>0:
            dfq = pd.DataFrame(quaterniondata, columns=['time', 'qw', 'qx', 'qy', 'qz'])
            dfq.to_csv('quaterniononly.csv')
        if len(calibrationdata)>0:
            dfcal = pd.DataFrame(calibrationdata, columns=['time','syscal', 'gyrocal', 'accelcal', 'magcal'])
            dfcal.to_csv('calibrationonly.csv')

    print("main: Done.")


if __name__ == "__main__":
    '''Starting Point of Program'''
    main()

所以,我的第一个问题是任何人都可以帮助我理解为什么我似乎没有在我的 Python 程序中获得所有更新吗?我应该看到 notification_quaternion_handler() 和 notification_calibration_handler() 被调用的次数相同,但我没有。我假设我没有正确使用 asyncio,但此时我无法调试它?

我的第二个问题是,是否有尝试从蓝牙接收相对高频更新的最佳实践,例如每 10-20 毫秒?我正在尝试读取 IMU 传感器数据,它需要以相当高的速率完成。

这是我第一次尝试蓝牙和 asyncio,显然我还有很多东西要学。

感谢您的帮助

您有多个以相同频率更新的特征。在低功耗蓝牙 (BLE) 中以相同的特性传输这些值更有效。我注意到的另一件事是您似乎将值作为字符串发送。从字符串中提取信息的方式来看,字符串格式可能是“key:value”。这也是通过 BLE 发送数据的低效方式。

通过 BLE 传输的数据始终是字节列表,因此如果需要浮点数,则需要将其更改为整数以作为字节发送。例如,如果我们想发送一个有两位小数的值,将它乘以 100 总是会删除小数位。换句话说,它将除以 100。例如:

>>> value = 12.34
>>> send = int(value * 100)
>>> print(send)
1234
>>> send / 100
12.34

struct 库允许将整数轻松打包成一系列字节来发送。例如:

>>> import struct
>>> value1 = 12.34
>>> value2 = 67.89
>>> send_bytes = struct.pack('<hh', int(value1 * 100), int(value2 * 100))
>>> print(send_bytes)
b'\xd2\x04\x85\x1a'

然后解压:

>>> r_val1, r_val2 = struct.unpack('<hh', send_bytes)
>>> print(f'Value1={r_val1/100} : Value2={r_val2/100}')
Value1=12.34 : Value2=67.89

使用传输的字节数最少的单个特征应该允许更快的通知。

要了解其他特性如何做到这一点,请查看来自蓝牙 SIG 的以下文档: https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-5/

Blood Pressure Measurement 特征就是一个很好的例子。

@ukBaz 的精彩回答。

总结给可能有类似问题的其他人。

在 Arduino 方面,我得到了这样的结果(只显示了重要部分):

typedef struct  __attribute__ ((packed)) {
  unsigned long timeread;
  int qw; //float Quaternion values will be scaled to int by multiplying by constant
  int qx;
  int qy;
  int qz;
  uint8_t cal_system;
  uint8_t cal_gyro;
  uint8_t cal_accel;
  uint8_t cal_mag;
}sensordata ;

//Declare struct and populate
  sensordata datareading;

  datareading.timeread=tnow;
  datareading.qw=(int) (quat.w()*10000);
  datareading.qx=(int) (quat.x()*10000);
  datareading.qy=(int) (quat.y()*10000);
  datareading.qz=(int) (quat.z()*10000);
  datareading.cal_system=system;
  datareading.cal_gyro=gyro;
  datareading.cal_accel=accel;
  datareading.cal_mag=mag;

  //Write values to Characteristics.
  
  structDataChar.writeValue((uint8_t *)&datareading, sizeof(datareading));

然后在 Python(Windows 桌面)端,我用这个来解压缩正在发送的数据:

def notification_structdata_handler(sender, data):
    """Simple notification handler which prints the data received."""
    
    # NOTE:  IT IS CRITICAL THAT THE UNPACK BYTE STRUCTURE MATCHES THE STRUCT
    #        CONFIGURATION SHOWN IN THE ARDUINO C PROGRAM.
    
    # <hh meaning:  <=little endian, h=short (2 bytes), b=1 byte, i=int 4 bytes, unsigned long = 4 bytes

    #Scale factor used in Arduino to convert floats to ints.
    scale=10000

    # Main Sensor struct
    t,qw,qx,qy,qz,cs,cg,ca,cm= struct.unpack('<5i4b', data)

    sensorstructdata.append([t,qw/scale,qx/scale,qy/scale,qz/scale,cs,cg,ca,cm])

    print(f'--->Struct Decoded. time={t}, qw={qw/scale}, qx={qx/scale}, qy={qy/scale}, qz={qz/scale},'
          f'cal_s={cs}, cal_g={cg}, cal_a={ca}, cal_m={cm}')

感谢您提供的所有帮助,如我所承诺的那样,性能比我开始时要好得多!