class ActiveClient: def __init__(self, address, cleanupCallback, bleAdapterAddress): self.address = address self.client = BleakClient(address, adapter=bleAdapterAddress) self.cleanupCallback = cleanupCallback self.client.set_disconnected_callback(self.forcedDisconnect) # Dict with service UUID as key, handle as value. self.services = {} # Dict with characteristic UUID as key, handle as value. self.characteristics = {} # Current callbacks for notifications, in the form of: def handleNotification(uuid: str, data) # Characteristic UUID is key, callback is value. self.notificationCallbacks = {} # Notifications we subscribed to. # Handle as key, UUID as value. self.notificationSubscriptions = {} def forcedDisconnect(self, data): BleEventBus.emit(SystemBleTopics.forcedDisconnect, self.address) self.cleanupCallback() async def isConnected(self): return await self.client.is_connected() async def subscribeNotifications(self, characteristicUuid: str, callback): _LOGGER.debug( f"register callback for notifications to uuid={characteristicUuid}" ) if characteristicUuid in self.notificationCallbacks: _LOGGER.error( f"There is already a callback registered for {characteristicUuid}" ) if characteristicUuid not in self.notificationSubscriptions.values(): # handle = self.characteristics.get(uuid, None) # if handle is not None: _LOGGER.debug(f"subscribe to uuid={characteristicUuid}") handle = self.characteristics[characteristicUuid] await self.client.start_notify(characteristicUuid, self._resultNotificationHandler) self.notificationSubscriptions[handle] = characteristicUuid self.notificationCallbacks[characteristicUuid] = callback def unsubscribeNotifications(self, characteristicUuid: str): _LOGGER.debug( f"remove callback for notifications to uuid={characteristicUuid}") self.notificationCallbacks.pop(characteristicUuid, None) def _resultNotificationHandler(self, characteristicHandle, data): uuid = self.notificationSubscriptions.get(characteristicHandle, None) if uuid is None: _LOGGER.error(f"UUID not found for handle {characteristicHandle}") callback = self.notificationCallbacks.get(uuid, None) if callback is not None: callback(uuid, data)
async def run(self, loop): while True: if self.keep_connected: try: client = BleakClient(self.address, loop=loop) await client.connect() self.was_disconnected = False def disconnect_callback(client, future=None): print("Got disconnected from scale") self.was_disconnected = True client.set_disconnected_callback(disconnect_callback) await client.is_connected() self.scale_connected = True self.hub.publish(topics.TOPIC_SCALE_CONNECTED, self.scale_connected) await client.start_notify(self.ACAIA_CHR_UUID, self.notification_handler) await self.ident(client) while True: if not self.was_disconnected and self.keep_connected: await self.send_heartbeat(client) self.hub.publish(topics.TOPIC_SCALE_HEARTBEAT_SENT, True) await asyncio.sleep(3) elif not self.keep_connected: await client.disconnect() break else: # if self.was_disconnected # for some weird reason, we *really* can't call client.disconnect in this case when using bluez break self.scale_connected = False self.hub.publish(topics.TOPIC_SCALE_CONNECTED, self.scale_connected) except BleakError: continue else: await asyncio.sleep(1)
async def connect_to_alpha(): alpha_dev = None print('--------------------------') print('-- Scanning for devices --') print('--------------------------') devices = await discover() for d in devices: print(d) if 'FANTMalpha' in d.name: alpha_dev = d print('***Found Device: {0}'.format(alpha_dev)) client = BleakClient(alpha_dev.address) try: await client.connect() except Exception as e: print(e) print('-----------------------') print('-- Scanning services --') print('-----------------------') svcs = await client.get_services() nus_svc = None for s in svcs: print(s) if 'Nordic UART Service' in s.description: nus_svc = s print('***Found Service: {0}'.format(nus_svc)) print('------------------------------') print('-- Scanning characteristics --') print('------------------------------') nus_svc_tx = None for ch in nus_svc.characteristics: print(ch) if 'Nordic UART TX' in ch.description: nus_svc_tx = ch print('***Found Characteristic: {0}'.format(nus_svc_tx)) print('---------------------------------') print('-- Listening for notifications --') print('---------------------------------') client.set_disconnected_callback(disconnect_callback) await client.start_notify(nus_svc_tx.uuid, notify_callback) return client
async def ble_service(loop: asyncio.AbstractEventLoop, tx: asyncio.Queue, disconnected_event: asyncio.Event): async def put_to_queue(data): ble_packet_event.inc(1) await tx.put(data) def notification_handler(sender, data): loop.create_task(put_to_queue(data)) logger.info("scanning client...") client = BleakClient(BLE_ADDR, loop=loop) logger.info("connecting to device {0} ...".format(BLE_ADDR)) await client.connect() x = await client.is_connected() logger.info("connected: {0}".format(x)) await client.start_notify(BLE_CHARACTERISTIC_UUID, notification_handler) logger.info("notification registered") def disconnect_callback(client): loop.call_soon_threadsafe(disconnected_event.set) client.set_disconnected_callback(disconnect_callback) try: await disconnected_event.wait() except: await client.stop_notify(BLE_CHARACTERISTIC_UUID, notification_handler) logger.info("disconnected from device") await tx.put(None) await asyncio.sleep(0.5)
class PybricksHubConnection(HubDataReceiver): async def connect(self, address): self.logger.info("Connecting to {0}".format(address)) self.client = BleakClient(address) await self.client.connect() self.client.set_disconnected_callback(self.update_state_disconnected) self.logger.info("Connected successfully!") await self.client.start_notify(bleNusCharTXUUID, self.update_data_buffer) async def disconnect(self): await self.client.stop_notify(bleNusCharTXUUID) await self.client.disconnect() async def write(self, data): n = 20 chunks = [data[i:i + n] for i in range(0, len(data), n)] for i, chunk in enumerate(chunks): self.logger.debug("\t\t\t\tTX: {0}".format(chunk)) await asyncio.sleep(0.05) await self.client.write_gatt_char(bleNusCharRXUUID, bytearray(chunk)) async def send_message(self, data): """Send bytes to the hub, and check if reply matches checksum.""" if len(data) > 100: raise ValueError("Cannot send this much data at once") # Compute expected reply checksum = 0 for b in data: checksum ^= b # Send the data await self.write(data) # Await the reply reply = await self.wait_for_checksum() self.logger.debug("expected: {0}, reply: {1}".format(checksum, reply)) # Raise errors if we did not get the checksum we wanted if reply is None: raise OSError("Did not receive reply.") if checksum != reply: raise ValueError("Did not receive expected checksum.") async def download_and_run(self, mpy): # Get length of file and send it as bytes to hub length = len(mpy).to_bytes(4, byteorder='little') await self.send_message(length) # Divide script in chunks of bytes n = 100 chunks = [mpy[i:i + n] for i in range(0, len(mpy), n)] # Send the data chunk by chunk for i, chunk in enumerate(chunks): self.logger.info("Sending: {0}%".format( round((i + 1) / len(chunks) * 100))) await self.send_message(chunk) # Wait for the program to finish await self.wait_until_not_running()
class SensorClient(QObject): """(Re-) connect a BLE client to a server at MAC. Notes ----- external disconnection: - sensor lost skin contact - sensor out of range internal disconnection: - user requests connection to another sensor `await x` means "do `x` and wait for it to return". In the meantime, if `x` chooses to suspend execution, other tasks which have already started elsewhere may run. Also see [1]. References ---------- [1] https://hynek.me/articles/waiting-in-asyncio/ """ ibi_update = Signal(object) def __init__(self): super().__init__() self._ble_client = None self._mac = None self._listening = False self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def run(self): """Start the (empty) asyncio event loop.""" self.loop.run_forever() async def stop(self): """Shut down client before app is closed.""" await self._discard_client() self.loop.stop() async def connect_client(self, mac): """Connect to BLE server.""" if mac == self._mac: print("Client already connected to this MAC.") return await self._discard_client() self._mac = mac await self._connect() async def _connect(self): """Try connecting to current MAC.""" self._ble_client = BleakClient( self._mac, disconnected_callback=self._cleanup_external_disconnection) print(f"Trying to connect client {self._ble_client}") self._listening = False max_retries = 5 n_retries = 0 while not self._listening: if n_retries > max_retries: print( f"Stopped trying to connect to {self._mac} after {max_retries} attempts." ) await self._discard_client() break try: print(f"Connecting to {self._mac}") await self._ble_client.connect( ) # potential exceptions: BleakError (device not found), asyncio TimeoutError print(f"Starting notification for {self._mac}.") await self._ble_client.start_notify(HR_UUID, self._data_handler) self._listening = True except (BleakError, asyncio.exceptions.TimeoutError, Exception) as error: print(f"Connection exception: {error}\nRetrying...") n_retries += 1 def _cleanup_external_disconnection(self, client): """Handle external disconnection.""" self.loop.create_task(self._discard_client()) async def _discard_client(self): try: self._ble_client.set_disconnected_callback( None) # deregister disconnection callback await self._ble_client.disconnect() print("Disconnected client.") except (Exception, BleakError) as error: print(f"Couldn't disconnect client: {error}.") finally: # runs before try block exits self._ble_client = None self._mac = None print("Discarded client.") def _data_handler( self, caller, data): # caller (UUID) unused but mandatory positional argument """ IMPORTANT: Polar H10 (H9) records IBIs in 1/1024 seconds format, i.e. not milliseconds! 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) if bytes[0] == 16: for i in range(2, len(bytes), 2): ibi = data[i] + 256 * data[i + 1] ibi = ceil(ibi / 1024 * 1000) # convert 1/1024 sec format to milliseconds print(f"IBI: {ibi}") self.ibi_update.emit(ibi)
class BLEStreamConnection(): def __init__(self, char_rx_UUID, char_tx_UUID, mtu, EOL): """Initializes and configures connection settings. Arguments: char_rx_UUID (str): UUID for RX. char_rx_UUID (str): UUID for TX. mtr (int): Maximum number of bytes per write operation. EOL (bytes): Character sequence that signifies end of line. """ # Save given settings self.char_rx_UUID = char_rx_UUID self.char_tx_UUID = char_tx_UUID self.EOL = EOL self.mtu = mtu # Create empty rx buffer self.char_buf = bytearray(b'') # Get a logger and set at given level self.logger = logging.getLogger('BLEStreamConnection') handler = logging.StreamHandler() formatter = logging.Formatter( '%(asctime)s: %(levelname)7s: %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) self.logger.setLevel(logging.WARNING) # Are we connected? self.connected = False def char_handler(self, char): """Handles new incoming characters. Intended to be overridden. Arguments: char (int): Character/byte to process. Returns: int or None: Processed character. """ self.logger.debug("RX CHAR: {0} ({1})".format(chr(char), char)) return char def line_handler(self, line): """Handles new incoming lines. Intended to be overridden. The default just prints the line that comes in. Arguments: line (bytearray): Line to process. """ print(line) def disconnected_handler(self, client, *args): """Handles disconnected event. Intended to be overridden.""" self.logger.info("Disconnected by server.") self.connected = False def _data_handler(self, sender, data): """Handles new incoming data. Calls char and line parsers when ready. Arguments: sender (str): Sender uuid. data (bytearray): Incoming data. """ self.logger.debug("RX DATA: {0}".format(data)) # For each new character, call its handler and add to buffer if any for byte in data: append = self.char_handler(byte) if append is not None: self.char_buf.append(append) # Some applications don't have any lines to process if self.EOL is None: return # Break up data into lines and take those out of the buffer lines = [] while True: # Find and split at end of line index = self.char_buf.find(self.EOL) # If no more line end is found, we are done if index < 0: break # If we found a line, save it, and take it from the buffer lines.append(self.char_buf[0:index]) del self.char_buf[0:index + len(self.EOL)] # Call handler for each line that we found for line in lines: self.line_handler(line) async def connect(self, address): """Creates connection to server at given address. Arguments: address (str): Client address """ print("Connecting to {0}".format(address)) self.client = BleakClient(address) await self.client.connect() self.client.set_disconnected_callback(self.disconnected_handler) await self.client.start_notify(self.char_tx_UUID, self._data_handler) print("Connected successfully!") self.connected = True async def disconnect(self): """Disconnects the client from the server.""" await self.client.stop_notify(self.char_tx_UUID) if self.connected: self.logger.debug("Disconnecting...") await self.client.disconnect() self.logger.info("Disconnected by client.") self.connected = False async def write(self, data, pause=0.05): """Write bytes to the server, split to chunks of maximum mtu size. Arguments: data (bytearray): Data to be sent to the server. pause (float): Time between chunks of data. """ # Chop data into chunks of maximum tranmission size chunks = [data[i:i + self.mtu] for i in range(0, len(data), self.mtu)] # Send the chunks one by one for chunk in chunks: self.logger.debug("TX CHUNK: {0}".format(chunk)) # Send one chunk await self.client.write_gatt_char(self.char_rx_UUID, bytearray(chunk)) # Give server some time to process chunk await asyncio.sleep(pause)
class BleUartPinCtrl: NORDIC_UART_SERVICE = "6e400001-b5a3-f393-e0a9-e50e24dcca9e" NUS_RX_CHAR = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" NUS_TX_CHAR = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" def __init__(self): # The bleak client instance self.client = None # The BLEDevice representing the remote device self.device = None @classmethod async def list_devices(cls): """ Finds and prints available BLE devices """ devices = await BleakScanner.discover() for device in devices: print(device) @classmethod async def new(cls, device_name: str = None): """ Creates and initializes a BLE connection to a target device running the Nordic UART service. :param device_name: Name of the device to connect to. """ self = BleUartPinCtrl() # Gather and look through devices for one that matches the target name devices = await BleakScanner.discover() # Scan through the devices to see if the desired device is available for device in devices: if device_name is not None: # search by device name if device.name == device_name: self.device = device if self.NORDIC_UART_SERVICE not in self.device.metadata[ 'uuids']: raise RuntimeError( "Device with given name does not have the Nordic UART service", self.device, self.NORDIC_UART_SERVICE) break else: # just grab the first one with the Nordic UART service if self.NORDIC_UART_SERVICE in device.metadata['uuids']: self.device = device break if self.device is None: raise RuntimeError( "Could not find a device with the given name or with the Nordic UART service", device_name) # Get the client and wait for a connection self.client = BleakClient(self.device) conn = await self.client.connect() if not conn: raise RuntimeError("Could not connect to device") # Start listening for notifications from NUS service await self.client.start_notify(self.NUS_TX_CHAR, _rx_callback) return self def get_mac(self) -> str: """ Gets the MAC address of the connected BLE device :return: a string containing the device's MAC address """ return str(self.device.address) def set_disconnect_callback(self, callback: Optional[Callable[[BaseBleakClient], None]]): """ Sets the callback to be called when BLE device is disconnected :param callback: """ self.client.set_disconnected_callback(callback) async def configure_gpio(self, port: int, pins: List[int], direction: BleUartPinCtrlGpioDirections): """ Configures GPIO on the connected device. The byte format is: <command 1-byte> <port 4-bytes> <pin mask 4-bytes> <0x00 = input, 0x01 = output> :param port: port number to configure :param pins: list of pins to configure :param direction: which directon to configure the pins as """ # Pack it all into a byte buffer (LE byte, LE 4 bytes, LE 4 bytes, LE byte byte_buffer = struct.pack("!BLLB", BleUartPinCtrlCommands.GPIO_CONFIGURE, port, BleUartPinCtrl.pin_list_to_bitmask(pins), int(direction)) print("configure_gpio sending: ", byte_buffer) # Transmit the byte buffer await self.client.write_gatt_char(self.NUS_RX_CHAR, bytearray(byte_buffer)) async def write_gpio(self, port: int, pins: List[int], output: BleUartPinCtrlGpioOutputs): """ Controls GPIO on the connected device. The byte format is: <command 1-byte> <port 4-bytes> <pin mask 4-bytes> :param port: port number to configure :param pins: list of pins to configure :param output: what output level to set on the GPIO """ # Pack it all into a byte buffer (LE byte, LE 4 bytes, LE 4 bytes, LE byte byte_buffer = struct.pack("!BLLB", BleUartPinCtrlCommands.GPIO_WRITE, port, BleUartPinCtrl.pin_list_to_bitmask(pins), int(output)) print("write_gpio sending: ", byte_buffer) # Transmit the byte buffer await self.client.write_gatt_char(self.NUS_RX_CHAR, bytearray(byte_buffer)) async def pulse_gpio(self, port: int, pins: List[int], duration_ms: int): # Pack it all into a byte buffer (LE byte, LE 4 bytes, LE 4 bytes, LE byte byte_buffer = struct.pack("!BLLL", BleUartPinCtrlCommands.GPIO_PULSE, port, BleUartPinCtrl.pin_list_to_bitmask(pins), duration_ms) print("pulse_gpio sending: ", byte_buffer) # Transmit the byte buffer await self.client.write_gatt_char(self.NUS_RX_CHAR, bytearray(byte_buffer)) async def query_gpio(self, port: int, pins: List[int]): """ TODO """ raise NotImplementedError("TODO CMK(11/15/20): Implement query_gpio") async def set_pwm(self, port: int, pins: List[int], duty_cycle: int): """ Sets PWM output on the connected device. The byte format is: <command 1-byte> <port 4-bytes> <pin mask 4-bytes> <intensity 1-byte> :param port: port number to configure :param pins: list of pins to configure :param duty_cycle: duty cycle of the PWM cycle (duty cycle is intensity / 255) """ # Pack it all into a byte buffer (LE byte, LE 4 bytes, LE 4 bytes, LE byte byte_buffer = struct.pack("!BLLB", BleUartPinCtrlCommands.PWM_SET, port, BleUartPinCtrl.pin_list_to_bitmask(pins), duty_cycle) print("set_pwm sending: ", byte_buffer) # Transmit the byte buffer await self.client.write_gatt_char(self.NUS_RX_CHAR, bytearray(byte_buffer)) async def query_state(self, port: int, pins: List[int]): """ TODO """ raise NotImplementedError("TODO CMK(11/15/20): Implement query_state") @staticmethod def pin_list_to_bitmask(pins: List[int]) -> int: """ Converts a string of pin numbers into a bitmask where each pin number is converted to a set bit of the corresponding position. :param pins: list of pin numbers to form into a bitmask :return: bitmask """ pin_mask = 0 for pin in pins: pin_mask = pin_mask | (1 << pin) return pin_mask
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 BlueBerryClient(): """ BlueBerry logger Bluetooth LE Client """ def __init__(self, *args, **kwargs): address = kwargs.get("address") if address is None: raise ValueError("invalid address") self._password = kwargs.get("password") timeout = kwargs.get("timeout", 5.0) self._bc = BleakClient(address, timeout=timeout) self._evt_cmd = ATimeoutEvent() self._evt_fetch = ATimeoutEvent() self._err_fetch = None try: self._bc.set_disconnected_callback(self._on_disconnect) # not in all backend (yet). will work without it but might hang forever except NotImplementedError: logger.warning("set_disconnected_callback not supported") # "fix" for bug in bleak MacOS backend version 0.7.x? except AttributeError: logger.warning("set_disconnected_callback not set") async def __aenter__(self): await self.connect() return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self._bc.disconnect() def _on_disconnect(self, client, _x=None): # _x only in bleak > 0.6 if not client is self._bc: logger.warning( "Unexpected disconnect callback from {} (self:{})".format( client.address, self._bc.address)) return # abort if someone is waiting on notifications and device disconnect if not self._evt_cmd.is_set(): self._evt_cmd.set() if not self._evt_fetch.is_set(): self._evt_fetch.set() async def connect(self): # called on enter await self._bc.connect() # TODO unlock only needed for same operations do it when needed await self._unlock(self._password) return True async def _unlock(self, password): """ unlock or "init" device. note: might set password if provided and BB device passcode state is INIT (directly after power on). passcode status can not be INIT for some operations like fetch (read log). """ rc = await self._pw_status() logger.debug("Unlock/init: password/passcode state {}".format(rc)) if rc == PASSCODE_STATUS.INIT: if password: await self._pw_write(password) logger.debug("Password protection enabled") else: # writing to log enable characteristic will change state to # DISABLED as a side effect. also logging is disabled after # power on #val = await self._read_u32(UUIDS.C_CFG_LOG_ENABLE) await self._write_u32(UUIDS.C_CFG_LOG_ENABLE, 0) logger.debug("Password protection disabled") elif rc == PASSCODE_STATUS.UNVERIFIED: if password is None: await self._bc.disconnect() raise ValueError("Password needed for this device") await self._pw_write(password) else: # password not needed for this device pass async def _write_u32(self, cuuid, val): val = int(val) data = val.to_bytes(4, byteorder="little", signed=False) data = bytearray(data) # fixes bug(!?) in txdbus ver 1.1.1 await self._bc.write_gatt_char(cuuid, data, response=True) async def _read_u32(self, cuuid): ba = await self._bc.read_gatt_char(cuuid) assert len(ba) == 4 return int.from_bytes(ba, byteorder="little", signed=False) async def _read_str(self, cuuid): """ read string """ ba = await self._bc.read_gatt_char(cuuid) return ba.decode("utf-8") # or ascii async def _cmd(self, txdata, rxsize=None): """ first byte in txdata is the cmd id """ txuuid = UUIDS.C_CMD_TX rxuuid = UUIDS.C_CMD_RX # bytes object not supported in txdbus txdata = bytearray(txdata) rxdata = bytearray() if not rxsize: return await self._bc.write_gatt_char(txuuid, txdata, response=True) self._evt_cmd.clear() def response_handler(sender, data): rxdata.extend(data) logger.debug("cmd RXD:{}".format(data)) self._evt_cmd.set() await self._bc.start_notify(rxuuid, response_handler) await self._bc.write_gatt_char(txuuid, txdata, response=True) if not await self._evt_cmd.wait(6): logger.error("notification timeout") # hide misleading error on unexpected disconnect if self._bc.is_connected: await self._bc.stop_notify(rxuuid) else: logger.warning("Unexpected disconnect") await asyncio.sleep(2) # TODO remove!? assert len(rxdata) == rxsize if rxsize and rxdata[0] != (txdata[0] | 0x80): raise RuntimeError( "Unexpected cmd id in response {}".format(rxdata)) return rxdata async def _pw_write(self, s): """ password write. if pw_status is "init", set new password, if pw_status="unverified", unlock device. Password must be 8 chars and ascii only """ data = bytearray([CMD_OPCODE.SET_PASSCODE]) assert len(s) == 8 data.extend(s) await self._cmd(data) async def _pw_status(self): """ get password status """ rsp = await self._cmd([CMD_OPCODE.GET_PASSCODE_STATE], 2) return rsp[1] async def set_password(self, password): if password is None: raise ValueError("No new password provided") rc = await self._pw_status() if rc == PASSCODE_STATUS.INIT: await self._pw_write(password) logger.debug("Password protection enabled") else: raise RuntimeError( "Device not in init mode. Please power cycle device") # TODO verify success async def blink(self, n=1): """ blink LED on device """ assert n > 0 while n: await self._cmd([CMD_OPCODE.BLINK_LED]) n = n - 1 if n > 0: await asyncio.sleep(1) async def config_read(self, outfile=None, fmt=None, **kwargs): conf = OrderedDict() val = await self._read_u32(UUIDS.C_CFG_LOG_ENABLE) conf["logging"] = bool(val) val = await self._read_u32(UUIDS.C_CFG_INTERVAL) conf["interval"] = val val = await self._pw_status() val = "{} ({})".format(val, enum2str(PASSCODE_STATUS, val)) conf["pwstatus"] = val enbits = await self._read_u32(UUIDS.C_CFG_SENSOR_ENABLE) for name, s in SENSORS.items(): conf[s.apiname] = bool(s.enmask & enbits) out = mk_OutputWriter(outfile=outfile, fmt=fmt) out.write_kv(conf) return conf async def config_write(self, **kwargs): setMask = 0 clrMask = 0 # sanity check all params before write for k, v in kwargs.items(): if v is None: continue if k in SENSORS: enmask = SENSORS[k].enmask if v: setMask |= enmask else: clrMask |= enmask else: logger.debug("Ignoring unknown config field '{}'".format(k)) logging = kwargs.get("logging") if logging is not None: await self._write_u32(UUIDS.C_CFG_LOG_ENABLE, logging) interval = kwargs.get("interval") if interval is not None: await self._write_u32(UUIDS.C_CFG_INTERVAL, interval) cuuid = UUIDS.C_CFG_SENSOR_ENABLE if setMask or clrMask: enMaskOld = await self._read_u32(cuuid) enMaskNew = (enMaskOld & ~clrMask) | setMask await self._write_u32(cuuid, enMaskNew) logger.debug("enabled sensors \ old=0x{:04X}, new=0x{:04X}".format(enMaskOld, enMaskNew)) async def enter_dfu(self): await self._cmd([CMD_OPCODE.ENTER_DFU]) async def device_info(self, outfile=None, fmt="txt", debug=False, **kwargs): if debug: services = await self._bc.get_services() for s in services: logger.debug("Characteristic for service: %s" % str(s)) for c in s.characteristics: logger.debug(" %s" % str(c)) d = {} d["manufacturer"] = await self._read_str(UUIDS.C_MANUFACTURER) d["software_rev"] = await self._read_str(UUIDS.C_SOFTWARE_REV) d["serial_number"] = await self._read_str(UUIDS.C_SERIAL_NUMBER) out = mk_OutputWriter(outfile=outfile, fmt=fmt) out.write_kv(d) return d async def fetch(self, outfile=None, fmt="txt", rtd=False, num=None, **kwargs): RTD_RATE_HZ_TO_VAL = {1: 0, 25: 6, 50: 7, 100: 8, 200: 9, 400: 10} uuid_ = UUIDS.C_SENSORS_LOG if not rtd: pass elif rtd in RTD_RATE_HZ_TO_VAL: #if rtd == 1: uuid_ = UUIDS.C_SENSORS_RTD rtd_rate = RTD_RATE_HZ_TO_VAL[rtd] await self._write_u32(UUIDS.C_CFG_RT_IMU, rtd_rate) else: raise ValueError("Invalid rtd") bbd = BlueBerryDeserializer(outfile=outfile, fmt=fmt) nentries = num self._evt_fetch.clear() self._err_fetch = None def response_handler(sender, data): # store exception and raise it later. # can not raise it from this conext as asyncio will only # print to stderr and continue execution. # there is probably a bettwr way of doning this. try: done = bbd.putb(data) except Exception as e: self._err_fetch = e done = True if nentries and bbd.nentries >= nentries: done = True if done: self._evt_fetch.set() await self._bc.start_notify(uuid_, response_handler) timeout = None # kwargs.get('timeout', 100) if not await self._evt_fetch.wait(timeout): logger.error("Notification timeout after %d sec" % timeout) # hide missleading error on unexpected disconnect if self._bc.is_connected: await self._bc.stop_notify(uuid_) else: logger.warning("Unexpected disconnect") logger.debug("Fetched %d entries" % bbd.nentries) if self._err_fetch: logger.debug("err %s" % str(self._err_fetch)) raise self._err_fetch
class BLE_interface(): def __init__(self): self._send_queue = asyncio.Queue() async def connect(self, addr_str: str, addr_type: str, adapter: str, timeout: float): # address_type used only in Windows .NET currently self.dev = BleakClient(addr_str, adapter=adapter, address_type=addr_type, timeout=timeout) self.dev.set_disconnected_callback(self.handle_disconnect) logging.info(f'Trying to connect with {addr_str}') await self.dev.connect() logging.info(f'Device {self.dev.address} connected') async def setup_chars(self, write_uuid: str, read_uuid: str, mode: str): self.read_enabled = 'r' in mode self.write_enabled = 'w' in mode if self.write_enabled: self.write_char = self.find_char( write_uuid, ['write', 'write-without-response']) else: logging.info('Writing disabled, skipping write UUID detection') if self.read_enabled: self.read_char = self.find_char(read_uuid, ['notify', 'indicate']) await self.dev.start_notify(self.read_char, self.handle_notify) else: logging.info('Reading disabled, skipping read UUID detection') def find_char(self, uuid: Optional[str], req_props: [str]) -> BleakGATTCharacteristic: name = req_props[0] # Use user supplied UUID first, otherwise try included list if uuid: uuid_candidates = [uuid] else: uuid_candidates = ble_chars logging.debug(f'No {name} uuid specified, trying builtin list') results = [] for srv in self.dev.services: for c in srv.characteristics: if c.uuid in uuid_candidates: results.append(c) if uuid: assert len(results) > 0, \ f"No characteristic with specified {name} UUID {uuid} found!" else: assert len(results) > 0, \ f"""No characteristic in builtin {name} list {uuid_candidates} found! Please specify one with {'-w/--write-uuid' if name == 'write' else '-r/--read-uuid'}, see also --help""" res_str = '\n'.join(f'\t{c} {c.properties}' for c in results) logging.debug(f'Characteristic candidates for {name}: \n{res_str}') # Check if there is a intersection of permission flags results[:] = [c for c in results if set(c.properties) & set(req_props)] assert len(results) > 0, \ f"No characteristic with {req_props} property found!" assert len(results) == 1, \ f'Multiple matching {name} characteristics found, please specify one' # must be valid here found = results[0] logging.info( f'Found {name} characteristic {found.uuid} (H. {found.handle})') return found def set_receiver(self, callback): self._cb = callback logging.info('Receiver set up') async def send_loop(self): assert hasattr(self, '_cb'), 'Callback must be set before receive loop!' while True: data = await self._send_queue.get() if data == None: break # Let future end on shutdown if not self.write_enabled: logging.warning(f'Ignoring unexpected write data: {data}') continue logging.debug(f'Sending {data}') await self.dev.write_gatt_char(self.write_char, data) def stop_loop(self): logging.info('Stopping Bluetooth event loop') self._send_queue.put_nowait(None) async def disconnect(self): if hasattr(self, 'dev') and self.dev.is_connected: if hasattr(self, 'read_char'): await self.dev.stop_notify(self.read_char) await self.dev.disconnect() logging.info('Bluetooth disconnected') def queue_send(self, data: bytes): self._send_queue.put_nowait(data) def handle_notify(self, handle: int, data: bytes): logging.debug(f'Received notify from {handle}: {data}') if not self.read_enabled: logging.warning(f'Read unexpected data, dropping: {data}') return self._cb(data) def handle_disconnect(self, client: BleakClient): logging.warning(f'Device {client.address} disconnected') self.stop_loop()
class BleController: def __init__(self, packetReceivedCallback, connectionStatusCallback): self.status = CONNECTION_STATUS.NOT_CONNECTED self.userDisconnected = False self.botID = -1 self.client = "" self.discoveredCharacs = {} self.deviceName = "" self.svcs = "" self.address = None self._packetReceivedCallback = packetReceivedCallback self._connectionStatusCallback = connectionStatusCallback self.lastReadWriteTime = 0 self.pauseHeartBeat = False self.heartbeatRequired = True self.heartbeatTask = None self.scanner = None def packetReceivedCallbackWrapper(self, sender, data): self.lastReadWriteTime = time.time() self._packetReceivedCallback(sender, data) def _updateConnectionStatus(self, newStatus): self.status = newStatus self._connectionStatusCallback(newStatus) def updateBotID(self, botID): self.botID = botID if not re.search("^\d{1,4}$", str(botID)): logger.warning( "Micromelon Robot IDs should be a number between 0 and 8999 - searching for '" + str(botID) + "' anyway.") self.deviceName = "Micromelon" + str(self.botID).zfill(4) # BLE IO async def read(self, gattChar): try: res = await self.client.read_gatt_char(gattChar) self.lastReadWriteTime = time.time() return res except Exception as e: logger.error(e) return False async def write(self, gattChar, data, response): try: res = await self.client.write_gatt_char(gattChar, bytearray(data), response) self.lastReadWriteTime = time.time() return res except Exception as e: logger.error(e) return False async def writeUartPacket(self, packet, withoutResponse): return await self.client.write_gatt_char(UART_UUID, bytearray(packet), not withoutResponse) # BLE Connection async def _detectionCallback(self): while 1: await asyncio.sleep(0.1) for dev in await self.scanner.get_discovered_devices(): if self.deviceName == None or self.deviceName == dev.name: self.address = dev.address logger.debug("Found with address: ", dev.address) logger.debug("Found name: " + str(dev.name)) # self.scanner.stop() return async def connectBLE(self, reqServices=None, name=None): if reqServices is None: reqServices = [] # Discover all available devices self.scanner = BleakScanner(timeout=SCAN_TIMEOUT) self.deviceName = name self._updateConnectionStatus(CONNECTION_STATUS.SEARCHING) await self.scanner.start() try: await asyncio.wait_for(self._detectionCallback(), timeout=SCAN_TIMEOUT) except asyncio.TimeoutError as e: # Add relevant message to exception e.args = ("Robot not found - Scan timed out", *e.args) raise finally: await self.scanner.stop() if not self.address: self._updateConnectionStatus(CONNECTION_STATUS.NOT_CONNECTED) raise Exception("Robot address not discovered") # Create a client instance for the bot self.client = BleakClient(self.address) self.client.set_disconnected_callback(self._onDisconnected) # Connect to the bot via the recorded address try: self._updateConnectionStatus(CONNECTION_STATUS.CONNECTING) await self.client.connect(timeout=CONNECT_TIMEOUT) self._updateConnectionStatus(CONNECTION_STATUS.INTERROGATING) # Record the available services self.svcs = await self.client.get_services() # Record the avaiable characteristics self.discoveredCharacs = self.svcs.characteristics except Exception as e: # Unable to connect so print the error logger.error("Bluetooth Error") logger.error(e) # Disconnect await self.disconnect() raise discoveredServiceUUIDs = [ str(x.uuid) for x in self.svcs.services.values() ] for service in reqServices: if (service not in discoveredServiceUUIDs and _long_to_short_uuid(service) not in discoveredServiceUUIDs): logger.debug("Service not found: " + service) await self.disconnect() raise Exception("Required BLE services not discovered") return True async def connectToRobot(self, botID): self.discoveredCharacs = {} self.userDisconnected = False self.updateBotID(botID) # Connect to device connectionResult = await self.connectBLE( reqServices=[MICROMELON_SERVICE_UUID], name=self.deviceName) if (not connectionResult or not len(self.discoveredCharacs) or not self.client.is_connected): await self.disconnect() return False try: # Sub to UART and HEARTBEAT characs await self.client.start_notify(UART_UUID, self.packetReceivedCallbackWrapper) await self.client.start_notify(HEARTBEAT_UUID, self.packetReceivedCallbackWrapper) except Exception as e: logger.error("Connection Incomplete") logger.error(e) logger.error("Please try again, your robot might need updating") await self.disconnect() return False self._updateConnectionStatus(CONNECTION_STATUS.CONNECTED) self.lastReadWriteTime = time.time() await self.heartbeat() return True async def timeout(self, time, cb=None): try: await asyncio.sleep(time) await cb() except asyncio.CancelledError: return async def heartbeat(self): if self.status != CONNECTION_STATUS.CONNECTED or not self.heartbeatRequired: return if (time.time() - self.lastReadWriteTime > HEARTBEAT_INTERVAL * 0.8) and not self.pauseHeartBeat: try: readVal = await asyncio.wait_for(self.read(HEARTBEAT_UUID), timeout=HEARTBEAT_TIMEOUT) # logger.debug('heartbeat read succeeded: ', readVal[0]) self.lastReadWriteTime = int(time.time()) except Exception as e: logger.debug("heartbeat read failed", e) await self.disconnect() return self.heartbeatTask = asyncio.create_task(self.timeout( HEARTBEAT_INTERVAL, self.heartbeat), name=HEARTBEAT_TASK_NAME) async def disconnect(self, calledByUser=True): self.userDisconnected = calledByUser if self.heartbeatTask: self.heartbeatTask.cancel() self.heartbeatTask = None self._updateConnectionStatus(CONNECTION_STATUS.DISCONNECTED) if self.client: await self.client.disconnect() def _onDisconnected(self, client): self._updateConnectionStatus(CONNECTION_STATUS.DISCONNECTED) logger.info("BLE disconnected event")
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