class BLE: DATA_CHARACTERISTIC_UUID = ("79b7950e-b94e-4293-bace-1db832cac77c") @staticmethod def format_notification_data(data): N = 2 # All data is assumed to be uint16s (2 bytes) grouped_data = [data[n:n + N] for n in range(0, len(data), N)] formatted_data = [ int.from_bytes(temp, byteorder="little", signed=False) for temp in grouped_data ] return formatted_data def __init__(self): self.client = None self.scanned_devices = [] def scan(self): asyncio.run(self.__scan()) return self.scanned_devices def connect(self, address): connected = asyncio.run(self.__connect(address)) return connected def disconnect(self): asyncio.run(self.__disconnect()) def enable_notifications(self, uuid, callback): asyncio.run(self.__enable_notifications(uuid, callback)) def disable_notifications(self, uuid): asyncio.run(self.__disable_notifications(uuid)) async def __disable_notifications(self, uuid): asyncio.create_task(self.client.stop_notify(uuid)) async def __enable_notifications(self, uuid, callback): asyncio.create_task(self.client.start_notify(uuid, callback)) async def __connect(self, address): loop = asyncio.get_event_loop() self.client = BleakClient(address, loop) try: await self.client.connect() return True except: return False async def __disconnect(self): asyncio.create_task(self.client.disconnect()) async def __scan(self): dev = await discover() for i in range(0, len(dev)): self.scanned_devices.append(dev[i])
class Ble(): ble_message_max_size = 512 command_uuid = uuid.UUID("48754770-0000-1000-8000-00805F9B34FB") log_uuid = uuid.UUID("48754771-0000-1000-8000-00805F9B34FB") keyboard_uuid = uuid.UUID("48754772-0000-1000-8000-00805F9B34FB") property_uuid = uuid.UUID("48754773-0000-1000-8000-00805F9B34FB") def __init__(self, hook_keyboard, mac_address): self.hook_keyboard = hook_keyboard self.connected = False self.notification_data = None self.mac_address = mac_address self.server = None self.client = None self.log_msg = "" self.log_level = 0 self.terminating = False self.loop = asyncio.get_event_loop() logging.getLogger('bleak').setLevel(logging.CRITICAL) def _disconnected_callback(self, client): logging.debug("disconnected callback for: %s", client) self.connected = False def _command_notyfy_callback(self, _sender: int, data: bytearray): self.notification_data = data async def _command(self, command_id, data, wait_for_answer): payload = bytes([command_id]) payload += data self.notification_data = None _answer = await self.client.write_gatt_char(self.command_uuid, payload, not wait_for_answer) if wait_for_answer: while not self.notification_data: await asyncio.sleep(0.01) return self.notification_data def command(self, command_id, data, wait_for_answer=True): if len(data) > self.ble_message_max_size: logging.error("try to send too long data %s for command: %d", data.hex(), command_id) return None else: return self.loop.run_until_complete(self._command(command_id, data, wait_for_answer)) async def _key_pressed(self, scan_code, key_name): data = scan_code.to_bytes(2, byteorder='big', signed=True) + key_name.encode("utf-8") await self.client.write_gatt_char(self.keyboard_uuid, data, False) def key_pressed(self, scan_code, key_name): self.loop.create_task(self._key_pressed(scan_code, key_name)) def _log_callback(self, _sender: int, data: bytearray): is_first = not self.log_msg if is_first: level = data[0] is_last = data[-1] != '\f'.encode("utf-8")[0] self.log_msg += data[(1 if is_first else 0) : (len(data) if is_last else -1)].decode("utf-8") # skip level and \f if is_last: logging.log(level, self.log_msg) self.log_msg = "" def _detection_callback(self, device, _advertisement_data): if device.name and device.name.startswith("HuGo"): reduced_address = device.address.replace(':', '') if not self.mac_address or self.mac_address.lower() == reduced_address.lower(): self.server = device.address logging.debug("HuGo has been found: '%s, %s'", device.name, device.address) async def _scan(self): logging.info("Scanning...") scanner = BleakScanner() scanner.register_detection_callback(self._detection_callback) logging.debug("searching HuGo device") waiting = 0.5 # to be cough power_up window in case of power save countdown = int(90 / waiting) #~1.5 min self.server = None while countdown: await scanner.start() await asyncio.sleep(waiting) await scanner.stop() if self.server: logging.debug("server found %s", self.server ) logging.info("Scanning DONE") return True countdown -= 1 logging.info("Scanning FAILED") return False async def _connect(self): if not self.server: if not await self._scan(): logging.error("HuGo not found") return False logging.info("connecting to %s", str(self.server)) for _i in range(9): # ~90 sec try: self.client = BleakClient(self.server, loop=self.loop) #it is better to create client again when the connection fails. in some cases the connections is created partially and is not possible to establish the new one self.connected = await self.client.connect() logging.debug("device was connected with status: %s", self.connected) if not self.connected: return False self.client.set_disconnected_callback(self._disconnected_callback) await self.client.start_notify(self.command_uuid, self._command_notyfy_callback) #FIXME: should be removed when logging is finished try: await self.client.start_notify(self.log_uuid, self._log_callback) except Exception as error: logging.warning("logging start_notify failed %s", error) return True except Exception as e: logging.debug("connection error: %s", e) logging.debug("device was not connected via BLE") return False def connect(self): return self.loop.run_until_complete(self._connect()) def disconnect(self): logging.info("disconnecting...") self.loop.run_until_complete(self.client.disconnect()) logging.info("disconnecting DONE") self.connected = False def keyboard_monitor(self, key_event): if key_event.name == "esc": keyboard.unhook_all() self.terminating = True else: self.key_pressed(key_event.scan_code, key_event.name) async def _async_monitor(self): if self.hook_keyboard: logging.info("Keyboard monitoring. Press 'Esc' to finish.") keyboard.on_press(self.keyboard_monitor, suppress=True) while True: if self.terminating: logging.debug("terminating...") break if not self.connected: await self._connect() await asyncio.sleep(0.1) def monitor(self): try: self.loop.run_until_complete(self._async_monitor()) except KeyboardInterrupt: self.terminating = True
class PolarClient: def __init__(self, path, name): # Configuration. parser = argparse.ArgumentParser() parser.add_argument("-i", "--inifile", default=os.path.join(path, name + '.ini'), help="name of the configuration file") args = parser.parse_args() config = configparser.ConfigParser(inline_comment_prefixes=('#', ';')) config.read(args.inifile) # Redis. try: rds = redis.StrictRedis(host=config.get('redis', 'hostname'), port=config.getint('redis', 'port'), db=0, charset='utf-8', decode_responses=True) except redis.ConnectionError as e: raise RuntimeError(e) # Combine the patching from the configuration file and Redis. self.patch = EEGsynth.patch(config, rds) # BLE client. self.loop = asyncio.get_event_loop() self.ble_client = BleakClient(self.patch.getstring("input", "mac"), loop=self.loop) self.monitor = EEGsynth.monitor(name=name, debug=self.patch.getint( "general", "debug")) async def connect(self): self.monitor.info("Trying to connect to Polar belt {0}".format( self.patch.getstring("input", "mac"))) await self.ble_client.connect() await self.ble_client.start_notify( self.patch.getstring("input", "hr_uuid"), self.data_handler) self.monitor.success("Connected to Polar belt {0}".format( self.patch.getstring("input", "mac"))) def start(self): asyncio.ensure_future(self.connect()) self.loop.run_forever() def stop(self): asyncio.ensure_future( self.ble_client.stop_notify( self.patch.getstring("input", "hr_uuid"))) asyncio.ensure_future(self.ble_client.disconnect()) self.monitor.success("Disconnected from Polar belt {0}".format( self.patch.getstring("input", "mac"))) sys.exit() def data_handler(self, sender, data): # sender (UUID) unused but required by Bleak API """ data has up to 6 bytes: byte 1: flags 00 = only HR 16 = HR and IBI(s) byte 2: HR byte 3 and 4: IBI1 byte 5 and 6: IBI2 (if present) byte 7 and 8: IBI3 (if present) etc. Polar H10 Heart Rate Characteristics (UUID: 00002a37-0000-1000-8000-00805f9b34fb): + Energy expenditure is not transmitted + HR only transmitted as uint8, no need to check if HR is transmitted as uint8 or uint16 (necessary for bpm > 255) Acceleration and raw ECG only available via Polar SDK """ bytes = list(data) hr = None ibis = [] if bytes[0] == 00: hr = data[1] if bytes[0] == 16: hr = data[1] for i in range(2, len(bytes), 2): ibis.append(data[i] + 256 * data[i + 1]) if ibis: for ibi in ibis: self.patch.setvalue(self.patch.getstring("output", "key_ibi"), ibi) if hr: self.patch.setvalue(self.patch.getstring("output", "key_hr"), hr) print("Received HR={0}, IBI(s)={1}".format(hr, ibis))
class BASE_BLE_DEVICE: def __init__(self, scan_dev, init=False, name=None, lenbuff=100, rssi=None, log=None): # BLE self.ble_client = None if hasattr(scan_dev, 'address'): self.UUID = scan_dev.address self.name = scan_dev.name self.rssi = scan_dev.rssi self.address = self.UUID else: self.UUID = scan_dev self.name = name self.rssi = rssi self.address = self.UUID self.connected = False self.services = {} self.services_rsum = {} self.services_rsum_handles = {} self.chars_desc_rsum = {} self.readables = {} self.writeables = {} self.notifiables = {} self.readables_handles = {} self.writeables_handles = {} self.notifiables_handles = {} self.loop = asyncio.get_event_loop() # self.raw_buff_queue = asyncio.Queue() self.kb_cmd = None self.is_notifying = False self.cmd_finished = True self.len_buffer = lenbuff # self.bytes_sent = 0 self.buff = b'' self.raw_buff = b'' self.prompt = b'>>> ' self.response = '' self._cmdstr = '' self._cmdfiltered = False self._kbi = '\x03' self._banner = '\x02' self._reset = '\x04' self._traceback = b'Traceback (most recent call last):' self._flush = b'' self.output = None self.platform = None self.break_flag = None self.log = log # if init: self.connect() # do connect def set_event_loop(self, loop): self.loop = loop # self.ble_client.loop = loop async def connect_client(self, n_tries=3, log=True): n = 0 self.ble_client = BleakClient(self.UUID) while n < n_tries: try: await asyncio.wait_for(self.ble_client.connect(timeout=3), timeout=60) self.connected = await self.ble_client.is_connected() if self.connected: self.name = self.ble_client._device_info.name() if log: self.log.info("Connected to: {}".format(self.UUID)) break except Exception as e: if log: if not self.break_flag: self.log.error(e) self.log.info('Trying again...') else: break time.sleep(1) n += 1 async def disconnect_client(self, log=True, timeout=None): if timeout: await asyncio.wait_for(self.ble_client.disconnect(), timeout=timeout) else: await self.ble_client.disconnect() self.connected = await self.ble_client.is_connected() if not self.connected: if log: self.log.info("Disconnected successfully") def connect(self, n_tries=3, show_servs=False, log=True): self.loop.run_until_complete( self.connect_client(n_tries=n_tries, log=log)) self.get_services(log=show_servs) def is_connected(self): return self.loop.run_until_complete(self.ble_client.is_connected()) def disconnect(self, log=True, timeout=None): self.loop.run_until_complete( self.disconnect_client(log=log, timeout=timeout)) def set_disconnected_callback(self, callback): self.ble_client.set_disconnected_callback(callback) def disconnection_callback(self, client): self.connected = False # RSSI def get_RSSI(self): if hasattr(self.ble_client, 'get_rssi'): self.rssi = self.loop.run_until_complete( self.ble_client.get_rssi()) else: self.rssi = 0 return self.rssi # SERVICES def get_services(self, log=True): for service in self.ble_client.services: if log: print("[Service] {0}: {1}".format(service.uuid.lower(), service.description)) self.services[service.description] = { 'UUID': service.uuid.lower(), 'CHARS': {} } self.services_rsum_handles[service.description] = [] for char in service.characteristics: self.services_rsum_handles[service.description].append( char.handle) if "read" in char.properties: try: self.readables[char.description] = char.uuid self.readables_handles[char.handle] = char.description except Exception as e: print(e) if "notify" in char.properties or 'indicate' in char.properties: try: self.notifiables[char.description] = char.uuid self.notifiables_handles[ char.handle] = char.description except Exception as e: print(e) if "write" in char.properties or 'write-without-response' in char.properties: try: self.writeables[char.description] = char.uuid self.writeables_handles[char.handle] = char.description except Exception as e: print(e) try: self.services[service.description]['CHARS'][char.uuid] = { char.description: ",".join(char.properties), 'Descriptors': { descriptor.uuid: descriptor.handle for descriptor in char.descriptors } } except Exception as e: print(e) self.chars_desc_rsum[char.description] = {} for descriptor in char.descriptors: self.chars_desc_rsum[char.description][ descriptor.description] = descriptor.handle if log: try: print( "\t[Characteristic] {0}: ({1}) | Name: {2}".format( char.uuid, ",".join(char.properties), char.description)) except Exception as e: print(e) if log: for descriptor in char.descriptors: print("\t\t[Descriptor] [{0}]: {1} (Handle: {2}) ". format(descriptor.uuid, descriptor.description, descriptor.handle)) self.services_rsum = { key: [ list(list(val['CHARS'].values())[i].keys())[0] for i in range(len(list(val['CHARS'].values()))) ] for key, val in self.services.items() } # WRITE/READ SERVICES def fmt_data(self, data, CR=True): if sys.platform == 'linux': if CR: return bytearray(data + '\r', 'utf-8') else: return bytearray(data, 'utf-8') else: if CR: return bytes(data + '\r', 'utf-8') else: return bytes(data, 'utf-8') async def as_read_descriptor(self, handle): return bytes(await self.ble_client.read_gatt_descriptor(handle)) def read_descriptor_raw(self, key=None, char=None): if key is not None: # print(self.chars_desc_rsum[char]) if key in list(self.chars_desc_rsum[char]): data = self.loop.run_until_complete( self.as_read_descriptor(self.chars_desc_rsum[char][key])) return data else: print('Descriptor not available for this characteristic') def read_descriptor(self, key=None, char=None, data_fmt="utf8"): try: if data_fmt == 'utf8': data = self.read_descriptor_raw(key=key, char=char).decode('utf8') return data else: data, = struct.unpack(data_fmt, self.read_char_raw(key=key, char=char)) return data except Exception as e: print(e) async def as_read_char(self, uuid): return bytes(await self.ble_client.read_gatt_char(uuid)) def read_char_raw(self, key=None, uuid=None, handle=None): if key is not None: if key in list(self.readables.keys()): if handle: data = self.loop.run_until_complete( self.as_read_char(handle)) else: data = self.loop.run_until_complete( self.as_read_char(self.readables[key])) return data else: print('Characteristic not readable') else: if uuid is not None: if uuid in list(self.readables.values()): if handle: data = self.loop.run_until_complete( self.as_read_char(handle)) else: data = self.loop.run_until_complete( self.as_read_char(uuid)) return data else: print('Characteristic not readable') def read_char(self, key=None, uuid=None, data_fmt="<h", handle=None): try: if data_fmt == 'utf8': # Here function that handles format and unpack properly data = self.read_char_raw(key=key, uuid=uuid, handle=handle).decode('utf8') return data else: if data_fmt == 'raw': data = self.read_char_raw(key=key, uuid=uuid, handle=handle) return data else: data, = struct.unpack( data_fmt, self.read_char_raw(key=key, uuid=uuid, handle=handle)) return data except Exception as e: print(e) async def as_write_char(self, uuid, data): await self.ble_client.write_gatt_char(uuid, data) def write_char(self, key=None, uuid=None, data=None, handle=None): if key is not None: if key in list(self.writeables.keys()): if handle: data = self.loop.run_until_complete( self.as_write_char(handle, data)) else: data = self.loop.run_until_complete( self.as_write_char(self.writeables[key], data)) # make fmt_data return data else: print('Characteristic not writeable') else: if uuid is not None: if uuid in list(self.writeables.values()): if handle: data = self.loop.run_until_complete( self.as_write_char(handle, data)) else: data = self.loop.run_until_complete( self.as_write_char(uuid, data)) # make fmt_data return data else: print('Characteristic not writeable') def write_char_raw(self, key=None, uuid=None, data=None): if key is not None: if key in list(self.writeables.keys()): data = self.loop.run_until_complete( self.as_write_char(self.writeables[key], self.fmt_data( data, CR=False))) # make fmt_data return data else: print('Characteristic not writeable') else: if uuid is not None: if uuid in list(self.writeables.values()): data = self.loop.run_until_complete( self.as_write_char(uuid, self.fmt_data( data, CR=False))) # make fmt_data return data else: print('Characteristic not writeable') def read_callback(self, sender, data): self.raw_buff += data def read_callback_follow(self, sender, data): try: if not self._cmdfiltered: cmd_filt = bytes(self._cmdstr + '\r\n', 'utf-8') data = b'' + data data = data.replace(cmd_filt, b'', 1) # data = data.replace(b'\r\n>>> ', b'') self._cmdfiltered = True else: try: data = b'' + data # data = data.replace(b'\r\n>>> ', b'') except Exception as e: pass self.raw_buff += data if self.prompt in data: data = data.replace(b'\r', b'').replace(b'\r\n>>> ', b'').replace( b'>>> ', b'').decode('utf-8', 'ignore') if data != '': print(data, end='') else: data = data.replace(b'\r', b'').replace(b'\r\n>>> ', b'').replace( b'>>> ', b'').decode('utf-8', 'ignore') print(data, end='') except KeyboardInterrupt: print('CALLBACK_KBI') pass # async def as_write_read_waitp(self, data, rtn_buff=False): await self.ble_client.start_notify(self.readables['TX'], self.read_callback) if len(data) > self.len_buffer: for i in range(0, len(data), self.len_buffer): await self.ble_client.write_gatt_char( self.writeables['RX'], data[i:i + self.len_buffer]) else: await self.ble_client.write_gatt_char(self.writeables['RX'], data) while self.prompt not in self.raw_buff: await asyncio.sleep(0.01, loop=self.loop) await self.ble_client.stop_notify(self.readables['TX']) if rtn_buff: return self.raw_buff async def as_write_read_follow(self, data, rtn_buff=False): if not self.is_notifying: try: await self.ble_client.start_notify(self.readables['TX'], self.read_callback_follow) self.is_notifying = True except Exception as e: pass if len(data) > self.len_buffer: for i in range(0, len(data), self.len_buffer): await self.ble_client.write_gatt_char( self.writeables['RX'], data[i:i + self.len_buffer]) else: await self.ble_client.write_gatt_char(self.writeables['RX'], data) while self.prompt not in self.raw_buff: try: await asyncio.sleep(0.01, loop=self.loop) except KeyboardInterrupt: print('Catch here1') data = bytes(self._kbi, 'utf-8') await self.ble_client.write_gatt_char(self.writeables['RX'], data) if self.is_notifying: try: await self.ble_client.stop_notify(self.readables['TX']) self.is_notifying = False except Exception as e: pass self._cmdfiltered = False if rtn_buff: return self.raw_buff def write_read(self, data='', follow=False, kb=False): if not follow: if not kb: try: self.loop.run_until_complete( self.as_write_read_waitp(data)) except Exception as e: print(e) else: asyncio.ensure_future(self.as_write_read_waitp(data), loop=self.loop) # wait here until there is raw_buff else: if not kb: try: self.loop.run_until_complete( self.as_write_read_follow(data)) except Exception as e: print('Catch here0') print(e) else: asyncio.ensure_future(self.as_write_read_follow(data, rtn_buff=True), loop=self.loop) def send_recv_cmd(self, cmd, follow=False, kb=False): data = self.fmt_data(cmd) # make fmt_data n_bytes = len(data) self.write_read(data=data, follow=follow, kb=kb) return n_bytes def write(self, cmd): data = self.fmt_data(cmd, CR=False) # make fmt_data n_bytes = len(data) self.write_char_raw(key='RX', data=data) return n_bytes def read_all(self): try: return self.raw_buff except Exception as e: print(e) return self.raw_buff def flush(self): flushed = 0 self.buff = self.read_all() flushed += 1 self.buff = b'' def wr_cmd(self, cmd, silent=False, rtn=True, rtn_resp=False, long_string=False, follow=False, kb=False): self.output = None self.response = '' self.raw_buff = b'' self.buff = b'' self._cmdstr = cmd # self.flush() self.bytes_sent = self.send_recv_cmd(cmd, follow=follow, kb=kb) # make fmt_datas # time.sleep(0.1) # self.buff = self.read_all()[self.bytes_sent:] self.buff = self.read_all() if self.buff == b'': # time.sleep(0.1) self.buff = self.read_all() # print(self.buff) # filter command if follow: silent = True cmd_filt = bytes(cmd + '\r\n', 'utf-8') self.buff = self.buff.replace(cmd_filt, b'', 1) if self._traceback in self.buff: long_string = True if long_string: self.response = self.buff.replace(b'\r', b'').replace( b'\r\n>>> ', b'').replace(b'>>> ', b'').decode('utf-8', 'ignore') else: self.response = self.buff.replace(b'\r\n', b'').replace( b'\r\n>>> ', b'').replace(b'>>> ', b'').decode('utf-8', 'ignore') if not silent: if self.response != '\n' and self.response != '': print(self.response) else: self.response = '' if rtn: self.get_output() if self.output == '\n' and self.output == '': self.output = None if self.output is None: if self.response != '' and self.response != '\n': self.output = self.response if rtn_resp: return self.output async def as_wr_cmd(self, cmd, silent=False, rtn=True, rtn_resp=False, long_string=False, follow=False, kb=False): self.output = None self.response = '' self.raw_buff = b'' self.buff = b'' self._cmdstr = cmd self.cmd_finished = False # self.flush() data = self.fmt_data(cmd) # make fmt_data n_bytes = len(data) self.bytes_sent = n_bytes # time.sleep(0.1) # self.buff = self.read_all()[self.bytes_sent:] if follow: self.buff = await self.as_write_read_follow(data, rtn_buff=True) else: self.buff = await self.as_write_read_waitp(data, rtn_buff=True) if self.buff == b'': # time.sleep(0.1) self.buff = self.read_all() # print(self.buff) # filter command if follow: silent = True cmd_filt = bytes(cmd + '\r\n', 'utf-8') self.buff = self.buff.replace(cmd_filt, b'', 1) if self._traceback in self.buff: long_string = True if long_string: self.response = self.buff.replace(b'\r', b'').replace( b'\r\n>>> ', b'').replace(b'>>> ', b'').decode('utf-8', 'ignore') else: self.response = self.buff.replace(b'\r\n', b'').replace( b'\r\n>>> ', b'').replace(b'>>> ', b'').decode('utf-8', 'ignore') if not silent: if self.response != '\n' and self.response != '': print(self.response) else: self.response = '' if rtn: self.get_output() if self.output == '\n' and self.output == '': self.output = None if self.output is None: if self.response != '' and self.response != '\n': self.output = self.response self.cmd_finished = True if rtn_resp: return self.output def kbi(self, silent=True, pipe=None): if pipe is not None: self.wr_cmd(self._kbi, silent=silent) bf_output = self.response.split('Traceback')[0] traceback = 'Traceback' + self.response.split('Traceback')[1] if bf_output != '' and bf_output != '\n': pipe(bf_output) pipe(traceback, std='stderr') else: self.wr_cmd(self._kbi, silent=silent) async def as_kbi(self): for i in range(1): print('This is buff: {}'.format(self.raw_buff)) await asyncio.sleep(1, loop=self.loop) data = bytes(self._kbi + '\r', 'utf-8') await self.ble_client.write_gatt_char(self.writeables['RX'], data) def banner(self, pipe=None, kb=False, follow=False): self.wr_cmd(self._banner, silent=True, long_string=True, kb=kb, follow=follow) if pipe is None and not follow: print(self.response.replace('\n\n', '\n')) else: if pipe: pipe(self.response.replace('\n\n', '\n')) def reset(self, silent=True): if not silent: print('Rebooting device...') self.write_char_raw(key='RX', data=self._reset) if not silent: print('Done!') async def as_reset(self, silent=True): if not silent: print('Rebooting device...') await self.as_write_char(self.writeables['RX'], bytes(self._reset, 'utf-8')) if not silent: print('Done!') return None def get_output(self): try: self.output = ast.literal_eval(self.response) except Exception as e: if 'bytearray' in self.response: try: self.output = bytearray( ast.literal_eval( self.response.strip().split('bytearray')[1])) except Exception as e: pass else: if 'array' in self.response: try: arr = ast.literal_eval( self.response.strip().split('array')[1]) self.output = array(arr[0], arr[1]) except Exception as e: pass pass