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)
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__()
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
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__()
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()
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__()
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)
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))
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)
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 = []
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))
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)
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))
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)
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)
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)
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
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)
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))
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()