Beispiel #1
0
class _DisClientCharacteristic(_DisCharacteristic):
    def __init__(self, service, uuid, data_class):
        super(_DisClientCharacteristic, self).__init__(service, uuid, data_class)
        self._char = service.find_characteristic(uuid)
        self._on_read_complete_event = EventSource("Char {} Read Complete".format(self.uuid))

    def _read_complete(self, characteristic, event_args):
        """
        :param characteristic:
        :type event_args: blatann.event_args.ReadCompleteEventArgs
        """
        decoded_value = None
        if event_args.status == GattStatusCode.success:
            try:
                stream = ble_data_types.BleDataStream(event_args.value)
                decoded_value = self.data_class.decode(stream)
            except Exception as e:  # TODO not so generic
                logger.error("Service {}, Characteristic {} failed to decode value on read. "
                             "Stream: [{}]".format(self.service.uuid, self.uuid, binascii.hexlify(event_args.value)))
                logger.exception(e)

        decoded_event_args = DecodedReadCompleteEventArgs.from_read_complete_event_args(event_args, decoded_value)
        self._on_read_complete_event.notify(characteristic, decoded_event_args)

    def read(self):
        if not self.is_defined:
            raise AttributeError("Characteristic {} is not present in the Device Info Service".format(self.uuid))
        self._char.read().then(self._read_complete)
        return EventWaitable(self._on_read_complete_event)
Beispiel #2
0
class Peer(object):
    """
    Object that represents a BLE-connected (or disconnected) peer
    """
    BLE_CONN_HANDLE_INVALID = BLE_CONN_HANDLE_INVALID
    """ Number of bytes that are header/overhead per MTU when sending a notification or indication """
    NOTIFICATION_INDICATION_OVERHEAD_BYTES = 3

    def __init__(self,
                 ble_device,
                 role,
                 connection_params=DEFAULT_CONNECTION_PARAMS,
                 security_params=DEFAULT_SECURITY_PARAMS):
        """
        :type ble_device: blatann.device.BleDevice
        """
        self._ble_device = ble_device
        self._role = role
        self._ideal_connection_params = connection_params
        self._current_connection_params = DEFAULT_CONNECTION_PARAMS
        self.conn_handle = BLE_CONN_HANDLE_INVALID
        self.peer_address = "",
        self.connection_state = PeerState.DISCONNECTED
        self._on_connect = EventSource("On Connect", logger)
        self._on_disconnect = EventSource("On Disconnect", logger)
        self._mtu_size = 23  # TODO: MTU Exchange procedure
        self._connection_based_driver_event_handlers = {}
        self._connection_handler_lock = threading.Lock()
        self.security = smp.SecurityManager(self._ble_device, self,
                                            security_params)

    """
    Properties
    """

    @property
    def connected(self):
        """
        Gets if this peer is currently connected

        :return: True if connected, False if not
        """
        return self.connection_state == PeerState.CONNECTED

    @property
    def mtu_size(self):
        """
        Gets the current size of the MTU for the peer

        :return: The current MTU size
        """
        return self._mtu_size

    @property
    def bytes_per_notification(self):
        """
        Gets the maximum number of bytes that can be sent in a single notification/indication

        :return: Number of bytes that can be sent in a notification/indication
        """
        return self._mtu_size - self.NOTIFICATION_INDICATION_OVERHEAD_BYTES

    @property
    def is_peripheral(self):
        """
        Gets if this peer is a Peripheral (the local device acting as a central/client)
        """
        return isinstance(self, Peripheral)

    @property
    def is_client(self):
        """
        Gets if this peer is a Client (the local device acting as a peripheral/server)
        """
        return isinstance(self, Client)

    """
    Events
    """

    @property
    def on_connect(self):
        """
        Event generated when the peer connects to the local device

        Event Args: None

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_connect

    @property
    def on_disconnect(self):
        """
        Event generated when the peer disconnects from the local device

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_disconnect

    """
    Public Methods
    """

    def disconnect(
            self,
            status_code=nrf_events.BLEHci.remote_user_terminated_connection):
        """
        Disconnects from the peer, giving the optional status code.
        Returns a waitable that will fire when the disconnection is complete

        :param status_code: The HCI Status code to send back to the peer
        :return: A waitable that will fire when the peer is disconnected
        :rtype: connection_waitable.DisconnectionWaitable
        """
        if self.connection_state != PeerState.CONNECTED:
            return
        self._ble_device.ble_driver.ble_gap_disconnect(self.conn_handle,
                                                       status_code)
        return self._disconnect_waitable

    def set_connection_parameters(self,
                                  min_connection_interval_ms,
                                  max_connection_interval_ms,
                                  connection_timeout_ms,
                                  slave_latency=0):
        """
        Sets the connection parameters for the peer and starts the connection parameter update process

        :param min_connection_interval_ms: The minimum acceptable connection interval, in milliseconds
        :param max_connection_interval_ms: The maximum acceptable connection interval, in milliseconds
        :param connection_timeout_ms: The connection timeout, in milliseconds
        :param slave_latency: The slave latency allowed
        """
        self._ideal_connection_params = ConnectionParameters(
            min_connection_interval_ms, max_connection_interval_ms,
            connection_timeout_ms, slave_latency)
        if not self.connected:
            return
        # Do stuff to set the connection parameters
        self._ble_device.ble_driver.ble_gap_conn_param_update(
            self.conn_handle, self._ideal_connection_params)

    """
    Internal Library Methods
    """

    def peer_connected(self, conn_handle, peer_address, connection_params):
        """
        Internal method called when the peer connects to set up the object
        """
        self.conn_handle = conn_handle
        self.peer_address = peer_address
        self._disconnect_waitable = connection_waitable.DisconnectionWaitable(
            self)
        self.connection_state = PeerState.CONNECTED
        self._current_connection_params = connection_params

        self._ble_device.ble_driver.event_subscribe(
            self._on_disconnect_event, nrf_events.GapEvtDisconnected)
        self._ble_device.ble_driver.event_subscribe(
            self._on_connection_param_update, nrf_events.GapEvtConnParamUpdate,
            nrf_events.GapEvtConnParamUpdateRequest)
        self._on_connect.notify(self)

    def _check_driver_event_connection_handle_wrapper(self, func):
        def wrapper(driver, event):
            """
            :param driver:
            :type event: blatann.nrf.nrf_events.BLEEvent
            """
            logger.debug("Got event: {} for peer {}".format(
                event, self.conn_handle))
            if self.connected and self.conn_handle == event.conn_handle:
                func(driver, event)

        return wrapper

    def driver_event_subscribe(self, handler, *event_types):
        """
        Internal method that subscribes handlers to NRF Driver events directed at this peer.
        Handlers are automatically unsubscribed once the peer disconnects

        :param handler: The handler to subscribe
        :param event_types: The NRF Driver event types to subscribe to
        """
        wrapped_handler = self._check_driver_event_connection_handle_wrapper(
            handler)
        with self._connection_handler_lock:
            if handler not in self._connection_based_driver_event_handlers:
                self._connection_based_driver_event_handlers[
                    handler] = wrapped_handler
                self._ble_device.ble_driver.event_subscribe(
                    wrapped_handler, *event_types)

    def driver_event_unsubscribe(self, handler, *event_types):
        """
        Internal method that unsubscribes handlers from NRF Driver events

        :param handler: The handler to unsubscribe
        :param event_types: The event types to unsubscribe from
        """
        with self._connection_handler_lock:
            wrapped_handler = self._connection_based_driver_event_handlers.get(
                handler, None)
            logger.debug("Unsubscribing {} ({})".format(
                handler, wrapped_handler))
            if wrapped_handler:
                self._ble_device.ble_driver.event_unsubscribe(
                    wrapped_handler, *event_types)
                del self._connection_based_driver_event_handlers[handler]

    """
    Private Methods
    """

    def _on_disconnect_event(self, driver, event):
        """
        :type event: nrf_events.GapEvtDisconnected
        """
        if not self.connected or self.conn_handle != event.conn_handle:
            return
        self.conn_handle = BLE_CONN_HANDLE_INVALID
        self.connection_state = PeerState.DISCONNECTED
        self._on_disconnect.notify(self, DisconnectionEventArgs(event.reason))

        with self._connection_handler_lock:
            for handler in self._connection_based_driver_event_handlers.values(
            ):
                self._ble_device.ble_driver.event_unsubscribe_all(handler)
            self._connection_based_driver_event_handlers = {}
        self._ble_device.ble_driver.event_unsubscribe(
            self._on_disconnect_event)
        self._ble_device.ble_driver.event_unsubscribe(
            self._on_connection_param_update)

    def _on_connection_param_update(self, driver, event):
        """
        :type event: nrf_events.GapEvtConnParamUpdate
        """
        if not self.connected or self.conn_handle != event.conn_handle:
            return
        if isinstance(event, nrf_events.GapEvtConnParamUpdateRequest
                      ) or self._role == nrf_events.BLEGapRoles.periph:
            logger.debug("[{}] Conn Params updating to {}".format(
                self.conn_handle, self._ideal_connection_params))
            self._ble_device.ble_driver.ble_gap_conn_param_update(
                self.conn_handle, self._ideal_connection_params)
        else:
            logger.debug("[{}] Updated to {}".format(self.conn_handle,
                                                     event.conn_params))
        self._current_connection_params = event.conn_params

    def __nonzero__(self):
        return self.conn_handle != BLE_CONN_HANDLE_INVALID

    def __bool__(self):
        return self.__nonzero__()
Beispiel #3
0
class GattsCharacteristic(gatt.Characteristic):
    """
    Represents a single characteristic within a service. This class is usually not instantiated directly; it
    is added to a service through GattsService::add_characteristic()
    """
    _QueuedChunk = namedtuple("QueuedChunk", ["offset", "data"])

    def __init__(self,
                 ble_device,
                 peer,
                 uuid,
                 properties,
                 notification_manager,
                 value=b"",
                 prefer_indications=True,
                 string_encoding="utf8"):
        """
        :param ble_device:
        :param peer:
        :param uuid:
        :type properties: gatt.GattsCharacteristicProperties
        :type notification_manager: _NotificationManager
        :param value:
        :param prefer_indications:
        """
        super(GattsCharacteristic, self).__init__(ble_device, peer, uuid,
                                                  properties, string_encoding)
        self._value = value
        self.prefer_indications = prefer_indications
        self._notification_manager = notification_manager
        # Events
        self._on_write = EventSource("Write Event", logger)
        self._on_read = EventSource("Read Event", logger)
        self._on_sub_change = EventSource("Subscription Change Event", logger)
        self._on_notify_complete = EventSource("Notification Complete Event",
                                               logger)
        # Subscribed events
        self.ble_device.ble_driver.event_subscribe(self._on_gatts_write,
                                                   nrf_events.GattsEvtWrite)
        self.ble_device.ble_driver.event_subscribe(
            self._on_rw_auth_request,
            nrf_events.GattsEvtReadWriteAuthorizeRequest)
        # Internal state tracking stuff
        self._write_queued = False
        self._read_in_process = False
        self._queued_write_chunks = []
        self.peer.on_disconnect.register(self._on_disconnect)

    """
    Public Methods
    """

    def set_value(
        self,
        value,
        notify_client=False
    ) -> Optional[EventWaitable[GattsCharacteristic,
                                NotificationCompleteEventArgs]]:
        """
        Sets the value of the characteristic.

        :param value: The value to set to. Must be an iterable type such as a str, bytearray, or list of uint8 values.
                      Length must be less than the characteristic's max length.
                      If a str is given, it will be encoded using the string_encoding property.
        :param notify_client: Flag whether or not to notify the client. If indications and notifications are not set up
                              for the characteristic, will raise an InvalidOperationException
        :raises: InvalidOperationException if value length is too long, or notify client set and characteristic
                 is not notifiable
        :return: If notify_client is true, this method will return the waitable for when the notification is sent to the client
        """
        if isinstance(value, BleDataStream):
            value = value.value
        if isinstance(value, str):
            value = value.encode(self.string_encoding)
        if len(value) > self.max_length:
            raise InvalidOperationException(
                "Attempted to set value of {} with length greater than max "
                "(got {}, max {})".format(self.uuid, len(value),
                                          self.max_length))
        if notify_client and not self.notifiable:
            raise InvalidOperationException(
                "Cannot notify client. "
                "{} not set up for notifications or indications".format(
                    self.uuid))

        v = nrf_types.BLEGattsValue(value)
        self.ble_device.ble_driver.ble_gatts_value_set(self.peer.conn_handle,
                                                       self.value_handle, v)
        self._value = value

        if notify_client and self.client_subscribed and not self._read_in_process:
            return self.notify(None)

    def notify(
        self, data
    ) -> EventWaitable[GattsCharacteristic, NotificationCompleteEventArgs]:
        """
        Notifies the client with the data provided without setting the data into the characteristic value.
        If data is not provided (None), will notify with the currently-set value of the characteristic

        :param data: The data to notify the client with
        :return: An EventWaitable that will fire when the notification is successfully sent to the client. The waitable
                 also contains the ID of the sent notification which is used in the on_notify_complete event
        :rtype: NotificationCompleteEventWaitable
        """
        if isinstance(data, BleDataStream):
            value = data.value
        if isinstance(data, str):
            value = data.encode(self.string_encoding)
        if not self.notifiable:
            raise InvalidOperationException(
                "Cannot notify client. "
                "{} not set up for notifications or indications".format(
                    self.uuid))
        if not self.client_subscribed:
            raise InvalidStateException(
                "Client is not subscribed, cannot notify client")

        notification_id = self._notification_manager.notify(
            self, self.value_handle, self._on_notify_complete, data)
        return IdBasedEventWaitable(self._on_notify_complete, notification_id)

    """
    Properties
    """

    @property
    def max_length(self) -> int:
        """
        The max possible the value the characteristic can be set to
        """
        return self._properties.max_len

    @property
    def notifiable(self) -> bool:
        """
        Gets if the characteristic is set up to asynchonously notify clients via notifications or indications
        """
        return self._properties.indicate or self._properties.notify

    @property
    def value(self) -> bytes:
        """
        Gets the current value of the characteristic
        """
        return self._value

    @property
    def client_subscribed(self) -> bool:
        """
        Gets if the client is currently subscribed (notify or indicate) to this characteristic
        """
        return self.peer and self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED

    """
    Events
    """

    @property
    def on_write(self) -> Event[GattsCharacteristic, WriteEventArgs]:
        """
        Event generated whenever a client writes to this characteristic.

        EventArgs type: WriteEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_write

    @property
    def on_read(self) -> Event[GattsCharacteristic, None]:
        """
        Event generated whenever a client requests to read from this characteristic. At this point, the application
        may choose to update the value of the characteristic to a new value using set_value.

        A good example of this is a "system time" characteristic which reports the applications system time in seconds.
        Instead of updating this characteristic every second, it can be "lazily" updated only when read from.

        NOTE: if there are multiple handlers subscribed to this and each set the value differently, it may cause
        undefined behavior.

        EventArgs type: None

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_read

    @property
    def on_subscription_change(
            self
    ) -> Event[GattsCharacteristic, SubscriptionStateChangeEventArgs]:
        """
        Event that is generated whenever a client changes its subscription state of the characteristic
        (notify, indicate, none).

        EventArgs type: SubscriptionStateChangeEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_sub_change

    @property
    def on_notify_complete(
            self) -> Event[GattsCharacteristic, NotificationCompleteEventArgs]:
        """
        Event that is generated when a notification or indication sent to the client is successfully sent
        """
        return self._on_notify_complete

    """
    Event Handling
    """

    def _handle_in_characteristic(self, attribute_handle):
        return attribute_handle in [self.value_handle, self.cccd_handle]

    def _execute_queued_write(self, write_op):
        if not self._write_queued:
            return

        self._write_queued = False
        if write_op == nrf_events.BLEGattsWriteOperation.exec_write_req_cancel:
            logger.info("Cancelling write request, char: {}".format(self.uuid))
        else:
            logger.info("Executing write request, char: {}".format(self.uuid))
            # TODO Assume that it was assembled properly. Error handling should go here
            new_value = bytearray()
            for chunk in self._queued_write_chunks:
                new_value += bytearray(chunk.data)
            logger.debug("New value: 0x{}".format(binascii.hexlify(new_value)))
            self.ble_device.ble_driver.ble_gatts_value_set(
                self.peer.conn_handle, self.value_handle,
                nrf_types.BLEGattsValue(new_value))
            self._value = bytes(new_value)
            self._on_write.notify(self, WriteEventArgs(self.value))
        self._queued_write_chunks = []

    def _on_cccd_write(self, event):
        """
        :type event: nrf_events.GattsEvtWrite
        """
        self.cccd_state = gatt.SubscriptionState.from_buffer(
            bytearray(event.data))
        self._on_sub_change.notify(
            self, SubscriptionStateChangeEventArgs(self.cccd_state))

    def _on_gatts_write(self, driver, event):
        """
        :type event: nrf_events.GattsEvtWrite
        """
        if event.attribute_handle == self.cccd_handle:
            self._on_cccd_write(event)
            return
        elif event.attribute_handle != self.value_handle:
            return
        self._value = bytes(bytearray(event.data))
        self._on_write.notify(self, WriteEventArgs(self.value))

    def _on_write_auth_request(self, write_event):
        """
        :type write_event: nrf_events.GattsEvtWrite
        """
        if write_event.write_op in [
                nrf_events.BLEGattsWriteOperation.exec_write_req_cancel,
                nrf_events.BLEGattsWriteOperation.exec_write_req_now
        ]:
            self._execute_queued_write(write_event.write_op)
            # Reply should already be handled in database since this can span multiple characteristics and services
            return

        if not self._handle_in_characteristic(write_event.attribute_handle):
            # Handle is not for this characteristic, do nothing
            return

        # Build out the reply
        params = nrf_types.BLEGattsAuthorizeParams(
            nrf_types.BLEGattStatusCode.success, True, write_event.offset,
            write_event.data)
        reply = nrf_types.BLEGattsRwAuthorizeReplyParams(write=params)

        # Check that the write length is valid
        if write_event.offset + len(
                write_event.data) > self._properties.max_len:
            params.gatt_status = nrf_types.BLEGattStatusCode.invalid_att_val_length
            self.ble_device.ble_driver.ble_gatts_rw_authorize_reply(
                write_event.conn_handle, reply)
        else:
            # Send reply before processing write, in case user sets data in gatts_write handler
            try:
                self.ble_device.ble_driver.ble_gatts_rw_authorize_reply(
                    write_event.conn_handle, reply)
            except Exception as e:
                pass
            if write_event.write_op == nrf_events.BLEGattsWriteOperation.prep_write_req:
                self._write_queued = True
                self._queued_write_chunks.append(
                    self._QueuedChunk(write_event.offset, write_event.data))
            elif write_event.write_op in [
                    nrf_events.BLEGattsWriteOperation.write_req,
                    nrf_types.BLEGattsWriteOperation.write_cmd
            ]:
                self._on_gatts_write(None, write_event)

        # TODO More logic

    def _on_read_auth_request(self, read_event):
        """
        :type read_event: nrf_events.GattsEvtRead
        """
        if not self._handle_in_characteristic(read_event.attribute_handle):
            # Don't care about handles outside of this characteristic
            return

        params = nrf_types.BLEGattsAuthorizeParams(
            nrf_types.BLEGattStatusCode.success, False, read_event.offset)
        reply = nrf_types.BLEGattsRwAuthorizeReplyParams(read=params)
        if read_event.offset > len(self.value):
            params.gatt_status = nrf_types.BLEGattStatusCode.invalid_offset
        else:
            self._read_in_process = True
            # If the client is reading from the beginning, notify handlers in case an update needs to be made
            if read_event.offset == 0:
                self._on_read.notify(self)
            self._read_in_process = False

        self.ble_device.ble_driver.ble_gatts_rw_authorize_reply(
            read_event.conn_handle, reply)

    def _on_rw_auth_request(self, driver, event):
        if not self.peer:
            logger.warning("Got RW request when peer not connected: {}".format(
                event.conn_handle))
            return
        if event.read:
            self._on_read_auth_request(event.read)
        elif event.write:
            self._on_write_auth_request(event.write)
        else:
            logging.error("auth request was not read or write???")

    def _on_disconnect(self, peer, event_args):
        if self.cccd_handle and self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED:
            self.cccd_state = gatt.SubscriptionState.NOT_SUBSCRIBED
Beispiel #4
0
class Peer(object):
    """
    Object that represents a BLE-connected (or disconnected) peer
    """
    BLE_CONN_HANDLE_INVALID = BLE_CONN_HANDLE_INVALID
    """ Number of bytes that are header/overhead per MTU when sending a notification or indication """
    NOTIFICATION_INDICATION_OVERHEAD_BYTES = 3

    def __init__(self,
                 ble_device,
                 role,
                 connection_params=DEFAULT_CONNECTION_PARAMS,
                 security_params=DEFAULT_SECURITY_PARAMS):
        """
        :type ble_device: blatann.device.BleDevice
        """
        self._ble_device = ble_device
        self._role = role
        self._ideal_connection_params = connection_params
        self._current_connection_params = DEFAULT_CONNECTION_PARAMS
        self.conn_handle = BLE_CONN_HANDLE_INVALID
        self.peer_address = "",
        self.connection_state = PeerState.DISCONNECTED
        self._on_connect = EventSource("On Connect", logger)
        self._on_disconnect = EventSource("On Disconnect", logger)
        self._on_mtu_exchange_complete = EventSource(
            "On MTU Exchange Complete", logger)
        self._on_mtu_size_updated = EventSource("On MTU Size Updated", logger)
        self._mtu_size = MTU_SIZE_DEFAULT
        self._preferred_mtu_size = MTU_SIZE_DEFAULT
        self._negotiated_mtu_size = None

        self._connection_based_driver_event_handlers = {}
        self._connection_handler_lock = threading.Lock()
        self.security = smp.SecurityManager(self._ble_device, self,
                                            security_params)

    """
    Properties
    """

    @property
    def connected(self):
        """
        Gets if this peer is currently connected

        :return: True if connected, False if not
        """
        return self.connection_state == PeerState.CONNECTED

    @property
    def bytes_per_notification(self):
        """
        Gets the maximum number of bytes that can be sent in a single notification/indication

        :return: Number of bytes that can be sent in a notification/indication
        """
        return self._mtu_size - self.NOTIFICATION_INDICATION_OVERHEAD_BYTES

    @property
    def is_peripheral(self):
        """
        Gets if this peer is a Peripheral (the local device acting as a central/client)
        """
        return isinstance(self, Peripheral)

    @property
    def is_client(self):
        """
        Gets if this peer is a Client (the local device acting as a peripheral/server)
        """
        return isinstance(self, Client)

    @property
    def is_previously_bonded(self):
        """
        Gets if the peer this security manager is for was bonded in a previous connection
        """
        return self.security.is_previously_bonded

    @property
    def mtu_size(self):
        """
        Gets the current negotiated size of the MTU for the peer

        :return: The current MTU size
        """
        return self._mtu_size

    @property
    def max_mtu_size(self):
        """
        The maximum allowed MTU size. This is set when initially configuring the BLE Device
        """
        return self._ble_device.max_mtu_size

    @property
    def preferred_mtu_size(self):
        """
        Gets the user-set preferred MTU size. Defaults to the Default MTU size (23)
        """
        return self._preferred_mtu_size

    @preferred_mtu_size.setter
    def preferred_mtu_size(self, mtu_size):
        """
        Sets the preferred MTU size to use when a MTU Exchange Request is received
        """
        self._validate_mtu_size(mtu_size)
        self._preferred_mtu_size = mtu_size

    """
    Events
    """

    @property
    def on_connect(self):
        """
        Event generated when the peer connects to the local device

        Event Args: None

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_connect

    @property
    def on_disconnect(self):
        """
        Event generated when the peer disconnects from the local device

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_disconnect

    @property
    def on_mtu_exchange_complete(self):
        """
        Event generated when an MTU exchange completes with the peer

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_mtu_exchange_complete

    @property
    def on_mtu_size_updated(self):
        """
        Event generated when the effective MTU size has been updated on the connection.

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_mtu_size_updated

    """
    Public Methods
    """

    def disconnect(
            self,
            status_code=nrf_events.BLEHci.remote_user_terminated_connection):
        """
        Disconnects from the peer, giving the optional status code.
        Returns a waitable that will fire when the disconnection is complete

        :param status_code: The HCI Status code to send back to the peer
        :return: A waitable that will fire when the peer is disconnected
        :rtype: connection_waitable.DisconnectionWaitable
        """
        if self.connection_state != PeerState.CONNECTED:
            return
        self._ble_device.ble_driver.ble_gap_disconnect(self.conn_handle,
                                                       status_code)
        return self._disconnect_waitable

    def set_connection_parameters(self,
                                  min_connection_interval_ms,
                                  max_connection_interval_ms,
                                  connection_timeout_ms,
                                  slave_latency=0):
        """
        Sets the connection parameters for the peer and starts the connection parameter update process

        :param min_connection_interval_ms: The minimum acceptable connection interval, in milliseconds
        :param max_connection_interval_ms: The maximum acceptable connection interval, in milliseconds
        :param connection_timeout_ms: The connection timeout, in milliseconds
        :param slave_latency: The slave latency allowed
        """
        self._ideal_connection_params = ConnectionParameters(
            min_connection_interval_ms, max_connection_interval_ms,
            connection_timeout_ms, slave_latency)
        if not self.connected:
            return
        # Do stuff to set the connection parameters
        self._ble_device.ble_driver.ble_gap_conn_param_update(
            self.conn_handle, self._ideal_connection_params)

    def exchange_mtu(self, mtu_size=None):
        """
        Initiates the MTU Exchange sequence with the peer device.

        If the MTU size is not provided the preferred_mtu_size value will be used.
        If an MTU size is provided the preferred_mtu_size will be updated to this

        :param mtu_size: Optional MTU size to use. If provided, it will also updated the preferred MTU size
        :return: A waitable that will fire when the MTU exchange completes
        :rtype: event_waitable.EventWaitable
        """
        # If the MTU size has already been negotiated we need to use the same value
        # as the previous exchange (Vol 3, Part F 3.4.2.2)
        if self._negotiated_mtu_size is None:
            if mtu_size is not None:
                self._validate_mtu_size(mtu_size)
                self._negotiated_mtu_size = mtu_size
            else:
                self._negotiated_mtu_size = self.preferred_mtu_size

        self._ble_device.ble_driver.ble_gattc_exchange_mtu_req(
            self.conn_handle, self._negotiated_mtu_size)
        return event_waitable.EventWaitable(self._on_mtu_exchange_complete)

    """
    Internal Library Methods
    """

    def peer_connected(self, conn_handle, peer_address, connection_params):
        """
        Internal method called when the peer connects to set up the object
        """
        self.conn_handle = conn_handle
        self.peer_address = peer_address
        self._mtu_size = MTU_SIZE_DEFAULT
        self._negotiated_mtu_size = None
        self._disconnect_waitable = connection_waitable.DisconnectionWaitable(
            self)
        self.connection_state = PeerState.CONNECTED
        self._current_connection_params = connection_params

        self._ble_device.ble_driver.event_subscribe(
            self._on_disconnect_event, nrf_events.GapEvtDisconnected)
        self._ble_device.ble_driver.event_subscribe(
            self._on_connection_param_update, nrf_events.GapEvtConnParamUpdate,
            nrf_events.GapEvtConnParamUpdateRequest)
        self.driver_event_subscribe(self._on_mtu_exchange_request,
                                    nrf_events.GattsEvtExchangeMtuRequest)
        self.driver_event_subscribe(self._on_mtu_exchange_response,
                                    nrf_events.GattcEvtMtuExchangeResponse)
        self._on_connect.notify(self)

    def _check_driver_event_connection_handle_wrapper(self, func):
        def wrapper(driver, event):
            """
            :param driver:
            :type event: blatann.nrf.nrf_events.BLEEvent
            """
            logger.debug("Got event: {} for peer {}".format(
                event, self.conn_handle))
            if self.connected and self.conn_handle == event.conn_handle:
                func(driver, event)

        return wrapper

    def driver_event_subscribe(self, handler, *event_types):
        """
        Internal method that subscribes handlers to NRF Driver events directed at this peer.
        Handlers are automatically unsubscribed once the peer disconnects

        :param handler: The handler to subscribe
        :param event_types: The NRF Driver event types to subscribe to
        """
        wrapped_handler = self._check_driver_event_connection_handle_wrapper(
            handler)
        with self._connection_handler_lock:
            if handler not in self._connection_based_driver_event_handlers:
                self._connection_based_driver_event_handlers[
                    handler] = wrapped_handler
                self._ble_device.ble_driver.event_subscribe(
                    wrapped_handler, *event_types)

    def driver_event_unsubscribe(self, handler, *event_types):
        """
        Internal method that unsubscribes handlers from NRF Driver events

        :param handler: The handler to unsubscribe
        :param event_types: The event types to unsubscribe from
        """
        with self._connection_handler_lock:
            wrapped_handler = self._connection_based_driver_event_handlers.get(
                handler, None)
            logger.debug("Unsubscribing {} ({})".format(
                handler, wrapped_handler))
            if wrapped_handler:
                self._ble_device.ble_driver.event_unsubscribe(
                    wrapped_handler, *event_types)
                del self._connection_based_driver_event_handlers[handler]

    """
    Private Methods
    """

    def _on_disconnect_event(self, driver, event):
        """
        :type event: nrf_events.GapEvtDisconnected
        """
        if not self.connected or self.conn_handle != event.conn_handle:
            return
        self.conn_handle = BLE_CONN_HANDLE_INVALID
        self.connection_state = PeerState.DISCONNECTED
        self._on_disconnect.notify(self, DisconnectionEventArgs(event.reason))

        with self._connection_handler_lock:
            for handler in self._connection_based_driver_event_handlers.values(
            ):
                self._ble_device.ble_driver.event_unsubscribe_all(handler)
            self._connection_based_driver_event_handlers = {}
        self._ble_device.ble_driver.event_unsubscribe(
            self._on_disconnect_event)
        self._ble_device.ble_driver.event_unsubscribe(
            self._on_connection_param_update)

    def _on_connection_param_update(self, driver, event):
        """
        :type event: nrf_events.GapEvtConnParamUpdate
        """
        if not self.connected or self.conn_handle != event.conn_handle:
            return
        if isinstance(event, nrf_events.GapEvtConnParamUpdateRequest):
            logger.debug("[{}] Conn Params updating to {}".format(
                self.conn_handle, self._ideal_connection_params))
            self._ble_device.ble_driver.ble_gap_conn_param_update(
                self.conn_handle, self._ideal_connection_params)
        else:
            logger.debug("[{}] Updated to {}".format(self.conn_handle,
                                                     event.conn_params))
        self._current_connection_params = event.conn_params

    def _validate_mtu_size(self, mtu_size):
        if mtu_size < MTU_SIZE_MINIMUM:
            raise ValueError("Invalid MTU size {}. "
                             "Minimum is {}".format(mtu_size,
                                                    MTU_SIZE_MINIMUM))
        if mtu_size > self.max_mtu_size:
            raise ValueError("Invalid MTU size {}. "
                             "Maximum configured in the BLE device: {}".format(
                                 mtu_size, self._ble_device.max_mtu_size))

    def _resolve_mtu_exchange(self, our_mtu, peer_mtu):
        previous_mtu_size = self._mtu_size
        self._mtu_size = max(min(our_mtu, peer_mtu), MTU_SIZE_MINIMUM)
        logger.debug(
            "[{}] MTU Exchange - Ours: {}, Peers: {}, Effective: {}".format(
                self.conn_handle, our_mtu, peer_mtu, self._mtu_size))
        self._on_mtu_size_updated.notify(
            self, MtuSizeUpdatedEventArgs(previous_mtu_size, self._mtu_size))

        return previous_mtu_size, self._mtu_size

    def _on_mtu_exchange_request(self, driver, event):
        if self._negotiated_mtu_size is None:
            self._negotiated_mtu_size = self.preferred_mtu_size

        self._ble_device.ble_driver.ble_gatts_exchange_mtu_reply(
            self.conn_handle, self._negotiated_mtu_size)
        self._resolve_mtu_exchange(self._negotiated_mtu_size, event.client_mtu)

    def _on_mtu_exchange_response(self, driver, event):
        previous, current = self._resolve_mtu_exchange(
            self._negotiated_mtu_size, event.server_mtu)
        self._on_mtu_exchange_complete.notify(
            self, MtuSizeUpdatedEventArgs(previous, current))

    def __nonzero__(self):
        return self.conn_handle != BLE_CONN_HANDLE_INVALID

    def __bool__(self):
        return self.__nonzero__()
Beispiel #5
0
class Advertiser(object):
    # Constant used to indicate that the BLE device should advertise indefinitely, until
    # connected to or stopped manually
    ADVERTISE_FOREVER = 0

    def __init__(self, ble_device, client):
        """
        :type ble_device: blatann.device.BleDevice
        :type client: blatann.peer.Client
        """
        self.ble_device = ble_device
        self.advertising = False
        self._auto_restart = False
        self.client = client
        self.ble_device.ble_driver.event_subscribe(self._handle_adv_timeout, nrf_events.GapEvtTimeout)
        self.ble_device.ble_driver.event_subscribe(self._handle_disconnect, nrf_events.GapEvtDisconnected)
        self._on_advertising_timeout = EventSource("Advertising Timeout", logger)
        self._advertise_interval = 100
        self._timeout = self.ADVERTISE_FOREVER
        self._advertise_mode = AdvertisingMode.connectable_undirected

    @property
    def on_advertising_timeout(self):
        """
        Event generated whenever advertising times out and finishes with no connections made
        Event args: None

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_advertising_timeout

    def set_advertise_data(self, advertise_data=AdvertisingData(), scan_response=AdvertisingData()):
        """
        Sets the advertising and scan response data which will be broadcasted to peers during advertising

        Note: BLE Restricts advertise and scan response data to an encoded length of 31 bytes each.
        Use AdvertisingData.check_encoded_length() to determine if the

        :param advertise_data: The advertise data to use
        :type advertise_data: AdvertisingData
        :param scan_response: The scan response data to use
        :type scan_response: AdvertisingData
        """
        adv_len, adv_pass = advertise_data.check_encoded_length()
        scan_len, scan_pass = advertise_data.check_encoded_length()

        if not adv_pass:
            raise exceptions.InvalidOperationException("Encoded Advertising data length is too long ({} bytes). "
                                                       "Max: {} bytes".format(adv_len, advertise_data.MAX_ENCODED_LENGTH))

        if not scan_pass:
            raise exceptions.InvalidOperationException("Encoded Scan Response data length is too long ({} bytes). "
                                                       "Max: {} bytes".format(scan_len, advertise_data.MAX_ENCODED_LENGTH))

        self.ble_device.ble_driver.ble_gap_adv_data_set(advertise_data.to_ble_adv_data(), scan_response.to_ble_adv_data())

    def set_default_advertise_params(self, advertise_interval_ms, timeout_seconds, advertise_mode=AdvertisingMode.connectable_undirected):
        """
        Sets the default advertising parameters so they do not need to be specified on each start

        :param advertise_interval_ms: The advertising interval, in milliseconds
        :param timeout_seconds: How long to advertise for before timing out, in seconds
        :param advertise_mode: The mode the advertiser should use
        :type advertise_mode: AdvertisingMode
        """
        self._advertise_interval = advertise_interval_ms
        self._timeout = timeout_seconds
        self._advertise_mode = advertise_mode

    def start(self, adv_interval_ms=None, timeout_sec=None, auto_restart=False, advertise_mode=None):
        """
        Starts advertising with the given parameters. If none given, will use the default

        :param adv_interval_ms: The interval at which to send out advertise packets, in milliseconds
        :param timeout_sec: The duration which to advertise for
        :param auto_restart: Flag indicating that advertising should restart automatically when the timeout expires, or
                             when the client disconnects
        :param advertise_mode: The mode the advertiser should use
        :return: A waitable that will expire either when the timeout occurs, or a client connects.
                 Waitable Returns a peer.Client() object
        :rtype: ClientConnectionWaitable
        """
        if self.advertising:
            self._stop()
        if adv_interval_ms is None:
            adv_interval_ms = self._advertise_interval
        if timeout_sec is None:
            timeout_sec = self._timeout
        if advertise_mode is None:
            advertise_mode = self._advertise_mode
        self._timeout = timeout_sec
        self._advertise_interval = adv_interval_ms
        self._advertise_mode = advertise_mode

        params = nrf_types.BLEGapAdvParams(adv_interval_ms, timeout_sec, advertise_mode)
        self._auto_restart = auto_restart
        logger.info("Starting advertising, params: {}, auto-restart: {}".format(params, auto_restart))
        self.ble_device.ble_driver.ble_gap_adv_start(params)
        self.advertising = True
        return ClientConnectionWaitable(self.ble_device, self.client)

    def stop(self):
        """
        Stops advertising and disables the auto-restart functionality (if enabled)
        """
        self._auto_restart = False
        self._stop()

    def _stop(self):
        if not self.advertising:
            return
        self.advertising = False
        try:
            self.ble_device.ble_driver.ble_gap_adv_stop()
        except Exception:
            pass

    def _handle_adv_timeout(self, driver, event):
        """
        :type event: nrf_events.GapEvtTimeout
        """
        if event.src == nrf_events.BLEGapTimeoutSrc.advertising:
            self.advertising = False
            self._on_advertising_timeout.notify(self)
            if self._auto_restart:
                self.start()

    def _handle_disconnect(self, driver, event):
        """
        :type event: nrf_events.GapEvtDisconnected
        """
        if event.conn_handle == self.client.conn_handle and self._auto_restart:
            self.start()
Beispiel #6
0
class Peer(object):
    """
    Object that represents a BLE-connected (or disconnected) peer
    """
    BLE_CONN_HANDLE_INVALID = BLE_CONN_HANDLE_INVALID
    """ Number of bytes that are header/overhead per MTU when sending a notification or indication """
    NOTIFICATION_INDICATION_OVERHEAD_BYTES = 3

    def __init__(self,
                 ble_device,
                 role,
                 connection_params=DEFAULT_CONNECTION_PARAMS,
                 security_params=DEFAULT_SECURITY_PARAMS,
                 name="",
                 write_no_resp_queue_size=1):
        """
        :type ble_device: blatann.device.BleDevice
        """
        self._ble_device = ble_device
        self._role = role
        self._name = name
        self._preferred_connection_params = connection_params
        self._current_connection_params = ActiveConnectionParameters(
            connection_params)
        self.conn_handle = BLE_CONN_HANDLE_INVALID
        self.peer_address = "",
        self.connection_state = PeerState.DISCONNECTED
        self._on_connect = EventSource("On Connect", logger)
        self._on_disconnect = EventSource("On Disconnect", logger)
        self._on_mtu_exchange_complete = EventSource(
            "On MTU Exchange Complete", logger)
        self._on_mtu_size_updated = EventSource("On MTU Size Updated", logger)
        self._on_data_length_updated = EventSource("On Data Length Updated",
                                                   logger)
        self._on_phy_updated = EventSource("On Phy Updated", logger)
        self._mtu_size = MTU_SIZE_DEFAULT
        self._preferred_mtu_size = MTU_SIZE_DEFAULT
        self._negotiated_mtu_size = None
        self._preferred_phy = Phy.auto
        self._current_phy = Phy.one_mbps
        self._disconnection_reason = nrf_events.BLEHci.local_host_terminated_connection

        self._connection_based_driver_event_handlers = {}
        self._connection_handler_lock = threading.Lock()

        self.security = smp.SecurityManager(self._ble_device, self,
                                            security_params)
        self._db = gattc.GattcDatabase(ble_device, self,
                                       write_no_resp_queue_size)
        self._discoverer = service_discovery.DatabaseDiscoverer(
            ble_device, self)

    """
    Properties
    """

    @property
    def name(self) -> str:
        """
        The name of the peer, if known. This property is for the user's benefit to name certain connections.
        The name is is also saved in the case that the peer is subsequently bonded to and can be looked up that way
        in the bond database

        .. note:: For central peers this name is unknown unless set by the setter.
           For peripheral peers the name is defaulted to the one found in the advertising payload, if any.

       :getter: Gets the name of the peer
       :setter: Sets the name of the peer
        """
        return self._name

    @name.setter
    def name(self, name: str):
        self._name = name

    @property
    def connected(self) -> bool:
        """
        **Read Only**

        Gets if this peer is currently connected
        """
        return self.connection_state == PeerState.CONNECTED

    @property
    def bytes_per_notification(self) -> int:
        """
        **Read Only**

        The maximum number of bytes that can be sent in a single notification/indication
        """
        return self._mtu_size - self.NOTIFICATION_INDICATION_OVERHEAD_BYTES

    @property
    def is_peripheral(self) -> bool:
        """
        **Read Only**

        Gets if this peer is a peripheral (the local device acting as a central/client)
        """
        return isinstance(self, Peripheral)

    @property
    def is_client(self) -> bool:
        """
        **Read Only**

        Gets if this peer is a Client (the local device acting as a peripheral/server)
        """
        return isinstance(self, Client)

    @property
    def is_previously_bonded(self) -> bool:
        """
        **Read Only**

        Gets if the peer has bonding information stored in the bond database (the peer was bonded to in a previous connection)
        """
        return self.security.is_previously_bonded

    @property
    def preferred_connection_params(self) -> ConnectionParameters:
        """
        **Read Only**

        The connection parameters that were negotiated for this peer
        """
        return self._preferred_connection_params

    @property
    def active_connection_params(self) -> ActiveConnectionParameters:
        """
        **Read Only**

        The active connection parameters in use with the peer.
        If the peer is disconnected, this will return the connection parameters last used
        """
        return self._current_connection_params

    @property
    def mtu_size(self) -> int:
        """
        **Read Only**

        The current size of the MTU for the connection to the peer
        """
        return self._mtu_size

    @property
    def max_mtu_size(self) -> int:
        """
        **Read Only**

        The maximum allowed MTU size. This is set when initially configuring the BLE Device
        """
        return self._ble_device.max_mtu_size

    @property
    def preferred_mtu_size(self) -> int:
        """
        The user-set preferred MTU size. Defaults to the Bluetooth default MTU size (23).
        This is the value that will be negotiated during an MTU Exchange but is not guaranteed in the case that the peer has a smaller MTU

        :getter: Gets the preferred MTU size that was configured
        :setter: Sets the preferred MTU size to use for MTU exchanges
        """
        return self._preferred_mtu_size

    @preferred_mtu_size.setter
    def preferred_mtu_size(self, mtu_size: int):
        self._validate_mtu_size(mtu_size)
        self._preferred_mtu_size = mtu_size

    @property
    def preferred_phy(self) -> Phy:
        """
        The PHY that is preferred for this connection.
        This value is used for Peer-initiated PHY update procedures and
        as the default for :meth:`update_phy`.

        Default value is :attr:`Phy.auto`

        :getter: Gets the preferred PHY
        :setter: Sets the preferred PHY
        """
        return self._preferred_phy

    @preferred_phy.setter
    def preferred_phy(self, phy: Phy):
        self._preferred_phy = phy

    @property
    def phy_channel(self) -> Phy:
        """
        **Read Only**

        The current PHY in use for the connection
        """
        return self._current_phy

    @property
    def database(self) -> gattc.GattcDatabase:
        """
        **Read Only**

        The GATT database of the peer.

        .. note:: This is not useful until services are discovered first
        """
        return self._db

    """
    Events
    """

    @property
    def on_connect(self) -> Event[Peer, None]:
        """
        Event generated when the peer connects to the local device
        """
        return self._on_connect

    @property
    def on_disconnect(self) -> Event[Peer, DisconnectionEventArgs]:
        """
        Event generated when the peer disconnects from the local device
        """
        return self._on_disconnect

    @property
    def on_mtu_exchange_complete(self) -> Event[Peer, MtuSizeUpdatedEventArgs]:
        """
        Event generated when an MTU exchange completes with the peer
        """
        return self._on_mtu_exchange_complete

    @property
    def on_mtu_size_updated(self) -> Event[Peer, MtuSizeUpdatedEventArgs]:
        """
        Event generated when the effective MTU size has been updated on the connection
        """
        return self._on_mtu_size_updated

    @property
    def on_data_length_updated(
            self) -> Event[Peer, DataLengthUpdatedEventArgs]:
        """
        Event generated when the link layer data length has been updated
        """
        return self._on_data_length_updated

    @property
    def on_phy_updated(self) -> Event[Peer, PhyUpdatedEventArgs]:
        """
        Event generated when the PHY in use for this peer has been updated
        """
        return self._on_phy_updated

    @property
    def on_database_discovery_complete(
            self) -> Event[Peripheral, DatabaseDiscoveryCompleteEventArgs]:
        """
        Event that is triggered when database discovery has completed
        """
        return self._discoverer.on_discovery_complete

    """
    Public Methods
    """

    def disconnect(
        self,
        status_code=nrf_events.BLEHci.remote_user_terminated_connection
    ) -> DisconnectionWaitable:
        """
        Disconnects from the peer, giving the optional status code.
        Returns a waitable that will trigger when the disconnection is complete.
        If the peer is already disconnected, the waitable will trigger immediately

        :param status_code: The HCI Status code to send back to the peer
        :return: A waitable that will trigger when the peer is disconnected
        """
        if self.connection_state != PeerState.CONNECTED:
            return EmptyWaitable(self, self._disconnection_reason)
        self._ble_device.ble_driver.ble_gap_disconnect(self.conn_handle,
                                                       status_code)
        return self._disconnect_waitable

    def set_connection_parameters(self,
                                  min_connection_interval_ms: float,
                                  max_connection_interval_ms: float,
                                  connection_timeout_ms: int,
                                  slave_latency=0):
        """
        Sets the connection parameters for the peer and starts the connection parameter update process (if connected)

        .. note:: Connection interval values should be a multiple of 1.25ms since that is the granularity allowed in the Bluetooth specification.
           Any non-multiples will be rounded down to the nearest 1.25ms.
           Additionally, the connection timeout has a granularity of 10 milliseconds and will also be rounded as such.

        :param min_connection_interval_ms: The minimum acceptable connection interval, in milliseconds
        :param max_connection_interval_ms: The maximum acceptable connection interval, in milliseconds
        :param connection_timeout_ms: The connection timeout, in milliseconds
        :param slave_latency: The slave latency allowed, which regulates how many connection intervals the peripheral is allowed to skip before responding
        """
        self._preferred_connection_params = ConnectionParameters(
            min_connection_interval_ms, max_connection_interval_ms,
            connection_timeout_ms, slave_latency)
        if self.connected:
            self.update_connection_parameters()

    def update_connection_parameters(self):
        """
        Starts the process to re-negotiate the connection parameters using the previously-set connection parameters
        """
        self._ble_device.ble_driver.ble_gap_conn_param_update(
            self.conn_handle, self._preferred_connection_params)

    def exchange_mtu(
            self,
            mtu_size=None) -> EventWaitable[Peer, MtuSizeUpdatedEventArgs]:
        """
        Initiates the MTU Exchange sequence with the peer device.

        If the MTU size is not provided :attr:`preferred_mtu_size` value will be used.
        If an MTU size is provided ``preferred_mtu_size`` will be updated to the given value.

        :param mtu_size: Optional MTU size to use. If provided, it will also updated the preferred MTU size
        :return: A waitable that will trigger when the MTU exchange completes
        """
        # If the MTU size has already been negotiated we need to use the same value
        # as the previous exchange (Vol 3, Part F 3.4.2.2)
        if self._negotiated_mtu_size is None:
            if mtu_size is not None:
                self._validate_mtu_size(mtu_size)
                self._negotiated_mtu_size = mtu_size
            else:
                self._negotiated_mtu_size = self.preferred_mtu_size

        self._ble_device.ble_driver.ble_gattc_exchange_mtu_req(
            self.conn_handle, self._negotiated_mtu_size)
        return EventWaitable(self._on_mtu_exchange_complete)

    def update_data_length(
        self,
        data_length: int = None
    ) -> EventWaitable[Peripheral, DataLengthUpdatedEventArgs]:
        """
        Starts the process which updates the link layer data length to the optimal value given the MTU.
        For best results call this method after the MTU is set to the desired size.

        :param data_length: Optional value to override the data length to.
                            If not provided, uses the optimal value based on the current MTU
        :return: A waitable that will trigger when the process finishes
        """
        if data_length is not None:
            if data_length > DLE_MAX or data_length < DLE_MIN:
                raise ValueError(
                    f"Data length must be between {DLE_MIN} and {DLE_MAX} (inclusive)"
                )
        else:
            data_length = self.mtu_size + DLE_OVERHEAD

        params = BLEGapDataLengthParams(data_length, data_length)
        self._ble_device.ble_driver.ble_gap_data_length_update(
            self.conn_handle, params)
        return EventWaitable(self._on_data_length_updated)

    def update_phy(self,
                   phy: Phy = None
                   ) -> EventWaitable[Peer, PhyUpdatedEventArgs]:
        """
        Performs the PHY update procedure, negotiating a new PHY (1Mbps, 2Mbps, or coded PHY)
        to use for the connection. Performing this procedure does not guarantee that the PHY
        will change based on what the peer supports.

        :param phy: Optional PHY to use. If None, uses the :attr:`preferred_phy` attribute.
                    If not None, the preferred PHY is updated to this value.
        :return: An event waitable that triggers when the phy process completes
        """
        if phy is None:
            phy = self._preferred_phy
        else:
            self._preferred_phy = phy
        self._ble_device.ble_driver.ble_gap_phy_update(self.conn_handle, phy,
                                                       phy)
        return EventWaitable(self._on_phy_updated)

    def discover_services(
            self) -> EventWaitable[Peer, DatabaseDiscoveryCompleteEventArgs]:
        """
        Starts the database discovery process of the peer. This will discover all services, characteristics, and
        descriptors on the peer's database.

        :return: a Waitable that will trigger when service discovery is complete
        """
        self._discoverer.start()
        return EventWaitable(self._discoverer.on_discovery_complete)

    """
    Internal Library Methods
    """

    def peer_connected(self, conn_handle, peer_address, connection_params):
        """
        Internal method called when the peer connects to set up the object.

        **Should not be called by the user**
        """
        self.conn_handle = conn_handle
        self.peer_address = peer_address
        self._mtu_size = MTU_SIZE_DEFAULT
        self._negotiated_mtu_size = None
        self._disconnect_waitable = DisconnectionWaitable(self)
        self.connection_state = PeerState.CONNECTED
        self._current_connection_params = ActiveConnectionParameters(
            connection_params)

        self._ble_device.ble_driver.event_subscribe(
            self._on_disconnect_event, nrf_events.GapEvtDisconnected)
        self.driver_event_subscribe(self._on_connection_param_update,
                                    nrf_events.GapEvtConnParamUpdate,
                                    nrf_events.GapEvtConnParamUpdateRequest)
        self.driver_event_subscribe(self._on_mtu_exchange_request,
                                    nrf_events.GattsEvtExchangeMtuRequest)
        self.driver_event_subscribe(self._on_mtu_exchange_response,
                                    nrf_events.GattcEvtMtuExchangeResponse)
        self.driver_event_subscribe(self._on_data_length_update_request,
                                    nrf_events.GapEvtDataLengthUpdateRequest)
        self.driver_event_subscribe(self._on_data_length_update,
                                    nrf_events.GapEvtDataLengthUpdate)
        self.driver_event_subscribe(self._on_phy_update_request,
                                    nrf_events.GapEvtPhyUpdateRequest)
        self.driver_event_subscribe(self._on_phy_update,
                                    nrf_events.GapEvtPhyUpdate)
        self._on_connect.notify(self)

    def _check_driver_event_connection_handle_wrapper(self, func):
        def wrapper(driver, event):
            if self.connected and self.conn_handle == event.conn_handle:
                func(driver, event)

        return wrapper

    def driver_event_subscribe(self, handler, *event_types):
        """
        Internal method that subscribes handlers to NRF Driver events directed at this peer.
        Handlers are automatically unsubscribed once the peer disconnects.

        :param handler: The handler to subscribe
        :param event_types: The NRF Driver event types to subscribe to
        """
        wrapped_handler = self._check_driver_event_connection_handle_wrapper(
            handler)
        with self._connection_handler_lock:
            if handler not in self._connection_based_driver_event_handlers:
                self._connection_based_driver_event_handlers[
                    handler] = wrapped_handler
                self._ble_device.ble_driver.event_subscribe(
                    wrapped_handler, *event_types)

    def driver_event_unsubscribe(self, handler, *event_types):
        """
        Internal method that unsubscribes handlers from NRF Driver events.

        :param handler: The handler to unsubscribe
        :param event_types: The event types to unsubscribe from
        """
        with self._connection_handler_lock:
            wrapped_handler = self._connection_based_driver_event_handlers.get(
                handler, None)
            logger.debug("Unsubscribing {} ({})".format(
                handler, wrapped_handler))
            if wrapped_handler:
                self._ble_device.ble_driver.event_unsubscribe(
                    wrapped_handler, *event_types)
                del self._connection_based_driver_event_handlers[handler]

    """
    Private Methods
    """

    def _on_disconnect_event(self, driver, event):
        """
        :type event: nrf_events.GapEvtDisconnected
        """
        if not self.connected or self.conn_handle != event.conn_handle:
            return
        self.conn_handle = BLE_CONN_HANDLE_INVALID
        self.connection_state = PeerState.DISCONNECTED
        self._disconnection_reason = event.reason
        self._on_disconnect.notify(self, DisconnectionEventArgs(event.reason))

        with self._connection_handler_lock:
            for handler in self._connection_based_driver_event_handlers.values(
            ):
                self._ble_device.ble_driver.event_unsubscribe_all(handler)
            self._connection_based_driver_event_handlers = {}
        self._ble_device.ble_driver.event_unsubscribe(
            self._on_disconnect_event)
        self._ble_device.ble_driver.event_unsubscribe(
            self._on_connection_param_update)

    def _on_connection_param_update(self, driver, event):
        """
        :type event: nrf_events.GapEvtConnParamUpdate
        """
        if not self.connected or self.conn_handle != event.conn_handle:
            return
        if isinstance(event, nrf_events.GapEvtConnParamUpdateRequest):
            logger.debug("[{}] Conn Params updating to {}".format(
                self.conn_handle, self._preferred_connection_params))
            self._ble_device.ble_driver.ble_gap_conn_param_update(
                self.conn_handle, self._preferred_connection_params)
        else:
            logger.debug("[{}] Updated to {}".format(self.conn_handle,
                                                     event.conn_params))
            self._current_connection_params = ActiveConnectionParameters(
                event.conn_params)

    def _validate_mtu_size(self, mtu_size):
        if mtu_size < MTU_SIZE_MINIMUM:
            raise ValueError("Invalid MTU size {}. "
                             "Minimum is {}".format(mtu_size,
                                                    MTU_SIZE_MINIMUM))
        if mtu_size > self.max_mtu_size:
            raise ValueError("Invalid MTU size {}. "
                             "Maximum configured in the BLE device: {}".format(
                                 mtu_size, self._ble_device.max_mtu_size))

    def _resolve_mtu_exchange(self, our_mtu, peer_mtu):
        previous_mtu_size = self._mtu_size
        self._mtu_size = max(min(our_mtu, peer_mtu), MTU_SIZE_MINIMUM)
        logger.debug(
            "[{}] MTU Exchange - Ours: {}, Peers: {}, Effective: {}".format(
                self.conn_handle, our_mtu, peer_mtu, self._mtu_size))
        self._on_mtu_size_updated.notify(
            self, MtuSizeUpdatedEventArgs(previous_mtu_size, self._mtu_size))

        return previous_mtu_size, self._mtu_size

    def _on_mtu_exchange_request(self, driver, event):
        if self._negotiated_mtu_size is None:
            self._negotiated_mtu_size = self.preferred_mtu_size

        self._ble_device.ble_driver.ble_gatts_exchange_mtu_reply(
            self.conn_handle, self._negotiated_mtu_size)
        self._resolve_mtu_exchange(self._negotiated_mtu_size, event.client_mtu)

    def _on_mtu_exchange_response(self, driver, event):
        previous, current = self._resolve_mtu_exchange(
            self._negotiated_mtu_size, event.server_mtu)
        self._on_mtu_exchange_complete.notify(
            self, MtuSizeUpdatedEventArgs(previous, current))

    def _on_data_length_update_request(self, driver, event):
        self._ble_device.ble_driver.ble_gap_data_length_update(
            self.conn_handle)

    def _on_data_length_update(self, driver, event):
        event_args = DataLengthUpdatedEventArgs(event.max_tx_octets,
                                                event.max_rx_octets,
                                                event.max_tx_time_us,
                                                event.max_rx_time_us)
        self._on_data_length_updated.notify(self, event_args)

    def _on_phy_update_request(self, driver, event):
        self._ble_device.ble_driver.ble_gap_phy_update(self.conn_handle)

    def _on_phy_update(self, driver, event: nrf_events.GapEvtPhyUpdate):
        self._current_phy = Phy(event.rx_phy) | Phy(event.tx_phy)
        self._on_phy_updated.notify(
            self, PhyUpdatedEventArgs(event.status, self._current_phy))

    def __nonzero__(self):
        return self.conn_handle != BLE_CONN_HANDLE_INVALID

    def __bool__(self):
        return self.__nonzero__()
Beispiel #7
0
class DatabaseDiscoverer(object):
    def __init__(self, ble_device, peer):
        """
        :type ble_device: blatann.device.BleDevice
        :type peer: blatann.peer.Peer
        """
        self.ble_device = ble_device
        self.peer = peer
        self._on_discovery_complete = EventSource("Service Discovery Complete",
                                                  logger)
        self._on_database_discovery_complete = EventSource(
            "Service Discovery Complete", logger)
        self._state = _DiscoveryState()
        self._service_discoverer = _ServiceDiscoverer(ble_device, peer)
        self._characteristic_discoverer = _CharacteristicDiscoverer(
            ble_device, peer)
        self._descriptor_discoverer = _DescriptorDiscoverer(ble_device, peer)

    @property
    def on_discovery_complete(self):
        """
        :rtype: Event[blatann.peer.Peripheral, DatabaseDiscoveryCompleteEventArgs]
        """
        return self._on_discovery_complete

    def _on_service_discovery_complete(self, sender, event_args):
        """
        :type sender: _ServiceDiscoverer
        :type event_args: _DiscoveryEventArgs
        """
        logger.info("Service Discovery complete")
        if event_args.status != nrf_events.BLEGattStatusCode.success:
            logger.error("Error discovering services: {}".format(
                event_args.status))
            self._on_complete([], event_args.status)
        else:
            self._characteristic_discoverer.start(event_args.services).then(
                self._on_characteristic_discovery_complete)

    def _on_characteristic_discovery_complete(self, sender, event_args):
        """
        :type sender: _CharacteristicDiscoverer
        :type event_args: _DiscoveryEventArgs
        """
        logger.info("Characteristic Discovery complete")
        if event_args.status != nrf_events.BLEGattStatusCode.success:
            logger.error("Error discovering characteristics: {}".format(
                event_args.status))
            self._on_complete([], event_args.status)
        else:
            self._descriptor_discoverer.start(event_args.services).then(
                self._on_descriptor_discovery_complete)

    def _on_descriptor_discovery_complete(self, sender, event_args):
        """
        :type sender: _DescriptorDiscoverer
        :type event_args: _DiscoveryEventArgs
        """
        logger.info("Descriptor Discovery complete")
        self._on_complete(event_args.services, event_args.status)

    def _on_complete(self, services, status):
        self.peer.database.add_discovered_services(services)
        self._on_discovery_complete.notify(
            self.peer, DatabaseDiscoveryCompleteEventArgs(status))
        logger.info("Database Discovery complete")

    def start(self):
        logger.info("Starting discovery..")
        self._service_discoverer.start().then(
            self._on_service_discovery_complete)
Beispiel #8
0
class SecurityManager(object):
    """
    Handles performing security procedures with a connected peer
    """
    def __init__(self, ble_device, peer, security_parameters):
        """
        :type ble_device: blatann.BleDevice
        :type peer: blatann.peer.Peer
        :type security_parameters: SecurityParameters
        """
        self.ble_device = ble_device
        self.peer = peer
        self.security_params = security_parameters
        self._pairing_in_process = False
        self._initiated_encryption = False
        self._is_previously_bonded_device = False
        self._on_authentication_complete_event = EventSource(
            "On Authentication Complete", logger)
        self._on_passkey_display_event = EventSource("On Passkey Display",
                                                     logger)
        self._on_passkey_entry_event = EventSource("On Passkey Entry", logger)
        self._on_security_level_changed_event = EventSource(
            "Security Level Changed", logger)
        self.peer.on_connect.register(self._on_peer_connected)
        self._auth_key_resolve_thread = threading.Thread()
        self.keyset = nrf_types.BLEGapSecKeyset()
        self.bond_db_entry = None
        self._security_level = SecurityLevel.NO_ACCESS
        self._private_key = smp_crypto.lesc_generate_private_key()
        self._public_key = self._private_key.public_key()
        self.keyset.own_keys.public_key.key = smp_crypto.lesc_pubkey_to_raw(
            self._public_key)

    """
    Events
    """

    @property
    def on_pairing_complete(self):
        """
        Event that is triggered when pairing completes with the peer
        EventArgs type: PairingCompleteEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_authentication_complete_event

    @property
    def on_security_level_changed(self):
        """
        Event that is triggered when the security/encryption level changes. This can be triggered from
        a pairing sequence or if a bonded client starts the encryption handshaking using the stored LTKs.

        Note: This event is triggered before on_pairing_complete

        EventArgs type: SecurityLevelChangedEventArgs
        :return: an Event which can have handlers registered to and deregestestered from
        :rtype: Event
        """
        return self._on_security_level_changed_event

    @property
    def on_passkey_display_required(self):
        """
        Event that is triggered when a passkey needs to be displayed to the user
        EventArgs type: PasskeyDisplayEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_passkey_display_event

    @property
    def on_passkey_required(self):
        """
        Event that is triggered when a passkey needs to be entered by the user
        EventArgs type: PasskeyEntryEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_passkey_entry_event

    @property
    def is_previously_bonded(self):
        """
        Gets if the peer this security manager is for was bonded in a previous connection

        :return: True if previously bonded, False if not
        """
        return self._is_previously_bonded_device

    @property
    def security_level(self):
        """
        Gets the current security level of the connection

        :rtype: SecurityLevel
        """
        return self._security_level

    """
    Public Methods
    """

    def set_security_params(self,
                            passcode_pairing,
                            io_capabilities,
                            bond,
                            out_of_band,
                            reject_pairing_requests=False,
                            lesc_pairing=False):
        """
        Sets the security parameters to use with the peer

        :param passcode_pairing: Flag indicating that passcode pairing is required
        :type passcode_pairing: bool
        :param io_capabilities: The input/output capabilities of this device
        :type io_capabilities: IoCapabilities
        :param bond: Flag indicating that long-term bonding should be performed
        :type bond: bool
        :param out_of_band: Flag indicating if out-of-band pairing is supported
        :type out_of_band: bool
        :param reject_pairing_requests: Flag indicating that all security requests by the peer should be rejected
        :type reject_pairing_requests: bool
        :param lesc_pairing: Flag indicating that LE Secure Pairing methods are supported
        """
        self.security_params = SecurityParameters(passcode_pairing,
                                                  io_capabilities, bond,
                                                  out_of_band,
                                                  reject_pairing_requests,
                                                  lesc_pairing)

    def pair(self, force_repairing=False):
        """
        Starts the encrypting process with the peer. If the peer has already been bonded to,
        Starts the pairing process with the peer given the set security parameters
        and returns a Waitable which will fire when the pairing process completes, whether successful or not.
        Waitable returns two parameters: (Peer, PairingCompleteEventArgs)

        :return: A waitiable that will fire when pairing is complete
        :rtype: blatann.waitables.EventWaitable
        """
        if self._pairing_in_process or self._initiated_encryption:
            raise InvalidStateException("Security manager busy")
        if self.security_params.reject_pairing_requests:
            raise InvalidOperationException(
                "Cannot initiate pairing while rejecting pairing requests")

        # if in the client role and don't want to force a re-pair, check for bonding data first
        if self.peer.is_peripheral and not force_repairing:
            bond_entry = self._find_db_entry(self.peer.peer_address)
            if bond_entry:
                logger.info("Re-establishing encryption with peer using LTKs")
                self.ble_device.ble_driver.ble_gap_encrypt(
                    self.peer.conn_handle,
                    bond_entry.bonding_data.own_ltk.master_id,
                    bond_entry.bonding_data.own_ltk.enc_info)
                self._initiated_encryption = True

        sec_params = self._get_security_params()
        self.ble_device.ble_driver.ble_gap_authenticate(
            self.peer.conn_handle, sec_params)
        self._pairing_in_process = True
        return EventWaitable(self.on_pairing_complete)

    def use_debug_lesc_key(self):
        """
        Changes the security settings to use the debug public/private key-pair for future LESC pairing interactions.
        The key is defined in the Core Bluetooth Specification v4.2 Vol.3, Part H, Section 2.3.5.6.

        .. warning:: Using this key allows Bluetooth sniffers to be able to decode the encrypted traffic over the air
        """
        self._private_key = smp_crypto.LESC_DEBUG_PRIVATE_KEY
        self._public_key = smp_crypto.LESC_DEBUG_PUBLIC_KEY
        self.keyset.own_keys.public_key.key = smp_crypto.lesc_pubkey_to_raw(
            self._public_key)

    """
    Private Methods
    """

    def _on_peer_connected(self, peer, event_args):
        # Reset the
        self._pairing_in_process = False
        self._initiated_encryption = False
        self._security_level = SecurityLevel.OPEN
        self.peer.driver_event_subscribe(self._on_security_params_request,
                                         nrf_events.GapEvtSecParamsRequest)
        self.peer.driver_event_subscribe(self._on_authentication_status,
                                         nrf_events.GapEvtAuthStatus)
        self.peer.driver_event_subscribe(self._on_conn_sec_status,
                                         nrf_events.GapEvtConnSecUpdate)
        self.peer.driver_event_subscribe(self._on_auth_key_request,
                                         nrf_events.GapEvtAuthKeyRequest)
        self.peer.driver_event_subscribe(self._on_passkey_display,
                                         nrf_events.GapEvtPasskeyDisplay)
        self.peer.driver_event_subscribe(self._on_security_info_request,
                                         nrf_events.GapEvtSecInfoRequest)
        self.peer.driver_event_subscribe(self._on_lesc_dhkey_request,
                                         nrf_events.GapEvtLescDhKeyRequest)
        # Search the bonding DB for this peer's info
        self.bond_db_entry = self._find_db_entry(self.peer.peer_address)
        if self.bond_db_entry:
            logger.info("Connected to previously bonded device {}".format(
                self.bond_db_entry.peer_addr))
            self._is_previously_bonded_device = True

    def _find_db_entry(self, peer_address):
        if peer_address.addr_type == nrf_types.BLEGapAddrTypes.random_private_non_resolvable:
            return None

        for r in self.ble_device.bond_db:
            if self.peer.is_client != r.peer_is_client:
                continue

            # If peer address is public or random static, check directly if they match (no IRK needed)
            if peer_address.addr_type in [
                    nrf_types.BLEGapAddrTypes.random_static,
                    nrf_types.BLEGapAddrTypes.public
            ]:
                if r.peer_addr == peer_address:
                    return r
            elif smp_crypto.private_address_resolves(
                    peer_address, r.bonding_data.peer_id.irk):
                logger.info("Resolved Peer ID to {}".format(r.peer_addr))
                return r

        return None

    def _get_security_params(self):
        keyset_own = nrf_types.BLEGapSecKeyDist(True, True, False, False)
        keyset_peer = nrf_types.BLEGapSecKeyDist(True, True, False, False)
        sec_params = nrf_types.BLEGapSecParams(
            self.security_params.bond, self.security_params.passcode_pairing,
            self.security_params.lesc_pairing, False,
            self.security_params.io_capabilities,
            self.security_params.out_of_band, 7, 16, keyset_own, keyset_peer)
        return sec_params

    def _on_security_params_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtSecParamsRequest
        """
        # Security parameters are only provided for clients
        sec_params = self._get_security_params(
        ) if self.peer.is_client else None

        if self.security_params.reject_pairing_requests:
            status = nrf_types.BLEGapSecStatus.pairing_not_supp
        else:
            status = nrf_types.BLEGapSecStatus.success

        self.ble_device.ble_driver.ble_gap_sec_params_reply(
            event.conn_handle, status, sec_params, self.keyset)

        if not self.security_params.reject_pairing_requests:
            self._pairing_in_process = True

    def _on_security_info_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtSecInfoRequest
        """
        found_record = None
        # Find the database entry based on the sec info given
        for r in self.ble_device.bond_db:
            # Check that roles match
            if r.peer_is_client != self.peer.is_client:
                continue
            own_mid = r.bonding_data.own_ltk.master_id
            peer_mid = r.bonding_data.peer_ltk.master_id
            if event.master_id.ediv == own_mid.ediv and event.master_id.rand == own_mid.rand:
                logger.info(
                    "Found matching record with own master ID for sec info request"
                )
                found_record = r
                break
            if event.master_id.ediv == peer_mid.ediv and event.master_id.rand == peer_mid.rand:
                logger.info(
                    "Found matching record with peer master ID for sec info request"
                )
                found_record = r
                break

        if not found_record:
            logger.info(
                "Unable to find Bonding record for peer master id {}".format(
                    event.master_id))
            self.ble_device.ble_driver.ble_gap_sec_info_reply(
                event.conn_handle)
        else:
            self.bond_db_entry = found_record
            ltk = found_record.bonding_data.own_ltk
            id_key = found_record.bonding_data.peer_id
            self.ble_device.ble_driver.ble_gap_sec_info_reply(
                event.conn_handle, ltk.enc_info, id_key, None)

    def _on_lesc_dhkey_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtLescDhKeyRequest
        """
        peer_public_key = smp_crypto.lesc_pubkey_from_raw(
            event.remote_public_key.key)
        dh_key = smp_crypto.lesc_compute_dh_key(self._private_key,
                                                peer_public_key,
                                                little_endian=True)
        self.ble_device.ble_driver.ble_gap_lesc_dhkey_reply(
            event.conn_handle, nrf_types.BLEGapDhKey(dh_key))

    def _on_conn_sec_status(self, driver, event):
        """
        :type event: nrf_events.GapEvtConnSecUpdate
        """
        self._security_level = SecurityLevel(event.sec_level)
        self._on_security_level_changed_event.notify(
            self.peer, SecurityLevelChangedEventArgs(self._security_level))
        if self._initiated_encryption:
            self._initiated_encryption = False
            if event.sec_level > 0 and event.sec_mode > 0:
                status = SecurityStatus.success
            else:
                # Peer failed to find/load the keys, return failure status code
                status = SecurityStatus.unspecified
            self._on_authentication_complete_event.notify(
                self.peer, PairingCompleteEventArgs(status,
                                                    self.security_level))

    def _on_authentication_status(self, driver, event):
        """
        :type event: nrf_events.GapEvtAuthStatus
        """
        self._pairing_in_process = False
        self._on_authentication_complete_event.notify(
            self.peer,
            PairingCompleteEventArgs(event.auth_status, self.security_level))
        # Save keys in the database if authenticated+bonded successfullly
        if event.auth_status == SecurityStatus.success and event.bonded:
            # Reload the keys from the C Memory space (were updated during the pairing process)
            self.keyset.reload()

            # If there wasn't a bond record initially, try again a second time using the new public peer address
            if not self.bond_db_entry:
                self.bond_db_entry = self._find_db_entry(
                    self.keyset.peer_keys.id_key.peer_addr)

            # Still no bond DB entry, create a new one
            if not self.bond_db_entry:
                logger.info("New bonded device, creating a DB Entry")
                self.bond_db_entry = self.ble_device.bond_db.create()
                self.bond_db_entry.peer_is_client = self.peer.is_client
                self.bond_db_entry.peer_addr = self.keyset.peer_keys.id_key.peer_addr
                self.bond_db_entry.bonding_data = BondingData(self.keyset)
                self.ble_device.bond_db.add(self.bond_db_entry)
            else:  # update the bonding info
                logger.info("Updating bond key for peer {}".format(
                    self.keyset.peer_keys.id_key.peer_addr))
                self.bond_db_entry.bonding_data = BondingData(self.keyset)

            # TODO: This doesn't belong here..
            self.ble_device.bond_db_loader.save(self.ble_device.bond_db)

    def _on_passkey_display(self, driver, event):
        """
        :type event: nrf_events.GapEvtPasskeyDisplay
        """
        def match_confirm(keys_match):
            if not self._pairing_in_process:
                return
            if keys_match:
                key_type = nrf_types.BLEGapAuthKeyType.PASSKEY
            else:
                key_type = nrf_types.BLEGapAuthKeyType.NONE

            self.ble_device.ble_driver.ble_gap_auth_key_reply(
                event.conn_handle, key_type, None)

        event_args = PasskeyDisplayEventArgs(event.passkey,
                                             event.match_request,
                                             match_confirm)
        if event.match_request:
            self._auth_key_resolve_thread = threading.Thread(
                name="{} Passkey Confirm".format(self.peer.conn_handle),
                target=self._on_passkey_display_event.notify,
                args=(self.peer, event_args))
            self._auth_key_resolve_thread.daemon = True
            self._auth_key_resolve_thread.start()
        else:
            self._on_passkey_display_event.notify(self.peer, event_args)

    def _on_auth_key_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtAuthKeyRequest
        """
        def resolve(passkey):
            if not self._pairing_in_process:
                return
            if isinstance(passkey, (long, int)):
                passkey = "{:06d}".format(passkey)
            elif isinstance(passkey, unicode):
                passkey = str(passkey)
            self.ble_device.ble_driver.ble_gap_auth_key_reply(
                self.peer.conn_handle, event.key_type, passkey)

        self._auth_key_resolve_thread = threading.Thread(
            name="{} Passkey Entry".format(self.peer.conn_handle),
            target=self._on_passkey_entry_event.notify,
            args=(self.peer, PasskeyEntryEventArgs(event.key_type, resolve)))
        self._auth_key_resolve_thread.daemon = True
        self._auth_key_resolve_thread.start()

    def _on_timeout(self, driver, event):
        """
        :type event: nrf_events.GapEvtTimeout
        """
        if event.src != nrf_types.BLEGapTimeoutSrc.security_req:
            return
        self._on_authentication_complete_event.notify(
            self.peer,
            PairingCompleteEventArgs(SecurityStatus.timeout,
                                     self.security_level))
Beispiel #9
0
class GattcCharacteristic(gatt.Characteristic):
    def __init__(self,
                 ble_device,
                 peer,
                 read_write_manager,
                 uuid,
                 properties,
                 declaration_handle,
                 value_handle,
                 cccd_handle=None):
        """
        :type ble_device: blatann.BleDevice
        :type peer: blatann.peer.Peripheral
        :type read_write_manager: _ReadWriteManager
        :type uuid: blatann.uuid.Uuid
        :type properties: gatt.CharacteristicProperties
        :param declaration_handle:
        :param value_handle:
        :param cccd_handle:
        """
        super(GattcCharacteristic, self).__init__(ble_device, peer, uuid,
                                                  properties)
        self.declaration_handle = declaration_handle
        self.value_handle = value_handle
        self.cccd_handle = cccd_handle
        self._manager = read_write_manager
        self._value = ""

        self._on_notification_event = EventSource("On Notification", logger)
        self._on_read_complete_event = EventSource("On Read Complete", logger)
        self._on_write_complete_event = EventSource("Write Complete", logger)
        self._on_cccd_write_complete_event = EventSource(
            "CCCD Write Complete", logger)

        self.peer.driver_event_subscribe(self._on_indication_notification,
                                         nrf_events.GattcEvtHvx)
        self._manager.on_write_complete.register(self._write_complete)
        self._manager.on_read_complete.register(self._read_complete)

    """
    Properties
    """

    @property
    def value(self):
        """
        The current value of the characteristic

        :return: The last known value of the characteristic
        """
        return self._value

    @property
    def readable(self):
        """
        Gets if the characteristic can be read from
        """
        return self._properties.read

    @property
    def writable(self):
        """
        Gets if the characteristic can be written to
        """
        return self._properties.write

    @property
    def subscribable(self):
        """
        Gets if the characteristic can be subscribed to
        """
        return self._properties.notify or self._properties.indicate

    @property
    def subscribed(self):
        """
        Gets if the characteristic is currently subscribed to
        """
        return self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED

    """
    Events
    """

    @property
    def on_read_complete(self):
        return self._on_read_complete_event

    @property
    def on_write_complete(self):
        return self._on_write_complete_event

    """
    Public Methods
    """

    def subscribe(self, on_notification_handler, prefer_indications=False):
        """
        Subscribes to the characteristic's indications or notifications, depending on what's available and the
        prefer_indications setting. Returns a Waitable that executes when the subscription on the peripheral finishes.

        The Waitable returns two parameters: (GattcCharacteristic this, SubscriptionWriteCompleteEventArgs event args)

        :param on_notification_handler: The handler to be called when an indication or notification is received from
        the peripheral. Must take three parameters: (GattcCharacteristic this, gatt.GattNotificationType, bytearray data)
        :param prefer_indications: If the peripheral supports both indications and notifications,
                                   will subscribe to indications instead of notifications
        :return: A Waitable that will fire when the subscription finishes
        :rtype: blatann.waitables.EventWaitable
        :raises: InvalidOperationException if the characteristic cannot be subscribed to
                (characteristic does not support indications or notifications)
        """
        if not self.subscribable:
            raise InvalidOperationException(
                "Cannot subscribe to Characteristic {}".format(self.uuid))
        if prefer_indications and self._properties.indicate or not self._properties.notify:
            value = gatt.SubscriptionState.INDICATION
        else:
            value = gatt.SubscriptionState.NOTIFY
        self._on_notification_event.register(on_notification_handler)

        write_id = self._manager.write(self.cccd_handle,
                                       gatt.SubscriptionState.to_buffer(value))
        return IdBasedEventWaitable(self._on_cccd_write_complete_event,
                                    write_id)

    def unsubscribe(self):
        """
        Unsubscribes from indications and notifications from the characteristic and clears out all handlers
        for the characteristic's on_notification event handler. Returns a Waitable that executes when the unsubscription
        finishes.

        The Waitable returns two parameters: (GattcCharacteristic this, SubscriptionWriteCompleteEventArgs event args)

        :return: A Waitable that will fire when the unsubscription finishes
        :rtype: blatann.waitables.EventWaitable
        """
        if not self.subscribable:
            raise InvalidOperationException(
                "Cannot subscribe to Characteristic {}".format(self.uuid))
        value = gatt.SubscriptionState.NOT_SUBSCRIBED
        write_id = self._manager.write(self.cccd_handle,
                                       gatt.SubscriptionState.to_buffer(value))
        self._on_notification_event.clear_handlers()

        return IdBasedEventWaitable(self._on_cccd_write_complete_event,
                                    write_id)

    def read(self):
        """
        Initiates a read of the characteristic and returns a Waitable that executes when the read finishes with
        the data read.

        The Waitable returns two parameters: (GattcCharacteristic this, ReadCompleteEventArgs event args)

        :return: A waitable that will fire when the read finishes
        :rtype: blatann.waitables.EventWaitable
        :raises: InvalidOperationException if characteristic not readable
        """
        if not self.readable:
            raise InvalidOperationException(
                "Characteristic {} is not readable".format(self.uuid))
        read_id = self._manager.read(self.value_handle)
        return IdBasedEventWaitable(self._on_read_complete_event, read_id)

    def write(self, data):
        """
        Initiates a write of the data provided to the characteristic and returns a Waitable that executes
        when the write completes.

        The Waitable returns two parameters: (GattcCharacteristic this, WriteCompleteEventArgs event args)

        :param data: The data to write. Can be a string, bytearray, or anything that can be converted to a bytearray
        :return: A waitable that returns when the write finishes
        :rtype: blatann.waitables.EventWaitable
        :raises: InvalidOperationException if characteristic is not writable
        """
        if not self.writable:
            raise InvalidOperationException(
                "Characteristic {} is not writable".format(self.uuid))
        write_id = self._manager.write(self.value_handle, bytearray(data))
        return IdBasedEventWaitable(self._on_write_complete_event, write_id)

    """
    Event Handlers
    """

    def _read_complete(self, sender, event_args):
        """
        Handler for _ReadWriteManager.on_read_complete.
        Dispatches the on_read_complete event and updates the internal value if read was successful

        :param sender: The reader that the read completed on
        :type sender: _ReadWriteManager
        :param event_args: The event arguments
        :type event_args: _ReadTask
        """
        if event_args.handle == self.value_handle:
            if event_args.status == nrf_types.BLEGattStatusCode.success:
                self._value = event_args.data
            args = ReadCompleteEventArgs(event_args.id, self._value,
                                         event_args.status, event_args.reason)
            self._on_read_complete_event.notify(self, args)

    def _write_complete(self, sender, event_args):
        """
        Handler for _ReadWriteManager.on_write_complete. Dispatches on_write_complete or on_cccd_write_complete
        depending on the handle the write finished on.

        :param sender: The writer that the write completed on
        :type sender: _ReadWriteManager
        :param event_args: The event arguments
        :type event_args: _WriteTask
        """
        # Success, update the local value
        if event_args.handle == self.value_handle:
            if event_args.status == nrf_types.BLEGattStatusCode.success:
                self._value = event_args.data
            args = WriteCompleteEventArgs(event_args.id, self._value,
                                          event_args.status, event_args.reason)
            self._on_write_complete_event.notify(self, args)
        elif event_args.handle == self.cccd_handle:
            if event_args.status == nrf_types.BLEGattStatusCode.success:
                self.cccd_state = gatt.SubscriptionState.from_buffer(
                    bytearray(event_args.data))
            args = SubscriptionWriteCompleteEventArgs(event_args.id,
                                                      self.cccd_state,
                                                      event_args.status,
                                                      event_args.reason)
            self._on_cccd_write_complete_event.notify(self, args)

    def _on_indication_notification(self, driver, event):
        """
        Handler for GattcEvtHvx. Dispatches the on_notification_event to listeners

        :type event: nrf_events.GattcEvtHvx
        """
        if event.conn_handle != self.peer.conn_handle or event.attr_handle != self.value_handle:
            return

        is_indication = False
        if event.hvx_type == nrf_events.BLEGattHVXType.indication:
            is_indication = True
            self.ble_device.ble_driver.ble_gattc_hv_confirm(
                event.conn_handle, event.attr_handle)
        self._value = bytearray(event.data)
        self._on_notification_event.notify(
            self, NotificationReceivedEventArgs(self._value, is_indication))

    """
    Factory methods
    """

    @classmethod
    def from_discovered_characteristic(cls, ble_device, peer,
                                       read_write_manager, nrf_characteristic):
        """
        Internal factory method used to create a new characteristic from a discovered nRF Characteristic

        :type nrf_characteristic: nrf_types.BLEGattCharacteristic
        """
        char_uuid = ble_device.uuid_manager.nrf_uuid_to_uuid(
            nrf_characteristic.uuid)
        properties = gatt.CharacteristicProperties.from_nrf_properties(
            nrf_characteristic.char_props)
        cccd_handle_list = [
            d.handle for d in nrf_characteristic.descs
            if d.uuid == nrf_types.BLEUUID.Standard.cccd
        ]
        cccd_handle = cccd_handle_list[0] if cccd_handle_list else None
        return GattcCharacteristic(ble_device, peer, read_write_manager,
                                   char_uuid, properties,
                                   nrf_characteristic.handle_decl,
                                   nrf_characteristic.handle_value,
                                   cccd_handle)
Beispiel #10
0
class GattsAttribute(Attribute):
    """
    Represents the server-side interface of a single attribute which lives inside a Characteristic.
    """
    _QueuedChunk = namedtuple("QueuedChunk", ["offset", "data"])

    def __init__(self,
                 ble_device: BleDevice,
                 peer: Peer,
                 parent: GattsCharacteristic,
                 uuid: Uuid,
                 handle: int,
                 properties: GattsAttributeProperties,
                 initial_value=b"",
                 string_encoding="utf8"):
        super(GattsAttribute, self).__init__(uuid, handle, initial_value,
                                             string_encoding)
        self._ble_device = ble_device
        self._peer = peer
        self._parent = parent
        self._properties = properties
        # Events
        self._on_write = EventSource("Write Event", logger)
        self._on_read = EventSource("Read Event", logger)
        # Subscribed events
        if properties.write:
            self._ble_device.ble_driver.event_subscribe(
                self._on_gatts_write, nrf_events.GattsEvtWrite)
        if properties.read_auth or properties.write_auth:
            self._ble_device.ble_driver.event_subscribe(
                self._on_rw_auth_request,
                nrf_events.GattsEvtReadWriteAuthorizeRequest)
        # Internal state tracking stuff
        self._write_queued = False
        self._read_in_process = False
        self._queued_write_chunks = []

    @property
    def parent(self) -> GattsCharacteristic:
        """
        **Read Only**

        Gets the parent characteristic which owns this attribute
        """
        return self._parent

    @property
    def max_length(self) -> int:
        """
        **Read Only**

        The max possible length data the attribute can be set to
        """
        return self._properties.max_len

    @property
    def read_in_process(self) -> bool:
        """
        **Read Only**

        Gets whether or not the client is in the process of reading out this attribute
        """
        return self._read_in_process

    """
    Public Methods
    """

    def set_value(self, value):
        """
        Sets the value of the attribute.

        :param value: The value to set to. Must be an iterable type such as a str, bytes, or list of uint8 values, or a BleDataStream object.
                      Length must be less than the attribute's max length.
                      If a str is given, it will be encoded using the string_encoding property.
        :raises: InvalidOperationException if value length is too long
        """
        if isinstance(value, BleDataStream):
            value = value.value
        if isinstance(value, str):
            value = value.encode(self.string_encoding)
        if len(value) > self.max_length:
            raise InvalidOperationException(
                "Attempted to set value of {} with length greater than max "
                "(got {}, max {})".format(self.uuid, len(value),
                                          self.max_length))

        v = nrf_types.BLEGattsValue(value)
        self._ble_device.ble_driver.ble_gatts_value_set(
            self._peer.conn_handle, self._handle, v)
        self._value = value

    def get_value(self) -> bytes:
        """
        Fetches the attribute's value from hardware and updates the local copy.
        This isn't often necessary and should instead use the value property to avoid unnecessary reads from the hardware.
        """
        v = nrf_types.BLEGattsValue(b"")
        self._ble_device.ble_driver.ble_gatts_value_get(
            self._peer.conn_handle, self._handle, v)
        self._value = bytes(bytearray(v.value))
        return self._value

    """
    Events
    """

    @property
    def on_write(self) -> Event[GattsAttribute, WriteEventArgs]:
        """
        Event generated whenever a client writes to this attribute.

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_write

    @property
    def on_read(self) -> Event[GattsAttribute, None]:
        """
        Event generated whenever a client requests to read from this attribute. At this point, the application
        may choose to update the value of the attribute to a new value using set_value.

        .. note:: This will only be triggered if the attribute was configured with the read_auth property

        A good example of using this is a "system time" characteristic which reports the application's current system time in seconds.
        Instead of updating this characteristic every second, it can be "lazily" updated only when read.

        NOTE: if there are multiple handlers subscribed to this and each set the value differently, it may cause
        undefined behavior.

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_read

    """
    Event Handlers
    """

    def _on_gatts_write(self, driver, event):
        """
        :type event: nrf_events.GattsEvtWrite
        """
        if event.attribute_handle != self._handle:
            return
        self._value = bytes(bytearray(event.data))
        self._on_write.notify(self, WriteEventArgs(self._value))

    def _on_write_auth_request(self, write_event):
        """
        :type write_event: nrf_events.GattsEvtWrite
        """
        if write_event.write_op in [
                nrf_events.BLEGattsWriteOperation.exec_write_req_cancel,
                nrf_events.BLEGattsWriteOperation.exec_write_req_now
        ]:
            self._execute_queued_write(write_event.write_op)
            # Reply should already be handled in database since this can span multiple attributes and services
            return

        if write_event.attribute_handle != self._handle:
            # Handle is not for this attribute, do nothing
            return

        # Build out the reply
        params = nrf_types.BLEGattsAuthorizeParams(
            nrf_types.BLEGattStatusCode.success, True, write_event.offset,
            write_event.data)
        reply = nrf_types.BLEGattsRwAuthorizeReplyParams(write=params)

        # Check that the write length is valid
        if write_event.offset + len(
                write_event.data) > self._properties.max_len:
            params.gatt_status = nrf_types.BLEGattStatusCode.invalid_att_val_length
            self._ble_device.ble_driver.ble_gatts_rw_authorize_reply(
                write_event.conn_handle, reply)
        else:
            # Send reply before processing write, in case user sets data in gatts_write handler
            try:
                self._ble_device.ble_driver.ble_gatts_rw_authorize_reply(
                    write_event.conn_handle, reply)
            except Exception as e:
                pass
            if write_event.write_op == nrf_events.BLEGattsWriteOperation.prep_write_req:
                self._write_queued = True
                self._queued_write_chunks.append(
                    self._QueuedChunk(write_event.offset, write_event.data))
            elif write_event.write_op in [
                    nrf_events.BLEGattsWriteOperation.write_req,
                    nrf_types.BLEGattsWriteOperation.write_cmd
            ]:
                self._on_gatts_write(None, write_event)

        # TODO More logic

    def _on_read_auth_request(self, read_event):
        """
        :type read_event: nrf_events.GattsEvtRead
        """
        if read_event.attribute_handle != self._handle:
            # Don't care about handles outside of this attribute
            return

        params = nrf_types.BLEGattsAuthorizeParams(
            nrf_types.BLEGattStatusCode.success, False, read_event.offset)
        reply = nrf_types.BLEGattsRwAuthorizeReplyParams(read=params)
        if read_event.offset > len(self._value):
            params.gatt_status = nrf_types.BLEGattStatusCode.invalid_offset
        else:
            self._read_in_process = True
            # If the client is reading from the beginning, notify handlers in case an update needs to be made
            if read_event.offset == 0:
                self._on_read.notify(self)
            self._read_in_process = False

        self._ble_device.ble_driver.ble_gatts_rw_authorize_reply(
            read_event.conn_handle, reply)

    def _on_rw_auth_request(self, driver, event):
        if not self._peer:
            logger.warning("Got RW request when peer not connected: {}".format(
                event.conn_handle))
            return
        if event.read:
            self._on_read_auth_request(event.read)
        elif event.write:
            self._on_write_auth_request(event.write)
        else:
            logging.error("auth request was not read or write???")

    def _execute_queued_write(self, write_op):
        if not self._write_queued:
            return

        self._write_queued = False
        if write_op == nrf_events.BLEGattsWriteOperation.exec_write_req_cancel:
            logger.info("Cancelling write request, char: {}".format(
                self._uuid))
        else:
            logger.info("Executing write request, char: {}".format(self._uuid))
            # TODO Assume that it was assembled properly. Error handling should go here
            new_value = bytearray()
            for chunk in self._queued_write_chunks:
                new_value += bytearray(chunk.data)
            logger.debug("New value: 0x{}".format(binascii.hexlify(new_value)))
            self._ble_device.ble_driver.ble_gatts_value_set(
                self._peer.conn_handle, self._handle,
                nrf_types.BLEGattsValue(new_value))
            self._value = bytes(new_value)
            self._on_write.notify(self, WriteEventArgs(self._value))
        self._queued_write_chunks = []
Beispiel #11
0
class GattcWriter(object):
    """
    Class which implements the state machine for writing a value to a peripheral's attribute
    """
    _WRITE_OVERHEAD = 3  # Number of bytes per MTU that are overhead for the write operation
    _LONG_WRITE_OVERHEAD = 5  # Number of bytes per MTU that are overhead for the long write operations

    def __init__(self, ble_device, peer):
        """
        :type ble_device: blatann.device.BleDevice
        :type peer: blatann.peer.Peer
        """
        self.ble_device = ble_device
        self.peer = peer
        self._on_write_complete = EventSource("On Write Complete", logger)
        self._busy = False
        self._data = ""
        self._handle = 0x0000
        self._offset = 0
        self.peer.driver_event_subscribe(self._on_write_response,
                                         nrf_events.GattcEvtWriteResponse)
        self._len_bytes_written = 0

    @property
    def on_write_complete(self):
        """
        Event that is emitted when a write completes on an attribute handler

        Handler args: (int attribute_handle, gatt.GattStatusCode, bytearray data_written)

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_write_complete

    def write(self, handle, data):
        """
        Writes data to the attribute at the handle provided. Can only write to a single attribute at a time.
        If a write is in progress, raises an InvalidStateException

        :param handle: The attribute handle to write
        :param data: The data to write
        :return: A Waitable that will fire when the write finishes. see on_write_complete for the values returned from the waitable
        :rtype: EventWaitable
        """
        if self._busy:
            raise InvalidStateException("Gattc Writer is busy")
        if len(data) == 0:
            raise ValueError("Data must be at least one byte")
        self._offset = 0
        self._handle = handle
        self._data = data
        logger.debug("Starting write to handle {}, len: {}".format(
            self._handle, len(self._data)))
        self._write_next_chunk()
        self._busy = True
        return EventWaitable(self.on_write_complete)

    def _write_next_chunk(self):
        flags = nrf_types.BLEGattExecWriteFlag.unused
        if self._offset != 0 or len(
                self._data) > (self.peer.mtu_size - self._WRITE_OVERHEAD):
            write_operation = nrf_types.BLEGattWriteOperation.prepare_write_req
            self._len_bytes_written = self.peer.mtu_size - self._LONG_WRITE_OVERHEAD
            self._len_bytes_written = min(self._len_bytes_written,
                                          len(self._data) - self._offset)
            if self._len_bytes_written <= 0:
                write_operation = nrf_types.BLEGattWriteOperation.execute_write_req
                flags = nrf_types.BLEGattExecWriteFlag.prepared_write
        else:
            # Can write it all in a single
            write_operation = nrf_types.BLEGattWriteOperation.write_req
            self._len_bytes_written = len(self._data)

        data_to_write = self._data[self._offset:self._offset +
                                   self._len_bytes_written]
        write_params = nrf_types.BLEGattcWriteParams(write_operation, flags,
                                                     self._handle,
                                                     data_to_write,
                                                     self._offset)
        logger.debug(
            "Writing chunk: handle: {}, offset: {}, len: {}, op: {}".format(
                self._handle, self._offset, len(data_to_write),
                write_operation))
        self.ble_device.ble_driver.ble_gattc_write(self.peer.conn_handle,
                                                   write_params)

    def _on_write_response(self, driver, event):
        """
        Handler for GattcEvtWriteResponse

        :type event: nrf_events.GattcEvtWriteResponse
        """
        if event.conn_handle != self.peer.conn_handle:
            return
        if event.attr_handle != self._handle and event.write_op != nrf_types.BLEGattWriteOperation.execute_write_req:
            return
        if event.status != nrf_events.BLEGattStatusCode.success:
            self._complete(event.status)
            return

        # Write successful, update offset and check operation
        self._offset += self._len_bytes_written

        if event.write_op in [
                nrf_types.BLEGattWriteOperation.write_req,
                nrf_types.BLEGattWriteOperation.execute_write_req
        ]:
            # Completed successfully
            self._complete()
        elif event.write_op == nrf_types.BLEGattWriteOperation.prepare_write_req:
            # Write next chunk (or execute if complete)
            self._write_next_chunk()
        else:
            logger.error("Got unknown write operation: {}".format(event))
            self._complete(nrf_types.BLEGattStatusCode.unknown)

    def _complete(self, status=nrf_events.BLEGattStatusCode.success):
        self._busy = False
        self._on_write_complete.notify(
            self, GattcWriteCompleteEventArgs(self._handle, status,
                                              self._data))
Beispiel #12
0
class GattcCharacteristic(gatt.Characteristic):
    """
    Represents a characteristic that lives within a service in the server's GATT database.

    This class is normally not instantiated directly and instead created when the database is discovered
    via :meth:`Peer.discover_services() <blatann.peer.Peer.discover_services>`
    """
    def __init__(self,
                 ble_device,
                 peer,
                 uuid: Uuid,
                 properties: gatt.CharacteristicProperties,
                 decl_attr: GattcAttribute,
                 value_attr: GattcAttribute,
                 cccd_attr: GattcAttribute = None,
                 attributes: List[GattcAttribute] = None):
        super(GattcCharacteristic, self).__init__(ble_device, peer, uuid,
                                                  properties)
        self._decl_attr = decl_attr
        self._value_attr = value_attr
        self._cccd_attr = cccd_attr
        self._on_notification_event = EventSource("On Notification", logger)
        self._attributes = tuple(sorted(attributes,
                                        key=lambda d: d.handle)) or ()
        self.peer = peer

        self._on_read_complete_event = EventSource("On Read Complete", logger)
        self._on_write_complete_event = EventSource("Write Complete", logger)
        self._on_cccd_write_complete_event = EventSource(
            "CCCD Write Complete", logger)

        self._value_attr.on_read_complete.register(self._read_complete)
        self._value_attr.on_write_complete.register(self._write_complete)
        if self._cccd_attr:
            self._cccd_attr.on_write_complete.register(
                self._cccd_write_complete)

        self.peer.driver_event_subscribe(self._on_indication_notification,
                                         nrf_events.GattcEvtHvx)

    """
    Properties
    """

    @property
    def declaration_attribute(self) -> GattcAttribute:
        """
        **Read Only**

        Gets the declaration attribute of the characteristic
        """
        return self._decl_attr

    @property
    def value_attribute(self) -> GattcAttribute:
        """
        **Read Only**

        Gets the value attribute of the characteristic
        """
        return self._value_attr

    @property
    def value(self) -> bytes:
        """
        **Read Only**

        The current value of the characteristic. This is updated through read, write, and notify operations
        """
        return self._value_attr.value

    @property
    def readable(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic can be read from
        """
        return self._properties.read

    @property
    def writable(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic can be written to
        """
        return self._properties.write

    @property
    def writable_without_response(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic accepts write commands that don't require a confirmation response
        """
        return self._properties.write_no_response

    @property
    def subscribable(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic can be subscribed to
        """
        return self._properties.notify or self._properties.indicate

    @property
    def subscribable_indications(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic can be subscribed to using indications
        """
        return self._properties.indicate

    @property
    def subscribable_notifications(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic can be subscribed to using notifications
        """
        return self._properties.notify

    @property
    def subscribed(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic is currently subscribed to
        """
        return self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED

    @property
    def attributes(self) -> Iterable[GattcAttribute]:
        """
        **Read Only**

        Returns the list of all attributes/descriptors that reside in the characteristic.
        This includes the declaration attribute, value attribute, and descriptors (CCCD, Name, etc.)
        """
        return self._attributes

    @property
    def string_encoding(self) -> str:
        """
        The default method for encoding strings into bytes when a string is provided as a value

        :getter: Gets the current string encoding for the characteristic
        :setter: Sets the string encoding for the characteristic
        """
        return self._value_attr.string_encoding

    @string_encoding.setter
    def string_encoding(self, encoding):
        self._value_attr.string_encoding = encoding

    """
    Events
    """

    @property
    def on_read_complete(
            self) -> Event[GattcCharacteristic, ReadCompleteEventArgs]:
        """
        Event that is raised when a read operation from the characteristic is completed
        """
        return self._on_read_complete_event

    @property
    def on_write_complete(
            self) -> Event[GattcCharacteristic, WriteCompleteEventArgs]:
        """
        Event that is raised when a write operation to the characteristic is completed
        """
        return self._on_write_complete_event

    @property
    def on_notification_received(
            self) -> Event[GattcCharacteristic, NotificationReceivedEventArgs]:
        """
        Event that is raised when an indication or notification is received on the characteristic
        """
        return self._on_notification_event

    """
    Public Methods
    """

    def subscribe(
        self,
        on_notification_handler: Callable[
            [GattcCharacteristic, NotificationReceivedEventArgs], None],
        prefer_indications=False
    ) -> EventWaitable[GattcCharacteristic,
                       SubscriptionWriteCompleteEventArgs]:
        """
        Subscribes to the characteristic's indications or notifications, depending on what's available and the
        prefer_indications setting. Returns a Waitable that triggers when the subscription on the peripheral finishes.

        :param on_notification_handler: The handler to be called when an indication or notification is received from
            the peripheral. Must take two parameters: (GattcCharacteristic this, NotificationReceivedEventArgs event args)
        :param prefer_indications: If the peripheral supports both indications and notifications,
            will subscribe to indications instead of notifications
        :return: A Waitable that will trigger when the subscription finishes
        :raises: InvalidOperationException if the characteristic cannot be subscribed to
            (characteristic does not support indications or notifications)
        """
        if not self.subscribable:
            raise InvalidOperationException(
                "Cannot subscribe to Characteristic {}".format(self.uuid))
        if prefer_indications and self._properties.indicate or not self._properties.notify:
            value = gatt.SubscriptionState.INDICATION
        else:
            value = gatt.SubscriptionState.NOTIFY
        self._on_notification_event.register(on_notification_handler)
        waitable = self._cccd_attr.write(
            gatt.SubscriptionState.to_buffer(value))
        return IdBasedEventWaitable(self._on_cccd_write_complete_event,
                                    waitable.id)

    def unsubscribe(
        self
    ) -> EventWaitable[GattcCharacteristic,
                       SubscriptionWriteCompleteEventArgs]:
        """
        Unsubscribes from indications and notifications from the characteristic and clears out all handlers
        for the characteristic's on_notification event handler. Returns a Waitable that triggers when the unsubscription
        finishes.

        :return: A Waitable that will trigger when the unsubscription operation finishes
        :raises: InvalidOperationException if characteristic cannot be subscribed to
            (characteristic does not support indications or notifications)
        """
        if not self.subscribable:
            raise InvalidOperationException(
                "Cannot subscribe to Characteristic {}".format(self.uuid))
        value = gatt.SubscriptionState.NOT_SUBSCRIBED
        waitable = self._cccd_attr.write(
            gatt.SubscriptionState.to_buffer(value))
        self._on_notification_event.clear_handlers()

        return IdBasedEventWaitable(self._on_cccd_write_complete_event,
                                    waitable.id)

    def read(
            self) -> EventWaitable[GattcCharacteristic, ReadCompleteEventArgs]:
        """
        Initiates a read of the characteristic and returns a Waitable that triggers when the read finishes with
        the data read.

        :return: A waitable that will trigger when the read finishes
        :raises: InvalidOperationException if characteristic not readable
        """
        if not self.readable:
            raise InvalidOperationException(
                "Characteristic {} is not readable".format(self.uuid))
        waitable = self._value_attr.read()
        return IdBasedEventWaitable(self._on_read_complete_event, waitable.id)

    def write(
            self, data
    ) -> EventWaitable[GattcCharacteristic, WriteCompleteEventArgs]:
        """
        Performs a write request of the data provided to the characteristic and returns a Waitable that triggers
        when the write completes and the confirmation response is received from the other device.

        :param data: The data to write. Can be a string, bytes, or anything that can be converted to bytes
        :type data: str or bytes or bytearray
        :return: A waitable that returns when the write finishes
        :raises: InvalidOperationException if characteristic is not writable
        """
        if not self.writable:
            raise InvalidOperationException(
                "Characteristic {} is not writable".format(self.uuid))
        if isinstance(data, str):
            data = data.encode(self.string_encoding)
        waitable = self._value_attr.write(bytes(data), True)
        return IdBasedEventWaitable(self._on_write_complete_event, waitable.id)

    def write_without_response(
            self, data
    ) -> EventWaitable[GattcCharacteristic, WriteCompleteEventArgs]:
        """
        Performs a write command, which does not require the peripheral to send a confirmation response packet.
        This is a faster but lossy operation in the case that the packet is dropped/never received by the peer.
        This returns a waitable that triggers when the write is transmitted to the peripheral device.

        .. note:: Data sent without responses must fit within a single MTU minus 3 bytes for the operation overhead.

        :param data: The data to write. Can be a string, bytes, or anything that can be converted to bytes
        :type data: str or bytes or bytearray
        :return: A waitable that returns when the write finishes
        :raises: InvalidOperationException if characteristic is not writable without responses
        """
        if not self.writable_without_response:
            raise InvalidOperationException(
                "Characteristic {} does not accept "
                "writes without responses".format(self.uuid))
        if isinstance(data, str):
            data = data.encode(self.string_encoding)
        waitable = self._value_attr.write(bytes(data), False)
        return IdBasedEventWaitable(self._on_write_complete_event, waitable.id)

    def find_descriptor(self, uuid: Uuid) -> Optional[GattcAttribute]:
        """
        Searches for the descriptor/attribute matching the UUID provided and returns the attribute.
        If not found, returns None.
        If multiple attributes with the same UUID exist in the characteristic, this returns the first attribute found.

        :param uuid: The UUID to search for
        :return: THe descriptor attribute, if found
        """
        for attr in self._attributes:
            if attr.uuid == uuid:
                return attr

    """
    Event Handlers
    """

    def _read_complete(self, sender: GattcAttribute,
                       event_args: ReadCompleteEventArgs):
        """
        Handler for GattcAttribute.on_read_complete.
        Dispatches the on_read_complete event and updates the internal value if read was successful
        """
        self._on_read_complete_event.notify(self, event_args)

    def _write_complete(self, sender: GattcAttribute,
                        event_args: WriteCompleteEventArgs):
        """
        Handler for value_attribute.on_write_complete. Dispatches on_write_complete.
        """
        self._on_write_complete_event.notify(self, event_args)

    def _cccd_write_complete(self, sender: GattcAttribute,
                             event_args: WriteCompleteEventArgs):
        """
        Handler for cccd_attribute.on_write_complete. Dispatches on_cccd_write_complete.
        """
        if event_args.status == nrf_types.BLEGattStatusCode.success:
            self.cccd_state = gatt.SubscriptionState.from_buffer(
                bytearray(event_args.value))
        args = SubscriptionWriteCompleteEventArgs(event_args.id,
                                                  self.cccd_state,
                                                  event_args.status,
                                                  event_args.reason)
        self._on_cccd_write_complete_event.notify(self, args)

    def _on_indication_notification(self, driver, event):
        """
        Handler for GattcEvtHvx. Dispatches the on_notification_event to listeners

        :type event: nrf_events.GattcEvtHvx
        """
        if (event.conn_handle != self.peer.conn_handle
                or event.attr_handle != self._value_attr.handle):
            return

        is_indication = False
        if event.hvx_type == nrf_events.BLEGattHVXType.indication:
            is_indication = True
            self.ble_device.ble_driver.ble_gattc_hv_confirm(
                event.conn_handle, event.attr_handle)

        # Update the value attribute with the data that was provided
        self._value_attr.update(bytearray(event.data))
        self._on_notification_event.notify(
            self, NotificationReceivedEventArgs(self.value, is_indication))

    """
    Factory methods
    """

    @classmethod
    def from_discovered_characteristic(cls, ble_device, peer,
                                       read_write_manager, nrf_characteristic):
        """
        Internal factory method used to create a new characteristic from a discovered nRF Characteristic

        :type ble_device: blatann.BleDevice
        :type peer: blatann.peer.Peer
        :type read_write_manager: GattcOperationManager
        :type nrf_characteristic: nrf_types.BLEGattCharacteristic
        """
        char_uuid = ble_device.uuid_manager.nrf_uuid_to_uuid(
            nrf_characteristic.uuid)
        properties = gatt.CharacteristicProperties.from_nrf_properties(
            nrf_characteristic.char_props)

        # Create the declaration and value attributes to start
        decl_attr = GattcAttribute(DeclarationUuid.characteristic,
                                   nrf_characteristic.handle_decl,
                                   read_write_manager,
                                   nrf_characteristic.data_decl)
        value_attr = GattcAttribute(char_uuid, nrf_characteristic.handle_value,
                                    read_write_manager,
                                    nrf_characteristic.data_value)
        cccd_attr = None

        attributes = [decl_attr, value_attr]

        for nrf_desc in nrf_characteristic.descs:
            # Already added the handle and value attributes, skip them here
            if nrf_desc.handle in [
                    nrf_characteristic.handle_decl,
                    nrf_characteristic.handle_value
            ]:
                continue

            attr_uuid = ble_device.uuid_manager.nrf_uuid_to_uuid(nrf_desc.uuid)
            attr = GattcAttribute(attr_uuid, nrf_desc.handle,
                                  read_write_manager)

            if attr_uuid == DescriptorUuid.cccd:
                cccd_attr = attr
            attributes.append(attr)

        return GattcCharacteristic(ble_device, peer, char_uuid, properties,
                                   decl_attr, value_attr, cccd_attr,
                                   attributes)
Beispiel #13
0
class SecurityManager(object):
    """
    Handles performing security procedures with a connected peer
    """
    def __init__(self, ble_device, peer, security_parameters):
        """
        :type ble_device: blatann.BleDevice
        :type peer: blatann.peer.Peer
        :type security_parameters: SecurityParameters
        """
        self.ble_device = ble_device
        self.peer = peer
        self.security_params = security_parameters
        self._busy = False
        self._on_authentication_complete_event = EventSource(
            "On Authentication Complete", logger)
        self._on_passkey_display_event = EventSource("On Passkey Display",
                                                     logger)
        self._on_passkey_entry_event = EventSource("On Passkey Entry", logger)
        self.peer.on_connect.register(self._on_peer_connected)
        self._auth_key_resolve_thread = threading.Thread()

    """
    Events
    """

    @property
    def on_pairing_complete(self):
        """
        Event that is triggered when pairing completes with the peer
        EventArgs type: PairingCompleteEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_authentication_complete_event

    @property
    def on_passkey_display_required(self):
        """
        Event that is triggered when a passkey needs to be displayed to the user
        EventArgs type: PasskeyDisplayEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_passkey_display_event

    @property
    def on_passkey_required(self):
        """
        Event that is triggered when a passkey needs to be entered by the user
        EventArgs type: PasskeyEntryEventArgs

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: blatann.event_type.Event
        """
        return self._on_passkey_entry_event

    """
    Public Methods
    """

    def set_security_params(self,
                            passcode_pairing,
                            io_capabilities,
                            bond,
                            out_of_band,
                            reject_pairing_requests=False):
        """
        Sets the security parameters to use with the peer

        :param passcode_pairing: Flag indicating that passcode pairing is required
        :type passcode_pairing: bool
        :param io_capabilities: The input/output capabilities of this device
        :type io_capabilities: IoCapabilities
        :param bond: Flag indicating that long-term bonding should be performed
        :type bond: bool
        :param out_of_band: Flag indicating if out-of-band pairing is supported
        :type out_of_band: bool
        :param reject_pairing_requests: Flag indicating that all security requests by the peer should be rejected
        :type reject_pairing_requests: bool
        """
        self.security_params = SecurityParameters(passcode_pairing,
                                                  io_capabilities, bond,
                                                  out_of_band,
                                                  reject_pairing_requests)

    def pair(self):
        """
        Starts the pairing process with the peer given the set security parameters
        and returns a Waitable which will fire when the pairing process completes, whether successful or not.
        Waitable returns two parameters: (Peer, PairingCompleteEventArgs)

        :return: A waitiable that will fire when pairing is complete
        :rtype: blatann.waitables.EventWaitable
        """
        if self._busy:
            raise InvalidStateException("Security manager busy")
        if self.security_params.reject_pairing_requests:
            raise InvalidOperationException(
                "Cannot initiate pairing while rejecting pairing requests")

        sec_params = self._get_security_params()
        self.ble_device.ble_driver.ble_gap_authenticate(
            self.peer.conn_handle, sec_params)
        self._busy = True
        return EventWaitable(self.on_pairing_complete)

    """
    Private Methods
    """

    def _on_peer_connected(self, peer, event_args):
        self._busy = False
        self.peer.driver_event_subscribe(self._on_security_params_request,
                                         nrf_events.GapEvtSecParamsRequest)
        self.peer.driver_event_subscribe(self._on_authentication_status,
                                         nrf_events.GapEvtAuthStatus)
        self.peer.driver_event_subscribe(self._on_auth_key_request,
                                         nrf_events.GapEvtAuthKeyRequest)
        self.peer.driver_event_subscribe(self._on_passkey_display,
                                         nrf_events.GapEvtPasskeyDisplay)

    def _get_security_params(self):
        keyset_own = nrf_types.BLEGapSecKeyDist()
        keyset_peer = nrf_types.BLEGapSecKeyDist()
        sec_params = nrf_types.BLEGapSecParams(
            self.security_params.bond, self.security_params.passcode_pairing,
            False, False, self.security_params.io_capabilities,
            self.security_params.out_of_band, 7, 16, keyset_own, keyset_peer)
        return sec_params

    def _on_security_params_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtSecParamsRequest
        """
        # Security parameters are only provided for clients
        sec_params = self._get_security_params(
        ) if self.peer.is_client else None
        keyset = nrf_types.BLEGapSecKeyset()

        if self.security_params.reject_pairing_requests:
            status = nrf_types.BLEGapSecStatus.pairing_not_supp
        else:
            status = nrf_types.BLEGapSecStatus.success

        self.ble_device.ble_driver.ble_gap_sec_params_reply(
            event.conn_handle, status, sec_params, keyset)

        if not self.security_params.reject_pairing_requests:
            self._busy = True

    def _on_security_request(self, driver, event):
        # TODO: Event not implemented
        pass

    def _on_authentication_status(self, driver, event):
        """
        :type event: nrf_events.GapEvtAuthStatus
        """
        self._busy = False
        self._on_authentication_complete_event.notify(
            self.peer, PairingCompleteEventArgs(event.auth_status))

    def _on_passkey_display(self, driver, event):
        """
        :type event: nrf_events.GapEvtPasskeyDisplay
        """
        # TODO: Better way to handle match request
        self._on_passkey_display_event.notify(
            self.peer,
            PasskeyDisplayEventArgs(event.passkey, event.match_request))

    def _on_auth_key_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtAuthKeyRequest
        """
        def resolve(passkey):
            if not self._busy:
                return
            if isinstance(passkey, (long, int)):
                passkey = "{:06d}".format(passkey)
            elif isinstance(passkey, unicode):
                passkey = str(passkey)
            self.ble_device.ble_driver.ble_gap_auth_key_reply(
                self.peer.conn_handle, event.key_type, passkey)

        self._auth_key_resolve_thread = threading.Thread(
            name="{} Passkey Entry".format(self.peer.conn_handle),
            target=self._on_passkey_entry_event.notify,
            args=(self.peer, PasskeyEntryEventArgs(event.key_type, resolve)))
        self._auth_key_resolve_thread.start()

    def _on_timeout(self, driver, event):
        """
        :type event: nrf_events.GapEvtTimeout
        """
        if event.src != nrf_types.BLEGapTimeoutSrc.security_req:
            return
        self._on_authentication_complete_event.notify(
            self.peer, PairingCompleteEventArgs(SecurityStatus.timeout))
Beispiel #14
0
class BatteryClient(object):
    def __init__(self, gattc_service):
        """
        :type gattc_service: blatann.gatt.gattc.GattcService
        """
        self._service = gattc_service
        self._batt_characteristic = gattc_service.find_characteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
        self._on_battery_level_updated_event = EventSource("Battery Level Update Event")

    def read(self) -> EventWaitable[BatteryClient, DecodedReadCompleteEventArgs[int]]:
        """
        Reads the Battery level characteristic.

        :return: A waitable for when the read completes, which waits for the on_battery_level_update_event to be
                 emitted
        """
        self._batt_characteristic.read().then(self._on_read_complete)
        return EventWaitable(self._on_battery_level_updated_event)

    @property
    def on_battery_level_updated(self) -> Event[BatteryClient, DecodedReadCompleteEventArgs[int]]:
        """
        Event that is generated whenever the battery level on the peripheral is updated, whether
        it is by notification or from reading the characteristic itself.

        The DecodedReadCompleteEventArgs value given is the integer battery percent received. If the read failed
        or failed to decode, the value will be equal to the raw bytes received.
        """
        return self._on_battery_level_updated_event

    @property
    def can_enable_notifications(self) -> bool:
        """
        Checks if the battery level characteristic allows notifications to be subscribed to

        :return: True if notifications can be enabled, False if not
        """
        return self._batt_characteristic.subscribable

    def enable_notifications(self):
        """
        Enables notifications for the battery level characteristic. Note: this function will raise an exception
        if notifications aren't possible

        :return: a Waitable which waits for the write to finish
        """
        return self._batt_characteristic.subscribe(self._on_battery_level_notification)

    def disable_notifications(self):
        """
        Disables notifications for the battery level characteristic. Note: this function will raise an exception
        if notifications aren't possible

        :return: a Waitable which waits for the write to finish
        """
        return self._batt_characteristic.unsubscribe()

    def _on_battery_level_notification(self, characteristic, event_args):
        """
        :param characteristic:
        :type event_args: blatann.event_args.NotificationReceivedEventArgs
        :return:
        """
        decoded_value = None
        try:
            stream = ble_data_types.BleDataStream(event_args.value)
            decoded_value = BatteryLevel.decode(stream)
        except Exception as e:  # TODO not so generic
            logger.error("Failed to decode Battery Level, stream: [{}]".format(binascii.hexlify(event_args.value)))
            logger.exception(e)

        decoded_event_args = DecodedReadCompleteEventArgs.from_notification_complete_event_args(event_args, decoded_value)
        self._on_battery_level_updated_event.notify(self, decoded_event_args)

    def _on_read_complete(self, characteristic, event_args):
        """
        :param characteristic:
        :type event_args: blatann.event_args.ReadCompleteEventArgs
        """
        decoded_value = None
        if event_args.status == GattStatusCode.success:
            try:
                stream = ble_data_types.BleDataStream(event_args.value)
                decoded_value = BatteryLevel.decode(stream)
            except Exception as e:  # TODO not so generic
                logger.error("Failed to decode Battery Level, stream: [{}]".format(binascii.hexlify(event_args.value)))
                logger.exception(e)

        decoded_event_args = DecodedReadCompleteEventArgs.from_read_complete_event_args(event_args, decoded_value)
        self._on_battery_level_updated_event.notify(self, decoded_event_args)

    @classmethod
    def find_in_database(cls, gattc_database):
        """
        :type gattc_database: blatann.gatt.gattc.GattcDatabase
        :rtype: BatteryClient
        """
        service = gattc_database.find_service(BATTERY_SERVICE_UUID)
        if service:
            return BatteryClient(service)
Beispiel #15
0
class GattcAttribute(Attribute):
    """
    Represents a client-side interface to a single attribute which lives inside a Characteristic
    """
    def __init__(self, uuid: Uuid, handle: int, read_write_manager: GattcOperationManager,
                 initial_value=b"", string_encoding="utf8"):
        super(GattcAttribute, self).__init__(uuid, handle, initial_value, string_encoding)
        self._manager = read_write_manager

        self._on_read_complete_event = EventSource(f"[{handle}/{uuid}] On Read Complete", logger)
        self._on_write_complete_event = EventSource(f"[{handle}/{uuid}] On Write Complete", logger)

    """
    Events
    """

    @property
    def on_read_complete(self) -> Event[GattcAttribute, ReadCompleteEventArgs]:
        """
        Event that is triggered when a read from the attribute is completed
        """
        return self._on_read_complete_event

    @property
    def on_write_complete(self) -> Event[GattcAttribute, WriteCompleteEventArgs]:
        """
        Event that is triggered when a write to the attribute is completed
        """
        return self._on_write_complete_event

    """
    Public Methods
    """

    def read(self) -> IdBasedEventWaitable[GattcAttribute, ReadCompleteEventArgs]:
        """
        Performs a read of the attribute and returns a Waitable that executes when the read finishes
        with the data read.

        :return: A waitable that will trigger when the read finishes
        """
        read_id = self._manager.read(self._handle, self._read_complete)
        return IdBasedEventWaitable(self._on_read_complete_event, read_id)

    def write(self, data, with_response=True) -> IdBasedEventWaitable[GattcAttribute, WriteCompleteEventArgs]:
        """
        Initiates a write of the data provided to the attribute and returns a Waitable that executes
        when the write completes and the confirmation response is received from the other device.

        :param data: The data to write. Can be a string, bytes, or anything that can be converted to bytes
        :type data: str or bytes or bytearray
        :param with_response: Used internally for characteristics that support write without responses.
                              Should always be true for any other case (descriptors, etc.).
        :return: A waitable that returns when the write finishes
        """
        if isinstance(data, str):
            data = data.encode(self._string_encoding)
        write_id = self._manager.write(self._handle, bytes(data), self._write_complete, with_response)
        return IdBasedEventWaitable(self._on_write_complete_event, write_id)

    def update(self, value):
        """
        Used internally to update the value after data is received from another means, i.e. Indication/notification.
        Should not be called by the user.
        """
        self._value = bytes(value)

    def _read_complete(self, sender, event_args):
        if event_args.handle == self._handle:
            if event_args.status == nrf_types.BLEGattStatusCode.success:
                self._value = event_args.data
            args = ReadCompleteEventArgs(event_args.id, self._value, event_args.status, event_args.reason)
            self._on_read_complete_event.notify(self, args)

    def _write_complete(self, sender, event_args):
        # Success, update the local value
        if event_args.handle == self._handle:
            if event_args.status == nrf_types.BLEGattStatusCode.success:
                self._value = event_args.data
            args = WriteCompleteEventArgs(event_args.id, self._value, event_args.status, event_args.reason)
            self._on_write_complete_event.notify(self, args)
Beispiel #16
0
class _ReadWriteManager(QueuedTasksManagerBase):
    def __init__(self, reader, writer):
        """
        :type reader: GattcReader
        :type writer: GattcWriter
        """
        super(_ReadWriteManager, self).__init__()
        self._reader = reader
        self._writer = writer
        self._reader.peer.on_disconnect.register(self._on_disconnect)
        self._cur_read_task = None
        self._cur_write_task = None
        self.on_read_complete = EventSource("Gattc Read Complete", logger)
        self.on_write_complete = EventSource("Gattc Write Complete", logger)
        self._reader.on_read_complete.register(self._read_complete)
        self._writer.on_write_complete.register(self._write_complete)

    def read(self, handle):
        read_task = _ReadTask(handle)
        self._add_task(read_task)
        return read_task.id

    def write(self, handle, value):
        write_task = _WriteTask(handle, value)
        self._add_task(write_task)
        return write_task.id

    def clear_all(self):
        self._clear_all(GattOperationCompleteReason.QUEUE_CLEARED)

    def _handle_task(self, task):
        if isinstance(task, _ReadTask):
            self._reader.read(task.handle)
            self._cur_read_task = task
        elif isinstance(task, _WriteTask):
            self._writer.write(task.handle, task.data)
            self._cur_write_task = task
        else:
            return True

    def _handle_task_failure(self, task, e):
        if isinstance(task, _ReadTask):
            self.on_read_complete.notify(self, task)
        elif isinstance(task, _WriteTask):
            self.on_write_complete.notify(self, task)

    def _handle_task_cleared(self, task, reason):
        if isinstance(task, _ReadTask):
            task.reason = reason
            self.on_read_complete.notify(self, task)
        elif isinstance(task, _WriteTask):
            task.reason = reason
            self.on_write_complete.notify(self, task)

    def _on_disconnect(self, sender, event_args):
        self._clear_all(GattOperationCompleteReason.SERVER_DISCONNECTED)

    def _read_complete(self, sender, event_args):
        """
        Handler for GattcReader.on_read_complete.
        Dispatches the on_read_complete event and updates the internal value if read was successful

        :param sender: The reader that the read completed on
        :type sender: blatann.gatt.reader.GattcReader
        :param event_args: The event arguments
        :type event_args: blatann.gatt.reader.GattcReadCompleteEventArgs
        """
        task = self._cur_read_task
        self._task_completed(self._cur_read_task)

        task.data = event_args.data
        task.status = event_args.status
        self.on_read_complete.notify(sender, task)

    def _write_complete(self, sender, event_args):
        """
        Handler for GattcWriter.on_write_complete. Dispatches on_write_complete or on_cccd_write_complete
        depending on the handle the write finished on.

        :param sender: The writer that the write completed on
        :type sender: blatann.gatt.writer.GattcWriter
        :param event_args: The event arguments
        :type event_args: blatann.gatt.writer.GattcWriteCompleteEventArgs
        """
        task = self._cur_write_task
        self._task_completed(self._cur_write_task)

        task.status = event_args.status
        self.on_write_complete.notify(sender, task)
Beispiel #17
0
class GattsCharacteristic(gatt.Characteristic):
    """
    Represents a single characteristic within a service. This class is usually not instantiated directly; it
    is added to a service through :meth:`GattsService.add_characteristic`
    """
    _QueuedChunk = namedtuple("QueuedChunk", ["offset", "data"])

    def __init__(self,
                 ble_device: BleDevice,
                 peer: Peer,
                 uuid: Uuid,
                 properties: GattsCharacteristicProperties,
                 value_handle: int,
                 cccd_handle: int,
                 sccd_handle: int,
                 user_desc_handle: int,
                 notification_manager: GattsOperationManager,
                 value=b"",
                 prefer_indications=True,
                 string_encoding="utf8"):
        super(GattsCharacteristic, self).__init__(ble_device, peer, uuid,
                                                  properties, string_encoding)
        self._value = value
        self.prefer_indications = prefer_indications
        self._notification_manager = notification_manager

        value_attr_props = GattsAttributeProperties(
            properties.read, properties.write or properties.write_no_response,
            properties.security_level, properties.max_len,
            properties.variable_length, True, True)
        self._value_attr = GattsAttribute(self.ble_device, self.peer, self,
                                          uuid, value_handle, value_attr_props,
                                          value, string_encoding)
        self._attrs: List[GattsAttribute] = [self._value_attr]
        self._presentation_format = properties.presentation

        if cccd_handle != nrf_types.BLE_GATT_HANDLE_INVALID:
            cccd_props = GattsAttributeProperties(True, True,
                                                  gatt.SecurityLevel.OPEN, 2,
                                                  False, False, False)
            self._cccd_attr = GattsAttribute(self.ble_device, self.peer, self,
                                             DescriptorUuid.cccd, cccd_handle,
                                             cccd_props, b"\x00\x00")
            self._attrs.append(self._cccd_attr)
        else:
            self._cccd_attr = None
        if user_desc_handle != nrf_types.BLE_GATT_HANDLE_INVALID:
            self._user_desc_attr = GattsAttribute(
                self.ble_device, self.peer, self,
                DescriptorUuid.user_description, user_desc_handle,
                properties.user_description, properties.user_description.value,
                string_encoding)
            self._attrs.append(self._user_desc_attr)
        else:
            self._user_desc_attr = None
        if sccd_handle != nrf_types.BLE_GATT_HANDLE_INVALID:
            sccd_props = GattsAttributeProperties(True, True,
                                                  gatt.SecurityLevel.OPEN, 2,
                                                  False, False, False)
            self._sccd_attr = GattsAttribute(self.ble_device, self.peer, self,
                                             DescriptorUuid.sccd, sccd_handle,
                                             sccd_props, b"\x00\x00")
            self._attrs.append(self._sccd_attr)

        # Events
        self._on_write = EventSource("Write Event", logger)
        self._on_read = EventSource("Read Event", logger)
        self._on_sub_change = EventSource("Subscription Change Event", logger)
        self._on_notify_complete = EventSource("Notification Complete Event",
                                               logger)
        # Subscribed events
        self.peer.on_disconnect.register(self._on_disconnect)
        self._value_attr.on_read.register(self._on_value_read)
        self._value_attr.on_write.register(self._on_value_write)
        if self._cccd_attr:
            self._cccd_attr.on_write.register(self._on_cccd_write)

    """
    Public Methods
    """

    def set_value(
        self,
        value,
        notify_client=False
    ) -> Optional[IdBasedEventWaitable[GattsCharacteristic,
                                       NotificationCompleteEventArgs]]:
        """
        Sets the value of the characteristic.

        :param value: The value to set to. Must be an iterable type such as a str, bytes, or list of uint8 values,
                      or a BleDataStream object.
                      Length must be less than or equal to the characteristic's max length.
                      If a string is given, it will be encoded using the string_encoding property of the characteristic.
        :param notify_client: Flag whether or not to notify the client. If indications and notifications are not set up
                              for the characteristic, will raise an InvalidOperationException
        :raises: InvalidOperationException if value length is too long, or notify client set and characteristic
                 is not notifiable
        :raises: InvalidStateException if the client is not currently subscribed to the characteristic
        :return: If notify_client is true, this method will return the waitable for when the notification is sent to the client
        """
        if notify_client and not self.notifiable:
            raise InvalidOperationException(
                "Cannot notify client. "
                "{} not set up for notifications or indications".format(
                    self.uuid))

        self._value_attr.set_value(value)
        if notify_client and self.client_subscribed and not self._value_attr.read_in_process:
            return self.notify(None)

    def notify(
        self, data
    ) -> IdBasedEventWaitable[GattsCharacteristic,
                              NotificationCompleteEventArgs]:
        """
        Notifies the client with the data provided without setting the data into the characteristic value.
        If data is not provided (None), will notify with the currently-set value of the characteristic

        :param data: Optional data to notify the client with. If supplied, must be an iterable type such as a
                     str, bytes, or list of uint8 values, or a BleDataStream object.
                     Length must be less than or equal to the characteristic's max length.
                     If a string is given, it will be encoded using the string_encoding property of the characteristic.
        :raises: InvalidStateException if the client is not subscribed to the characteristic
        :raises: InvalidOperationException if the characteristic is not configured for notifications/indications
        :return: An EventWaitable that will trigger when the notification is successfully sent to the client. The waitable
                 also contains the ID of the sent notification which is used in the on_notify_complete event
        """
        if isinstance(data, BleDataStream):
            value = data.value
        if isinstance(data, str):
            value = data.encode(self.string_encoding)
        if not self.notifiable:
            raise InvalidOperationException(
                "Cannot notify client. "
                "{} not set up for notifications or indications".format(
                    self.uuid))
        if not self.client_subscribed:
            raise InvalidStateException(
                "Client is not subscribed, cannot notify client")

        notification_id = self._notification_manager.notify(
            self, self._value_attr.handle, self._on_notify_complete, data)
        return IdBasedEventWaitable(self._on_notify_complete, notification_id)

    def add_descriptor(self,
                       uuid: Uuid,
                       properties: GattsAttributeProperties,
                       initial_value=b"",
                       string_encoding="utf8") -> GattsAttribute:
        """
        Creates and adds a descriptor to the characteristic

        .. note:: Due to limitations of the BLE stack, the CCCD, SCCD, User Description, Extended Properties,
           and Presentation Format descriptors cannot be added through this method. They must be added through the
           ``GattsCharacteristicProperties`` fields when creating the characteristic.

        :param uuid: The UUID of the descriptor to add, and cannot be the UUIDs of any of the reserved descriptor UUIDs in the note
        :param properties: The properties of the descriptor
        :param initial_value: The initial value to set the descriptor to
        :param string_encoding: The string encoding to use, if a string is set
        :return: the descriptor that was created and added to the characteristic
        """
        if isinstance(initial_value, str):
            initial_value = initial_value.encode(string_encoding)

        self.ble_device.uuid_manager.register_uuid(uuid)
        security = _security_mapping[properties.security_level]
        read_perm = security if properties.read else nrf_types.BLEGapSecModeType.NO_ACCESS
        write_perm = security if properties.write else nrf_types.BLEGapSecModeType.NO_ACCESS
        max_len = max(len(initial_value), properties.max_len)
        metadata = nrf_types.BLEGattsAttrMetadata(
            read_perm,
            write_perm,
            properties.variable_length,
            read_auth=properties.read_auth,
            write_auth=properties.write_auth)
        attr = nrf_types.BLEGattsAttribute(uuid.nrf_uuid, metadata, max_len,
                                           initial_value)
        self.ble_device.ble_driver.ble_gatts_descriptor_add(
            self._value_attr.handle, attr)

        attr = GattsAttribute(self.ble_device, self.peer, self, uuid,
                              attr.handle, properties, initial_value,
                              string_encoding)
        self._attrs.append(attr)
        return attr

    def add_constant_value_descriptor(
            self,
            uuid: Uuid,
            value: bytes,
            security_level=gatt.SecurityLevel.OPEN) -> GattsAttribute:
        """
        Adds a descriptor to the characteristic which is a constant, read-only value that cannot be updated
        after this call. This is a simplified parameter set built on top of :meth:`add_descriptor` for this common use-case.

        .. note:: See note on :meth:`add_descriptor()` for limitations on descriptors that can be added through this method.

        :param uuid: The UUID of the descriptor to add
        :param value: The value to set the descriptor to
        :param security_level: The security level for the descriptor
        :return: The descriptor that was created and added to the characteristic
        """
        props = GattsAttributeProperties(read=True,
                                         write=False,
                                         security_level=security_level,
                                         max_length=len(value),
                                         variable_length=False,
                                         write_auth=False,
                                         read_auth=False)
        return self.add_descriptor(uuid, props, value)

    """
    Properties
    """

    @property
    def max_length(self) -> int:
        """
        **Read Only**

        The max possible the value the characteristic can be set to
        """
        return self._properties.max_len

    @property
    def notifiable(self) -> bool:
        """
        **Read Only**

        Gets if the characteristic is set up to asynchonously notify clients via notifications or indications
        """
        return self._properties.indicate or self._properties.notify

    @property
    def value(self) -> bytes:
        """
        **Read Only**

        Gets the current value of the characteristic.
        Value is updated using :meth:`set_value`
        """
        return self._value

    @property
    def client_subscribed(self) -> bool:
        """
        **Read Only**

        Gets if the client is currently subscribed (notify or indicate) to this characteristic
        """
        return self.peer and self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED

    @property
    def attributes(self) -> Iterable[GattsAttribute]:
        """
        **Read Only**

        Gets all of the attributes and descriptors associated with this characteristic
        """
        return tuple(self._attrs)

    @property
    def user_description(self) -> Optional[GattsAttribute]:
        """
        **Read Only**

        Gets the User Description attribute for the characteristic if set in the properties.
        If the user description was not configured for the characteristic, returns ``None``
        """
        return self._user_desc_attr

    @property
    def sccd(self) -> Optional[GattsAttribute]:
        """
        **Read Only**

        Gets the Server Characteristic Configuration Descriptor (SCCD) attribute if set in the properties.
        If the SCCD was not configured for the characteristic, returns ``None``
        """
        return self._sccd_attr

    @property
    def presentation_format(self) -> Optional[PresentationFormat]:
        """
        **Read Only**

        Gets the presentation format that was set for the characteristic.
        If the presentation format was not configured for the characteristic, returns ``None``
        """
        return self._presentation_format

    @property
    def string_encoding(self) -> str:
        """
        The default method for encoding strings into bytes when a string is provided as a value

        :getter: Gets the string encoding in use
        :setter: Sets the string encoding to use
        """
        return self._value_attr.string_encoding

    @string_encoding.setter
    def string_encoding(self, value: str):
        self._value_attr.string_encoding = value

    """
    Events
    """

    @property
    def on_write(self) -> Event[GattsCharacteristic, WriteEventArgs]:
        """
        Event generated whenever a client writes to this characteristic.

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_write

    @property
    def on_read(self) -> Event[GattsCharacteristic, None]:
        """
        Event generated whenever a client requests to read from this characteristic. At this point, the application
        may choose to update the value of the characteristic to a new value using set_value.

        A good example of this is a "system time" characteristic which reports the applications system time in seconds.
        Instead of updating this characteristic every second, it can be "lazily" updated only when read from.

        NOTE: if there are multiple handlers subscribed to this and each set the value differently, it may cause
        undefined behavior.

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_read

    @property
    def on_subscription_change(
            self
    ) -> Event[GattsCharacteristic, SubscriptionStateChangeEventArgs]:
        """
        Event that is generated whenever a client changes its subscription state of the characteristic
        (notify, indicate, none).

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_sub_change

    @property
    def on_notify_complete(
            self) -> Event[GattsCharacteristic, NotificationCompleteEventArgs]:
        """
        Event that is generated when a notification or indication sent to the client successfully

        :return: an event which can have handlers registered to and deregistered from
        """
        return self._on_notify_complete

    """
    Event Handling
    """

    def _on_cccd_write(self, sender, event_args):
        self.cccd_state = gatt.SubscriptionState.from_buffer(
            bytearray(event_args.value))
        self._on_sub_change.notify(
            self, SubscriptionStateChangeEventArgs(self.cccd_state))

    def _on_value_write(self, sender, event_args):
        self._on_write.notify(self, event_args)

    def _on_value_read(self, sender, event_args):
        self._on_read.notify(self, event_args)

    def _on_disconnect(self, peer, event_args):
        if self._cccd_attr and self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED:
            self.cccd_state = gatt.SubscriptionState.NOT_SUBSCRIBED
Beispiel #18
0
class GattcReader(object):
    """
    Class which implements the state machine for completely reading a peripheral's attribute
    """
    _READ_OVERHEAD = 1  # Number of bytes per MTU that are overhead for the read operation

    def __init__(self, ble_device, peer):
        """
        :type ble_device: blatann.device.BleDevice
        :type peer: blatann.peer.Peer
        """
        self.ble_device = ble_device
        self.peer = peer
        self._on_read_complete_event = EventSource("On Read Complete", logger)
        self._busy = False
        self._data = bytearray()
        self._handle = 0x0000
        self._offset = 0
        self.peer.driver_event_subscribe(self._on_read_response, nrf_events.GattcEvtReadResponse)

    @property
    def on_read_complete(self):
        """
        Event that is emitted when a read completes on an attribute handle.

        Handler args: (int attribute_handle, gatt.GattStatusCode, bytes data_read)

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_read_complete_event

    def read(self, handle):
        """
        Reads the attribute value from the handle provided. Can only read from a single attribute at a time. If a
        read is in progress, raises an InvalidStateException

        :param handle: the attribute handle to read
        :return: A waitable that will fire when the read finishes.
                 See on_read_complete for the values returned from the waitable
        :rtype: EventWaitable
        """
        if self._busy:
            raise InvalidStateException("Gattc Reader is busy")
        self._handle = handle
        self._offset = 0
        self._data = bytearray()
        logger.debug("Starting read from handle {}".format(handle))
        self._read_next_chunk()
        self._busy = True
        return EventWaitable(self.on_read_complete)

    def _read_next_chunk(self):
        self.ble_device.ble_driver.ble_gattc_read(self.peer.conn_handle, self._handle, self._offset)

    def _on_read_response(self, driver, event):
        """
        Handler for GattcEvtReadResponse

        :type event: nrf_events.GattcEvtReadResponse
        """
        if event.conn_handle != self.peer.conn_handle or event.attr_handle != self._handle:
            return
        if event.status != nrf_events.BLEGattStatusCode.success:
            self._complete(event.status)
            return

        bytes_read = len(event.data)
        self._data += bytearray(event.data)
        self._offset += bytes_read

        if bytes_read == (self.peer.mtu_size - self._READ_OVERHEAD):
            self._read_next_chunk()
        else:
            self._complete()

    def _complete(self, status=nrf_events.BLEGattStatusCode.success):
        self._busy = False
        event_args = GattcReadCompleteEventArgs(self._handle, status, bytes(self._data))
        self._on_read_complete_event.notify(self, event_args)
Beispiel #19
0
class SecurityManager(object):
    """
    Handles performing security procedures with a connected peer
    """
    def __init__(self, ble_device, peer, security_parameters):
        """
        :type ble_device: blatann.BleDevice
        :type peer: blatann.peer.Peer
        :type security_parameters: SecurityParameters
        """
        self.ble_device = ble_device
        self.peer = peer
        self._security_params = security_parameters
        self._pairing_in_process = False
        self._initiated_encryption = False
        self._is_previously_bonded_device = False

        self._on_authentication_complete_event = EventSource(
            "On Authentication Complete", logger)
        self._on_passkey_display_event = EventSource("On Passkey Display",
                                                     logger)
        self._on_passkey_entry_event = EventSource("On Passkey Entry", logger)
        self._on_security_level_changed_event = EventSource(
            "Security Level Changed", logger)
        self._on_peripheral_security_request_event = EventSource(
            "Peripheral Security Request", logger)
        self._on_pairing_request_rejected_event = EventSource(
            "Pairing Attempt Rejected", logger)
        self.peer.on_connect.register(self._on_peer_connected)
        self._auth_key_resolve_thread = threading.Thread(daemon=True)
        self._peripheral_security_request_thread = threading.Thread(
            daemon=True)
        self.keyset = nrf_types.BLEGapSecKeyset()
        self.bond_db_entry = None
        self._security_level = SecurityLevel.NO_ACCESS
        self._private_key = smp_crypto.lesc_generate_private_key()
        self._public_key = self._private_key.public_key()
        self.keyset.own_keys.public_key.key = smp_crypto.lesc_pubkey_to_raw(
            self._public_key)

    """
    Events
    """

    @property
    def on_pairing_complete(self) -> Event[Peer, PairingCompleteEventArgs]:
        """
        Event that is triggered when pairing completes with the peer

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_authentication_complete_event

    @property
    def on_security_level_changed(
            self) -> Event[Peer, SecurityLevelChangedEventArgs]:
        """
        Event that is triggered when the security/encryption level changes. This can be triggered from
        a pairing sequence or if a bonded client starts the encryption handshaking using the stored LTKs.

        Note: This event is triggered before on_pairing_complete

        :return: an Event which can have handlers registered to and deregestestered from
        """
        return self._on_security_level_changed_event

    @property
    def on_passkey_display_required(
            self) -> Event[Peer, PasskeyDisplayEventArgs]:
        """
        Event that is triggered when a passkey needs to be displayed to the user and depending on
        the pairing mode the user must confirm that keys match (PasskeyDisplayEventArgs.match_request == True).

        .. note:: If multiple handlers are registered to this event, the first handler which resolves the match
           confirmation will set the response. All others will be ignored.

        :return: an Event which can have handlers registered to and deregistered from
        :rtype: Event
        """
        return self._on_passkey_display_event

    @property
    def on_passkey_required(self) -> Event[Peer, PasskeyEntryEventArgs]:
        """
        Event that is triggered when a passkey needs to be entered by the user

        .. note:: If multiple handlers are registered to this event, the first handler which resolves the passkey will
           set the value. All others will be ignored.

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_passkey_entry_event

    @property
    def on_peripheral_security_request(
            self) -> Event[Peer, PeripheralSecurityRequestEventArgs]:
        """
        Event that is triggered when the connected peripheral explicitly requests pairing/encryption
        to be enabled. The event provides the higher levels an opportunity to accept, reject,
        or force re-pair with the peripheral.

        If no handler is registered to this event, pairing requests will be accepted unless the reject_pairing_requests
        parameter is set.

        .. note:: If a handler is registered to this event, it **must** respond with one of the options (accept/reject/repair).

        .. note:: If multiple handlers are registered to this event, the first handler to respond is the response used.
           All other inputs will be ignored

        :return: Event that is triggered when the peripheral requests a secure connection
        """
        return self._on_peripheral_security_request_event

    @property
    def on_pairing_request_rejected(
            self) -> Event[Peer, PairingRejectedEventArgs]:
        """
        Event that's emitted when a pairing request is rejected locally, either due to the user
        event handler or due to the rejection policy set in the security parameters

        :return: Event that is triggered when a pairing request is rejected
        """
        return self._on_pairing_request_rejected_event

    """
    Properties
    """

    @property
    def is_previously_bonded(self) -> bool:
        """
        Gets if the peer this security manager is for was bonded in a previous connection

        :return: True if previously bonded, False if not
        """
        return self._is_previously_bonded_device

    @property
    def pairing_in_process(self) -> bool:
        """
        Gets whether or not pairing/encryption is currently in process
        """
        return self._pairing_in_process or self._initiated_encryption

    @property
    def security_level(self) -> SecurityLevel:
        """
        Gets the current security level of the connection
        """
        return self._security_level

    @property
    def security_params(self) -> SecurityParameters:
        """
        Gets the security parameters structure
        """
        return self._security_params

    @security_params.setter
    def security_params(self, params: SecurityParameters):
        """
        Sets the security parameters
        """
        self._security_params = params

    """
    Public Methods
    """

    def set_security_params(
            self,
            passcode_pairing: bool,
            io_capabilities: IoCapabilities,
            bond: bool,
            out_of_band: bool,
            reject_pairing_requests: Union[bool, PairingPolicy] = False,
            lesc_pairing: bool = False):
        """
        Sets the security parameters to use with the peer

        :param passcode_pairing: Flag indicating that passcode pairing is required
        :param io_capabilities: The input/output capabilities of this device
        :param bond: Flag indicating that long-term bonding should be performed
        :param out_of_band: Flag indicating if out-of-band pairing is supported
        :param reject_pairing_requests: Flag indicating that all security requests by the peer should be rejected
        :param lesc_pairing: Flag indicating that LE Secure Pairing methods are supported
        """
        self._security_params = SecurityParameters(passcode_pairing,
                                                   io_capabilities, bond,
                                                   out_of_band,
                                                   reject_pairing_requests,
                                                   lesc_pairing)

    def pair(
        self,
        force_repairing=False
    ) -> EventWaitable[Peer, PairingCompleteEventArgs]:
        """
        Starts the pairing process with the peer with the set security parameters.

        If the peer is already bonded, initiates the encryption process unless force_repairing is set to True

        If the peer is a central and we are a local device, sends the peripheral security request to the central
        so they can start the pairing/encryption process

        :return: A waitable that will trigger when pairing is complete
        """
        if self.pairing_in_process:
            logger.warning(
                "Attempted to pair while pairing/encryption already in progress. Returning waitable for when it finishes"
            )
            return EventWaitable(self.on_pairing_complete)

        # if in the client role and don't want to force a re-pair, check for bonding data first
        if self.peer.is_peripheral and not force_repairing:
            bond_entry = self._find_db_entry(self.peer.peer_address)
            if bond_entry:
                logger.info("Re-establishing encryption with peer using LTKs")
                self.ble_device.ble_driver.ble_gap_encrypt(
                    self.peer.conn_handle,
                    bond_entry.bonding_data.own_ltk.master_id,
                    bond_entry.bonding_data.own_ltk.enc_info)
                self._initiated_encryption = True
                return EventWaitable(self.on_pairing_complete)

        sec_params = self._get_security_params()
        self.ble_device.ble_driver.ble_gap_authenticate(
            self.peer.conn_handle, sec_params)
        self._pairing_in_process = True
        return EventWaitable(self.on_pairing_complete)

    def use_debug_lesc_key(self):
        """
        Changes the security settings to use the debug public/private key-pair for future LESC pairing interactions.
        The key is defined in the Core Bluetooth Specification v4.2 Vol.3, Part H, Section 2.3.5.6.

        .. warning:: Using this key allows Bluetooth sniffers to be able to decode the encrypted traffic over the air
        """
        self._private_key = smp_crypto.LESC_DEBUG_PRIVATE_KEY
        self._public_key = smp_crypto.LESC_DEBUG_PUBLIC_KEY
        self.keyset.own_keys.public_key.key = smp_crypto.lesc_pubkey_to_raw(
            self._public_key)

    def delete_bonding_data(self):
        """
        Deletes the bonding data for the peer, if any.
        Cannot be called during pairing, will throw an InvalidOperationException
        """
        if self.pairing_in_process:
            raise InvalidOperationException(
                "Cannot clear bonding data while pairing is in progress")
        if self.bond_db_entry:
            db_entry = self.bond_db_entry
            self.bond_db_entry = None
            self.ble_device.bond_db.delete(db_entry)
            self._is_previously_bonded_device = False
            # TODO: This doesn't belong here..
            self.ble_device.bond_db_loader.save(self.ble_device.bond_db)

    """
    Private Methods
    """

    @property
    def _pairing_policy(self) -> PairingPolicy:
        return self._security_params.reject_pairing_requests

    def _on_peer_connected(self, peer, event_args):
        # Reset the
        self._pairing_in_process = False
        self._initiated_encryption = False
        self._security_level = SecurityLevel.OPEN
        self.peer.driver_event_subscribe(self._on_security_params_request,
                                         nrf_events.GapEvtSecParamsRequest)
        self.peer.driver_event_subscribe(self._on_authentication_status,
                                         nrf_events.GapEvtAuthStatus)
        self.peer.driver_event_subscribe(self._on_conn_sec_status,
                                         nrf_events.GapEvtConnSecUpdate)
        self.peer.driver_event_subscribe(self._on_auth_key_request,
                                         nrf_events.GapEvtAuthKeyRequest)
        self.peer.driver_event_subscribe(self._on_passkey_display,
                                         nrf_events.GapEvtPasskeyDisplay)
        self.peer.driver_event_subscribe(self._on_security_info_request,
                                         nrf_events.GapEvtSecInfoRequest)
        self.peer.driver_event_subscribe(self._on_lesc_dhkey_request,
                                         nrf_events.GapEvtLescDhKeyRequest)
        self.peer.driver_event_subscribe(self._on_security_request,
                                         nrf_events.GapEvtSecRequest)
        # Search the bonding DB for this peer's info
        self.bond_db_entry = self._find_db_entry(self.peer.peer_address)
        if self.bond_db_entry:
            logger.info("Connected to previously bonded device {}".format(
                self.bond_db_entry.peer_addr))
            self._is_previously_bonded_device = True
        else:
            self._is_previously_bonded_device = False

    def _find_db_entry(self, peer_address):
        if peer_address.addr_type == nrf_types.BLEGapAddrTypes.random_private_non_resolvable:
            return None

        for r in self.ble_device.bond_db:
            if self.peer.is_client != r.peer_is_client:
                continue

            # If peer address is public or random static, check directly if they match (no IRK needed)
            if peer_address.addr_type in [
                    nrf_types.BLEGapAddrTypes.random_static,
                    nrf_types.BLEGapAddrTypes.public
            ]:
                if r.peer_addr == peer_address:
                    return r
            elif smp_crypto.private_address_resolves(
                    peer_address, r.bonding_data.peer_id.irk):
                logger.info("Resolved Peer ID to {}".format(r.peer_addr))
                return r

        return None

    def _get_security_params(self):
        keyset_own = nrf_types.BLEGapSecKeyDist(True, True, False, False)
        keyset_peer = nrf_types.BLEGapSecKeyDist(True, True, False, False)
        sec_params = nrf_types.BLEGapSecParams(
            self._security_params.bond, self._security_params.passcode_pairing,
            self._security_params.lesc_pairing, False,
            self._security_params.io_capabilities,
            self._security_params.out_of_band, 7, 16, keyset_own, keyset_peer)
        return sec_params

    def _on_security_params_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtSecParamsRequest
        """
        # Security parameters are only provided for clients
        sec_params = self._get_security_params(
        ) if self.peer.is_client else None
        rejection_reason = None

        # Check if the pairing request should be rejected
        if self.peer.is_client:
            if self.is_previously_bonded and PairingPolicy.reject_bonded_device_repairing_requests in self._pairing_policy:
                rejection_reason = PairingRejectedReason.bonded_device_repairing
            elif PairingPolicy.reject_new_pairing_requests in self._pairing_policy:
                rejection_reason = PairingRejectedReason.non_bonded_central_request

        if not rejection_reason:
            status = nrf_types.BLEGapSecStatus.success
            self.ble_device.ble_driver.ble_gap_sec_params_reply(
                event.conn_handle, nrf_types.BLEGapSecStatus.success,
                sec_params, self.keyset)
            self._pairing_in_process = True
        else:
            self.ble_device.ble_driver.ble_gap_sec_params_reply(
                event.conn_handle, nrf_types.BLEGapSecStatus.pairing_not_supp,
                sec_params, self.keyset)
            self._on_pairing_request_rejected_event.notify(
                self.peer, PairingRejectedEventArgs(rejection_reason))

    def _on_security_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtSecRequest
        """
        if self._on_peripheral_security_request_event.has_handlers:
            request_handled = threading.Event()

            def handle_request(
                    mode=PeripheralSecurityRequestEventArgs.Response.accept):
                if request_handled.is_set():
                    return
                if mode == PeripheralSecurityRequestEventArgs.Response.reject:
                    self.ble_device.ble_driver.ble_gap_authenticate(
                        event.conn_handle, None)
                    args = PairingRejectedEventArgs(
                        PairingRejectedReason.user_rejected)
                    self._on_pairing_request_rejected_event.notify(
                        self.peer, args)
                else:
                    force_repair = mode == PeripheralSecurityRequestEventArgs.Response.force_repair
                    self.pair(force_repair)
                request_handled.set()

            event_args = PeripheralSecurityRequestEventArgs(
                event.bond, event.mitm, event.lesc, event.keypress,
                self.is_previously_bonded, handle_request)
            self._peripheral_security_request_thread = threading.Thread(
                name=f"{self.peer.conn_handle} Security Request",
                target=self._on_peripheral_security_request_event.notify,
                args=(self.peer, event_args),
                daemon=True)
            self._peripheral_security_request_thread.start()
            return

        # No handler specified, use pairing policy to reject if needed
        rejection_reason = None
        if self.is_previously_bonded:
            if PairingPolicy.reject_bonded_peripheral_requests in self._pairing_policy:
                rejection_reason = PairingRejectedReason.bonded_peripheral_request
        else:
            policy_checks = [
                PairingPolicy.reject_nonbonded_peripheral_requests,
                PairingPolicy.reject_new_pairing_requests
            ]
            if any(p in self._pairing_policy for p in policy_checks):
                rejection_reason = PairingRejectedReason.non_bonded_peripheral_request

        if rejection_reason:
            self.ble_device.ble_driver.ble_gap_authenticate(
                event.conn_handle, None)
            self._on_pairing_request_rejected_event.notify(
                self.peer, PairingRejectedEventArgs(rejection_reason))
        else:
            self.pair()
        return

    def _on_security_info_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtSecInfoRequest
        """
        found_record = None
        # Find the database entry based on the sec info given
        for r in self.ble_device.bond_db:
            # Check that roles match
            if r.peer_is_client != self.peer.is_client:
                continue
            own_mid = r.bonding_data.own_ltk.master_id
            peer_mid = r.bonding_data.peer_ltk.master_id
            if event.master_id.ediv == own_mid.ediv and event.master_id.rand == own_mid.rand:
                logger.info(
                    "Found matching record with own master ID for sec info request"
                )
                found_record = r
                break
            if event.master_id.ediv == peer_mid.ediv and event.master_id.rand == peer_mid.rand:
                logger.info(
                    "Found matching record with peer master ID for sec info request"
                )
                found_record = r
                break

        if not found_record:
            logger.info(
                "Unable to find Bonding record for peer master id {}".format(
                    event.master_id))
            self.ble_device.ble_driver.ble_gap_sec_info_reply(
                event.conn_handle)
        else:
            self.bond_db_entry = found_record
            ltk = found_record.bonding_data.own_ltk
            id_key = found_record.bonding_data.peer_id
            self.ble_device.ble_driver.ble_gap_sec_info_reply(
                event.conn_handle, ltk.enc_info, id_key, None)

    def _on_lesc_dhkey_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtLescDhKeyRequest
        """
        peer_public_key = smp_crypto.lesc_pubkey_from_raw(
            event.remote_public_key.key)
        dh_key = smp_crypto.lesc_compute_dh_key(self._private_key,
                                                peer_public_key,
                                                little_endian=True)
        self.ble_device.ble_driver.ble_gap_lesc_dhkey_reply(
            event.conn_handle, nrf_types.BLEGapDhKey(dh_key))

    def _on_conn_sec_status(self, driver, event):
        """
        :type event: nrf_events.GapEvtConnSecUpdate
        """
        self._security_level = SecurityLevel(event.sec_level)
        self._on_security_level_changed_event.notify(
            self.peer, SecurityLevelChangedEventArgs(self._security_level))
        if self._initiated_encryption:
            self._initiated_encryption = False
            if event.sec_level > 0 and event.sec_mode > 0:
                status = SecurityStatus.success
            else:
                logger.warning(
                    "Peer failed to load bonding data, deleting bond entry from database"
                )
                # Peer failed to find/load the keys, return failure status code and remove key from database
                self.delete_bonding_data()
                status = SecurityStatus.unspecified
            self._on_authentication_complete_event.notify(
                self.peer, PairingCompleteEventArgs(status,
                                                    self.security_level))

    def _on_authentication_status(self, driver, event):
        """
        :type event: nrf_events.GapEvtAuthStatus
        """
        self._pairing_in_process = False
        self._on_authentication_complete_event.notify(
            self.peer,
            PairingCompleteEventArgs(event.auth_status, self.security_level))
        # Save keys in the database if authenticated+bonded successfully
        if event.auth_status == SecurityStatus.success and event.bonded:
            # Reload the keys from the C Memory space (were updated during the pairing process)
            self.keyset.reload()

            # If there wasn't a bond record initially, try again a second time using the new public peer address
            if not self.bond_db_entry:
                self.bond_db_entry = self._find_db_entry(
                    self.keyset.peer_keys.id_key.peer_addr)

            # Still no bond DB entry, create a new one
            if not self.bond_db_entry:
                logger.info("New bonded device, creating a DB Entry")
                self.bond_db_entry = self.ble_device.bond_db.create()
                self.bond_db_entry.peer_is_client = self.peer.is_client
                self.bond_db_entry.peer_addr = self.keyset.peer_keys.id_key.peer_addr
                self.bond_db_entry.bonding_data = BondingData(self.keyset)
                self.bond_db_entry.name = self.peer.name
                self.ble_device.bond_db.add(self.bond_db_entry)
            else:  # update the bonding info
                logger.info("Updating bond key for peer {}".format(
                    self.keyset.peer_keys.id_key.peer_addr))
                self.bond_db_entry.bonding_data = BondingData(self.keyset)

            # TODO: This doesn't belong here..
            self.ble_device.bond_db_loader.save(self.ble_device.bond_db)

    def _on_passkey_display(self, driver, event):
        """
        :type event: nrf_events.GapEvtPasskeyDisplay
        """
        match_confirmed = threading.Event()

        def match_confirm(keys_match):
            if not self._pairing_in_process or match_confirmed.is_set():
                return
            if keys_match:
                key_type = nrf_types.BLEGapAuthKeyType.PASSKEY
            else:
                key_type = nrf_types.BLEGapAuthKeyType.NONE

            self.ble_device.ble_driver.ble_gap_auth_key_reply(
                event.conn_handle, key_type, None)
            match_confirmed.set()

        event_args = PasskeyDisplayEventArgs(event.passkey,
                                             event.match_request,
                                             match_confirm)
        if event.match_request:
            self._auth_key_resolve_thread = threading.Thread(
                name="{} Passkey Confirm".format(self.peer.conn_handle),
                target=self._on_passkey_display_event.notify,
                args=(self.peer, event_args),
                daemon=True)
            self._auth_key_resolve_thread.daemon = True
            self._auth_key_resolve_thread.start()
        else:
            self._on_passkey_display_event.notify(self.peer, event_args)

    def _on_auth_key_request(self, driver, event):
        """
        :type event: nrf_events.GapEvtAuthKeyRequest
        """
        passkey_entered = threading.Event()

        def resolve(passkey):
            if not self._pairing_in_process or passkey_entered.is_set():
                return
            if isinstance(passkey, int):
                passkey = "{:06d}".format(passkey).encode("ascii")
            elif isinstance(passkey, str):
                passkey = passkey.encode("ascii")
            self.ble_device.ble_driver.ble_gap_auth_key_reply(
                self.peer.conn_handle, event.key_type, passkey)
            passkey_entered.set()

        self._auth_key_resolve_thread = threading.Thread(
            name="{} Passkey Entry".format(self.peer.conn_handle),
            target=self._on_passkey_entry_event.notify,
            args=(self.peer, PasskeyEntryEventArgs(event.key_type, resolve)),
            daemon=True)
        self._auth_key_resolve_thread.start()

    def _on_timeout(self, driver, event):
        """
        :type event: nrf_events.GapEvtTimeout
        """
        if event.src != nrf_types.BLEGapTimeoutSrc.security_req:
            return
        self._on_authentication_complete_event.notify(
            self.peer,
            PairingCompleteEventArgs(SecurityStatus.timeout,
                                     self.security_level))
Beispiel #20
0
class Advertiser(object):
    """
    Class which manages the advertising state of the BLE Device
    """
    # Constant used to indicate that the BLE device should advertise indefinitely, until
    # connected to or stopped manually
    ADVERTISE_FOREVER = 0
    """Special value used to indicate that the BLE device should advertise indefinitely 
       until either a central is connected or stopped manually."""
    def __init__(self, ble_device, client, conn_tag=0):
        """
        :type ble_device: blatann.device.BleDevice
        :type client: blatann.peer.Client
        """
        self.ble_device = ble_device
        self._is_advertising = False
        self._auto_restart = False
        self.client = client
        self.ble_device.ble_driver.event_subscribe(self._handle_adv_timeout,
                                                   nrf_events.GapEvtTimeout)
        self.client.on_disconnect.register(self._handle_disconnect)
        self.client.on_connect.register(self._handle_connect)
        self._on_advertising_timeout = EventSource("Advertising Timeout",
                                                   logger)
        self._advertise_interval = 100
        self._timeout = self.ADVERTISE_FOREVER
        self._advertise_mode = AdvertisingMode.connectable_undirected
        self._conn_tag = conn_tag

    @property
    def on_advertising_timeout(self) -> Event[Advertiser, None]:
        """
        Event generated whenever advertising times out and finishes with no connections made

        .. note:: If auto-restart advertising is enabled, this will trigger on each advertising timeout configured

        :return: an Event which can have handlers registered to and deregistered from
        """
        return self._on_advertising_timeout

    @property
    def is_advertising(self) -> bool:
        """
        **Read Only**

        Current state of advertising
        """
        return self._is_advertising

    @property
    def min_interval_ms(self) -> float:
        """
        **Read Only**

        The minimum allowed advertising interval, in millseconds.
        This is defined by the Bluetooth specification.
        """
        return MIN_ADVERTISING_INTERVAL_MS

    @property
    def max_interval_ms(self) -> float:
        """
        **Read Only**

        The maximum allowed advertising interval, in milliseconds.
        This is defined by the Bluetooth specification.
        """
        return MAX_ADVERTISING_INTERVAL_MS

    @property
    def auto_restart(self) -> bool:
        """
        Enables/disables whether or not the device should automatically restart
        advertising when an advertising timeout occurs or the client is disconnected.

        .. note:: Auto-restart is disabled automatically when :meth:`stop` is called

        :getter: Gets the auto-restart flag
        :setter: Sets/clears the auto-restart flag
        """
        return self._auto_restart

    @auto_restart.setter
    def auto_restart(self, value: bool):
        self._auto_restart = bool(value)

    def set_advertise_data(self,
                           advertise_data: AdvertisingData = AdvertisingData(),
                           scan_response: AdvertisingData = AdvertisingData()):
        """
        Sets the advertising and scan response data which will be broadcasted to peers during advertising

        .. note:: BLE Restricts advertise and scan response data to an encoded length of 31 bytes each.
           Use :meth:`AdvertisingData.check_encoded_length() <blatann.gap.advertise_data.AdvertiseData.check_encoded_length>`
           to determine if the payload is too large

        :param advertise_data: The advertising data to use
        :param scan_response: The scan response data to use.
                              This data is only sent when a scanning device requests the scan response packet (active scanning)
        :raises: InvalidOperationException if one of the payloads is too large
        """
        adv_len, adv_pass = advertise_data.check_encoded_length()
        scan_len, scan_pass = scan_response.check_encoded_length()

        if not adv_pass:
            raise exceptions.InvalidOperationException(
                "Encoded Advertising data length is too long ({} bytes). "
                "Max: {} bytes".format(adv_len,
                                       advertise_data.MAX_ENCODED_LENGTH))

        if not scan_pass:
            raise exceptions.InvalidOperationException(
                "Encoded Scan Response data length is too long ({} bytes). "
                "Max: {} bytes".format(scan_len,
                                       advertise_data.MAX_ENCODED_LENGTH))

        self.ble_device.ble_driver.ble_gap_adv_data_set(
            advertise_data.to_ble_adv_data(), scan_response.to_ble_adv_data())

    def set_default_advertise_params(
        self,
        advertise_interval_ms: float,
        timeout_seconds: int,
        advertise_mode: AdvertisingMode = AdvertisingMode.
        connectable_undirected):
        """
        Sets the default advertising parameters so they do not need to be specified on each start

        :param advertise_interval_ms: The advertising interval, in milliseconds.
                                      Should be a multiple of 0.625ms, otherwise it'll be rounded down to the nearest 0.625ms
        :param timeout_seconds: How long to advertise for before timing out, in seconds. For no timeout, use ADVERTISE_FOREVER (0)
        :param advertise_mode: The mode the advertiser should use
        """
        nrf_types.adv_interval_range.validate(advertise_interval_ms)
        self._advertise_interval = advertise_interval_ms
        self._timeout = timeout_seconds
        self._advertise_mode = advertise_mode

    def start(self,
              adv_interval_ms: float = None,
              timeout_sec: int = None,
              auto_restart: bool = None,
              advertise_mode: AdvertisingMode = None):
        """
        Starts advertising with the given parameters. If none given, will use the default set through :meth:`set_default_advertise_params`

        :param adv_interval_ms: The interval at which to send out advertise packets, in milliseconds.
                                Should be a multiple of 0.625ms, otherwise it'll be round down to the nearest 0.625ms
        :param timeout_sec: The duration which to advertise for. For no timeout, use ADVERTISE_FOREVER (0)
        :param auto_restart: Flag indicating that advertising should restart automatically when the timeout expires, or
                             when the client disconnects
        :param advertise_mode: The mode the advertiser should use
        :return: A waitable that will expire either when the timeout occurs or a client connects.
                 The waitable will return either ``None`` on timeout or :class:`~blatann.peer.Client` on successful connection
        :rtype: ClientConnectionWaitable
        """
        if self._is_advertising:
            self._stop()
        if adv_interval_ms is None:
            adv_interval_ms = self._advertise_interval
        else:
            nrf_types.adv_interval_range.validate(adv_interval_ms)
        if timeout_sec is None:
            timeout_sec = self._timeout
        if advertise_mode is None:
            advertise_mode = self._advertise_mode
        if auto_restart is None:
            auto_restart = self._auto_restart

        self._timeout = timeout_sec
        self._advertise_interval = adv_interval_ms
        self._advertise_mode = advertise_mode
        self._auto_restart = auto_restart

        self._start()

        return ClientConnectionWaitable(self.ble_device, self.client)

    def _start(self):
        params = nrf_types.BLEGapAdvParams(self._advertise_interval,
                                           self._timeout, self._advertise_mode)
        logger.info(
            "Starting advertising, params: {}, auto-restart: {}".format(
                params, self._auto_restart))
        self.ble_device.ble_driver.ble_gap_adv_start(params, self._conn_tag)
        self._is_advertising = True

    def stop(self):
        """
        Stops advertising and disables the auto-restart functionality (if enabled)
        """
        self._auto_restart = False
        self._stop()

    def _stop(self):
        self._is_advertising = False
        try:
            self.ble_device.ble_driver.ble_gap_adv_stop()
        except Exception:
            pass

    def _handle_adv_timeout(self, driver, event):
        """
        :type event: nrf_events.GapEvtTimeout
        """
        if event.src == nrf_events.BLEGapTimeoutSrc.advertising:
            # Notify that advertising timed out first which may call stop() to disable auto-restart
            self._on_advertising_timeout.notify(self)
            if self._auto_restart:
                self._start()
            else:
                self._is_advertising = False

    def _handle_connect(self, peer, event):
        self._is_advertising = False

    def _handle_disconnect(self, peer, event):
        if self._auto_restart:
            self._start()