Issue
I have an Arduino 33 BLE that is updating a few bluetooth characteristics with a string representation of BNO055 sensor calibration and quaternion data. On the Arduino side, I see the calibration and quaternion data getting updated in a nice orderly sequence as expected.
I have a Python (3.9) program running on Windows 10 that uses asyncio to subscribe to the characteristics on the Arduino to read the updates. Everything works fine when I have an update rate on the Arduino of 1/second. By "works fine" I mean I see the orderly sequence of updates: quaternion, calibration, quaternion, calibration,.... The problem I have is that I changed the update rate to the 10/second (100ms delay in Arduino) and now I am getting, for example, 100 updates for quaternion data but only 50 updates for calibration data when the number of updates should be equal. Somehow I'm not handling the updates properly on the python side.
The python code is listed below:
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()
So, my first question is can anyone help me understand why I do not seem to be getting all the updates in my Python program? I should be seeing notification_quaternion_handler() and notification_calibration_handler() called the same number of times but I am not. I assume I am not using asyncio properly but I am at a loss to debug it at this point?
My second question is, are there best practices for trying to receive relatively high frequency updates from bluetooth, for example every 10-20 ms? I am trying to read IMU sensor data and it needs to be done at a fairly high rate.
This is my first attempt at bluetooth and asyncio so clearly I have a lot to learn.
Thank You for the help
Solution
You have multiple characteristics that are being updated at the same frequency. It is more efficient in Bluetooth Low Energy (BLE) to transmit those values in the same characteristic. The other thing I noticed is that you appear to be sending the value as a string. It looks like the string format might "key:value" by the way you are extracting information from the string. This is also inefficient way to send data via BLE.
The data that is transmitted over BLE is always a list of bytes so if a float is required, it needs to be changed into an integer to be sent as bytes. As an example, if we wanted to send a value with two decimal places, multiplying it by 100 would always remove the decimal places. To go the other way it would be divide by 100. e.g:
>>> value = 12.34
>>> send = int(value * 100)
>>> print(send)
1234
>>> send / 100
12.34
The struct
library allows integers to be easily packed that into a series of byes to send. As an example:
>>> 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'
To then unpack that:
>>> 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
Using a single characteristic with the minimum number of bytes being transmitted should allow for the faster notifications.
To look at how other characteristics do this then look at the following document from the Bluetooth SIG: https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-5/
A good example might be the Blood Pressure Measurement
characteristic.
Answered By - ukBaz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.