Beispiel #1
0
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)
Beispiel #2
0
    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)
Beispiel #3
0
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
Beispiel #4
0
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)
Beispiel #5
0
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()
Beispiel #6
0
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)
Beispiel #7
0
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
Beispiel #9
0
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
Beispiel #10
0
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
Beispiel #11
0
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")
Beispiel #13
0
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