Ble Communication

To be mentioned, the ble-based communication is already be done in Week7-Embedded Computing. In Week9-Input Devices, to make sure the robustness of the data transformation, I used serial port to record data. And this ble-communication version is based on the serial port transformation version. Because the Week7 website page focus more on the system demonstration, I would like to demonstrate the whole code for ble communication and data receiving in this week's websit. In summary, the homework for Week7, 9 and 11 used the same hardware which demonstrated in Week7-Embedded Computing, but focus on different contents.

Wireless Sensors and Signal Transformation

Three IMU sensors are connected together with a controller (Arduino Nano 33 BLE). The controller is responsible for gathering data and transmitting data to a computer via Bluetooth Low Energy (BLE) channel. All data will be sent to a paired computer (laptop), which is responsible for data processing and the presentation of the recognition result. We also enabled smartphones with BLE functionality to control an LED on the primary node to identify each device. A median average filtering algorithm on software was adopted to avoid sensor signal jittering introduced by signal crosstalk, which is common in excessively small PCBs. All IMUs were connected via I2C communication.

import asyncio
                import math
                import numpy as np
                from datetime import datetime
                from typing import Callable, Any
                
                # from aioconsole import ainput
                from bleak import BleakClient, discover
                
                connection_flag = False
                
                selected_device = []
                
                node = 128  # max length
                filterLength = 4
                
                # save the temporary data
                qw = np.zeros((node, filterLength))
                qx = np.zeros((node, filterLength))
                qy = np.zeros((node, filterLength))
                qz = np.zeros((node, filterLength))
                
                # save the filtered data
                filter_qw = np.zeros(node)
                filter_qx = np.zeros(node)
                filter_qy = np.zeros(node)
                filter_qz = np.zeros(node)
                
                i = 0
                threshold = 0.4
                error_num = np.zeros((node, 4))
                filter_flag = True
                
                
                def filter(quaternions):
                    global i
                    sumqw, sumqx, sumqy, sumqz = [], [], [], []
                
                    for num, w, x, y, z in quaternions:
                        num = int(num)
                        if i < filterLength:
                            qw[num][i], qx[num][i], qy[num][i], qz[num][i] = w, x, y, z
                        else:
                            # remove the abnormal value
                            if w > 1.1 or w < -1.1:  # made by noise
                                w = qw[num][filterLength-1]
                            elif math.fabs(w - qw[num][filterLength-1]) > threshold and error_num[num][0] <= 4:  # made by noise or fast moving
                                w = qw[num][filterLength-1]
                                error_num[num][0] += 1  # detect error made by fast moving
                            else:
                                error_num[num][0] = 0
                
                            if x > 1.1 or x < -1.1:
                                x = qx[num][filterLength-1]
                            elif math.fabs(x - qx[num][filterLength-1]) > threshold and error_num[num][1] <= 4:
                                x = qx[num][filterLength-1]
                                error_num[num][1] += 1
                            else:
                                error_num[num][1] = 0
                
                            if y > 1.1 or y < -1.1:
                                y = qy[num][filterLength-1]
                            elif math.fabs(y - qy[num][filterLength-1]) > threshold and error_num[num][2] <= 4:
                                y = qy[num][filterLength-1]
                                error_num[num][2] += 1
                            else:
                                error_num[num][2] = 0
                
                            if z > 1.1 or z < -1.1:
                                z = qz[num][filterLength-1]
                            elif math.fabs(z - qz[num][filterLength-1]) > threshold and error_num[num][3] <= 4:
                                z = qz[num][filterLength-1]
                                error_num[num][3] += 1
                            else:
                                error_num[num][3] = 0
                
                            # update the buffer array / move left 1 unit
                            qw[num][:-1], qx[num][:-1], qy[num][:-1], qz[num][:-1] = qw[num][1:], qx[num][1:], qy[num][1:], qz[num][1:]
                            qw[num][filterLength-1], qx[num][filterLength-1], qy[num][filterLength-1], qz[num][filterLength-1] = w, x, y, z
                
                            # get the index of the max/min value in arrays
                            maxqw, maxqx, maxqy, maxqz = np.argmax(qw[num]), np.argmax(qx[num]), np.argmax(qy[num]), np.argmax(qz[num])
                            minqw, minqx, minqy, minqz = np.argmin(qw[num]), np.argmin(qx[num]), np.argmin(qy[num]), np.argmin(qz[num])
                
                            # sum except the max/min value
                            for k in range(filterLength):
                                if k != maxqw or k != minqw:
                                    sumqw.append(qw[num][k])
                                if k != maxqx or k != minqx:
                                    sumqx.append(qx[num][k])
                                if k != maxqy or k != minqy:
                                    sumqy.append(qy[num][k])
                                if k != maxqz or k != minqz:
                                    sumqz.append(qz[num][k])
                
                            # mean
                            w, x, y, z = sum(sumqw)/len(sumqw), sum(sumqx)/len(sumqx), sum(sumqy)/len(sumqy), sum(sumqz)/len(sumqz)
                
                            # update the buffer array / remove the max/min value
                            qw[num][maxqw], qw[num][minqw] = w, w
                            qx[num][maxqx], qx[num][minqx] = x, x
                            qy[num][maxqy], qy[num][minqy] = y, y
                            qz[num][maxqz], qz[num][minqz] = z, z
                
                            filter_qw[num], filter_qx[num], filter_qy[num], filter_qz[num] = w, x, y, z
                            sumqw, sumqx, sumqy, sumqz = [], [], [], []
                
                    if i < filterLength:
                        i += 1
                
                start_time = datetime.now()
                end_time = datetime.now()
                
                class DataToFile:
                    column_names = ["time", "delay", "data_value"]
                
                    def __init__(self):
                        pass
                
                    def write_to_txt(self, data_values, addr):
                        print("---------------------------")
                
                        with open('/tmp/sensors-{}.txt'.format(addr), 'a') as f:
                            f.write(data_values + '\n')
                        try:
                            quaternions = data_values.split(';')[:-1]
                            ts = int(quaternions[0])
                            quaternions = quaternions[1:]
                            for i in range(len(quaternions)):
                                quaternion = quaternions[i].split(' ')
                                quaternions[i] = quaternion[:5]
                
                            print('timestamp: {:+}ms'.format(ts))
                
                            quaternions = np.array(quaternions)
                            quaternions = quaternions.astype(np.float16)
                
                            filter(quaternions)
                
                            for num, w, x, y, z in quaternions:
                                num = int(num)
                
                                print("{} {:+4.2f} {:+4.2f} {:+4.2f} {:+4.2f}".format(num, filter_qw[num], filter_qx[num], filter_qy[num], filter_qz[num]))
                
                                with open("/tmp/sensor-{}-{}.txt".format(num, addr), 'w') as f:
                                    f.write("{:+4.2f} {:+4.2f} {:+4.2f} {:+4.2f}".format(filter_qw[num], filter_qx[num], filter_qy[num], filter_qz[num]))
                        except Exception as e:
                            print(e)
                            return
                
                
                class Connection:
                    client: BleakClient = None
                
                    def __init__(
                        self,
                        loop: asyncio.AbstractEventLoop,
                        read_characteristic: str,
                        write_characteristic: str,
                        data_dump_handler: Callable[[str, Any], None],
                        data_dump_size: int = 10,
                    ):
                        self.loop = loop
                        self.read_characteristic = read_characteristic
                        self.write_characteristic = write_characteristic
                        self.data_dump_handler = data_dump_handler
                
                        self.last_packet_time = datetime.now()
                        self.dump_size = data_dump_size
                        self.connected = False
                        self.connected_device = None
                
                        self.rx_data = ''
                        self.rx_timestamps = []
                        self.rx_delays = []
                
                
                    async def cleanup(self):
                        if self.client:
                            await self.client.stop_notify(read_characteristic)
                            await self.client.disconnect()
                
                    async def manager(self):
                        print("Starting connection manager.")
                        while True:
                            if self.client:
                                await self.connect()
                            else:
                                await self.select_device()
                                await asyncio.sleep(5.0)
                
                    async def connect(self):
                        if self.connected:
                            return
                        try:
                            print('try connecting')
                            await self.client.connect()
                            self.connected = await self.client.is_connected()
                            if self.connected:
                                print(F"Connected to {self.connected_device.name}")
                                await self.client.start_notify(self.read_characteristic, self.notification_handler,)
                                while True:
                                    if not self.connected:
                                        break
                                    await asyncio.sleep(1.0)
                            else:
                                print(f"Failed to connect to {self.connected_device.name}")
                        except Exception as e:
                            print(e)
                
                    async def select_device(self):
                        print("Bluetooh LE hardware warming up...")
                        await asyncio.sleep(2.0)  # Wait for BLE to initialize.
                        devices = await discover()
                
                        print("Please select device: ")
                        response = -1
                        while response == -1:
                            for i, device in enumerate(devices):
                                print(f"{i}: {device.name}")
                                if device.name == 'IMUsMonitor':
                                    response = i
                            if response == -1:
                                devices = await discover()
                
                        # while True:
                        print("Select device: ", response)
                
                        print(f"Connecting to {devices[response].name}")
                        self.connected_device = devices[response]
                        self.client = BleakClient(devices[response].address, loop=self.loop)
                
                    def record_time_info(self):
                        present_time = datetime.now()
                        self.rx_timestamps.append(present_time)
                        self.rx_delays.append((present_time - self.last_packet_time).microseconds)
                        self.last_packet_time = present_time
                
                    def clear_lists(self):
                        self.rx_data = ''
                
                    def notification_handler(self, sender: str, data: Any):
                        try:
                            self.rx_data = str(data, 'utf-8')
                        except:
                            print(data)
                            return
                        self.data_dump_handler(self.rx_data, sender)
                        self.clear_lists()
                
                async def main():
                    while True:
                #        connection_flag = self.client.is_connected()
                        # YOUR APP CODE WOULD GO HERE.
                        # print('nihao')
                        await asyncio.sleep(1)
                
                
                #############
                # App Main
                #############
                read_characteristic = "00001143-0000-1000-8000-00805f9b34fb"
                write_characteristic = "00001142-0000-1000-8000-00805f9b34fb"
                
                if __name__ == "__main__":
                
                    while True:
                        # Create the event loop.
                        loop = asyncio.get_event_loop()
                
                        data_to_file = DataToFile()
                        connection = Connection(loop, read_characteristic, write_characteristic, data_to_file.write_to_txt)
                        try:
                            asyncio.ensure_future(connection.manager())
                
                            loop.run_forever()
                        except KeyboardInterrupt:
                            print()
                            print("User stopped program.")
                            break
                        finally:
                            print("Disconnecting...")
                            loop.run_until_complete(connection.cleanup())