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)
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)
def __init__(self, gattc_service): """ :type gattc_service: blatann.gatt.gattc.GattcService """ self._service = gattc_service self._current_time_char = gattc_service.find_characteristic( CURRENT_TIME_CHARACTERISTIC_UUID) self._local_time_info_char = gattc_service.find_characteristic( LOCAL_TIME_INFO_CHARACTERISTIC_UUID) self._ref_info_char = gattc_service.find_characteristic( REFERENCE_INFO_CHARACTERISTIC_UUID) self._on_current_time_updated_event = EventSource( "Current Time Update Event") self._on_local_time_info_updated_event = EventSource( "Local Time Info Update Event") self._on_reference_info_updated_event = EventSource( "Reference Info Update Event") self._current_time_dispatcher = DecodedReadWriteEventDispatcher( self, CurrentTime, self._on_current_time_updated_event, logger) self._local_time_dispatcher = DecodedReadWriteEventDispatcher( self, LocalTimeInfo, self._on_local_time_info_updated_event, logger) self._ref_time_dispatcher = DecodedReadWriteEventDispatcher( self, ReferenceTimeInfo, self._on_reference_info_updated_event, logger)
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 = []
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)
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 __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)
def __init__(self, reader: GattcReader, 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) self._reader.peer.driver_event_subscribe(self._on_timeout, nrf_events.GattcEvtTimeout)
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)
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)
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 = b"" 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)
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
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)
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 __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
def __init__(self, ble_device): """ :type ble_device: blatann.device.BleDevice """ self.ble_device = ble_device self._default_scan_params = ScanParameters(200, 150, 10) self._is_scanning = False ble_device.ble_driver.event_subscribe(self._on_adv_report, nrf_events.GapEvtAdvReport) ble_device.ble_driver.event_subscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) self.scan_report = ScanReportCollection() self._on_scan_received: EventSource[Scanner, ScanReport] = EventSource( "On Scan Received", logger) self._on_scan_timeout: EventSource[ Scanner, ScanReportCollection] = EventSource("On Scan Timeout")
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()
def __init__(self, ble_device, peer, uuid, properties, notification_manager, value="", prefer_indications=True): """ :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) 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)
def __init__(self, name, ble_device, peer): """ :type ble_device: blatann.BleDevice :type peer: blatann.peer.Peer """ self.ble_device = ble_device self.peer = peer self._state = _DiscoveryState() self._on_complete_event = EventSource("{} Complete".format(name), logger)
def __init__(self, service, is_writable=False, enable_local_time_info_char=False, enable_ref_time_info_char=False): """ :type service: GattsService :param is_writable: :param enable_local_time_info_char: :param enable_ref_time_info_char: """ self._service = service self._is_writable = is_writable self._has_local_time_info = enable_local_time_info_char self._has_ref_time_info = enable_ref_time_info_char self._current_time_read_callback = self._on_characteristic_read_auto self._time_delta = datetime.timedelta() self._on_current_time_write_event = EventSource("Current Time Write Event") self._on_local_time_info_write_event = EventSource("Local Time Info Write Event") self._current_time_dispatcher = DecodedReadWriteEventDispatcher(self, CurrentTime, self._on_current_time_write_event, logger) self._local_time_dispatcher = DecodedReadWriteEventDispatcher(self, LocalTimeInfo, self._on_local_time_info_write_event, logger) cur_time_char_props = GattsCharacteristicProperties(read=True, notify=True, write=is_writable, variable_length=False, max_length=CurrentTime.encoded_size()) self._cur_time_char = service.add_characteristic(CURRENT_TIME_CHARACTERISTIC_UUID, cur_time_char_props) self._cur_time_char.on_read.register(self._on_current_time_read) self._cur_time_char.on_write.register(self._current_time_dispatcher) if enable_local_time_info_char: local_time_props = GattsCharacteristicProperties(read=True, notify=True, write=is_writable, variable_length=False, max_length=LocalTimeInfo.encoded_size()) self._local_time_char = service.add_characteristic(LOCAL_TIME_INFO_CHARACTERISTIC_UUID, local_time_props) self.set_local_time_info() self._local_time_char.on_write.register(self._local_time_dispatcher) if enable_ref_time_info_char: ref_time_props = GattsCharacteristicProperties(read=True, notify=False, write=False, variable_length=False, max_length=ReferenceTimeInfo.encoded_size()) self._ref_time_char = service.add_characteristic(REFERENCE_INFO_CHARACTERISTIC_UUID, ref_time_props) self.set_reference_info() self.set_time(datetime.datetime.utcfromtimestamp(0))
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)
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)
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 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 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 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 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
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))
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__()