示例#1
0
    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)
示例#2
0
class _DisClientCharacteristic(_DisCharacteristic):
    def __init__(self, service, uuid, data_class):
        super(_DisClientCharacteristic, self).__init__(service, uuid, data_class)
        self._char = service.find_characteristic(uuid)
        self._on_read_complete_event = EventSource("Char {} Read Complete".format(self.uuid))

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

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

    def read(self):
        if not self.is_defined:
            raise AttributeError("Characteristic {} is not present in the Device Info Service".format(self.uuid))
        self._char.read().then(self._read_complete)
        return EventWaitable(self._on_read_complete_event)
示例#3
0
    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)
示例#4
0
 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 = []
示例#5
0
    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)
示例#6
0
 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")
示例#7
0
 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)
示例#8
0
 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)
示例#9
0
 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)
示例#10
0
文件: peer.py 项目: jiemde/blatann
    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)
示例#11
0
    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)
示例#12
0
 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
示例#13
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)
示例#14
0
 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)
示例#15
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
示例#16
0
 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")
示例#17
0
 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()
示例#18
0
 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)
示例#19
0
 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)
示例#20
0
    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))
示例#21
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)
示例#22
0
    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)
示例#23
0
class SecurityManager(object):
    """
    Handles performing security procedures with a connected peer
    """
    def __init__(self, ble_device, peer, security_parameters):
        """
        :type ble_device: blatann.BleDevice
        :type peer: blatann.peer.Peer
        :type security_parameters: SecurityParameters
        """
        self.ble_device = ble_device
        self.peer = peer
        self._security_params = security_parameters
        self._pairing_in_process = False
        self._initiated_encryption = False
        self._is_previously_bonded_device = False

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

    """
    Events
    """

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

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

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

        Note: This event is triggered before on_pairing_complete

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    """
    Properties
    """

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

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

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

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

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

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

    """
    Public Methods
    """

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

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

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

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

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

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

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

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

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

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

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

    """
    Private Methods
    """

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

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

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

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

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

        return None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def _on_timeout(self, driver, event):
        """
        :type event: nrf_events.GapEvtTimeout
        """
        if event.src != nrf_types.BLEGapTimeoutSrc.security_req:
            return
        self._on_authentication_complete_event.notify(
            self.peer,
            PairingCompleteEventArgs(SecurityStatus.timeout,
                                     self.security_level))
示例#24
0
class DatabaseDiscoverer(object):
    def __init__(self, ble_device, peer):
        """
        :type ble_device: blatann.device.BleDevice
        :type peer: blatann.peer.Peer
        """
        self.ble_device = ble_device
        self.peer = peer
        self._on_discovery_complete = EventSource("Service Discovery Complete",
                                                  logger)
        self._on_database_discovery_complete = EventSource(
            "Service Discovery Complete", logger)
        self._state = _DiscoveryState()
        self._service_discoverer = _ServiceDiscoverer(ble_device, peer)
        self._characteristic_discoverer = _CharacteristicDiscoverer(
            ble_device, peer)
        self._descriptor_discoverer = _DescriptorDiscoverer(ble_device, peer)

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

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

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

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

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

    def start(self):
        logger.info("Starting discovery..")
        self._service_discoverer.start().then(
            self._on_service_discovery_complete)
示例#25
0
class GattsCharacteristic(gatt.Characteristic):
    """
    Represents a single characteristic within a service. This class is usually not instantiated directly; it
    is added to a service through :meth:`GattsService.add_characteristic`
    """
    _QueuedChunk = namedtuple("QueuedChunk", ["offset", "data"])

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

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

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

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

    """
    Public Methods
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    """
    Properties
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    """
    Events
    """

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

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

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

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

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

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

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

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

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

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

    """
    Event Handling
    """

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

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

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

    def _on_disconnect(self, peer, event_args):
        if self._cccd_attr and self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED:
            self.cccd_state = gatt.SubscriptionState.NOT_SUBSCRIBED
示例#26
0
class GattcAttribute(Attribute):
    """
    Represents a client-side interface to a single attribute which lives inside a Characteristic
    """
    def __init__(self, uuid: Uuid, handle: int, read_write_manager: GattcOperationManager,
                 initial_value=b"", string_encoding="utf8"):
        super(GattcAttribute, self).__init__(uuid, handle, initial_value, string_encoding)
        self._manager = read_write_manager

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

    """
    Events
    """

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

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

    """
    Public Methods
    """

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

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

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

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

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

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

    def _write_complete(self, sender, event_args):
        # Success, update the local value
        if event_args.handle == self._handle:
            if event_args.status == nrf_types.BLEGattStatusCode.success:
                self._value = event_args.data
            args = WriteCompleteEventArgs(event_args.id, self._value, event_args.status, event_args.reason)
            self._on_write_complete_event.notify(self, args)
示例#27
0
class Peer(object):
    """
    Object that represents a BLE-connected (or disconnected) peer
    """
    BLE_CONN_HANDLE_INVALID = BLE_CONN_HANDLE_INVALID
    """ Number of bytes that are header/overhead per MTU when sending a notification or indication """
    NOTIFICATION_INDICATION_OVERHEAD_BYTES = 3

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

    """
    Properties
    """

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

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

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

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

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

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

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

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

    """
    Events
    """

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

        Event Args: None

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

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

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

    """
    Public Methods
    """

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

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

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

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

    """
    Internal Library Methods
    """

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

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

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

        return wrapper

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

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

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

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

    """
    Private Methods
    """

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

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

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

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

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

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

    """
    Public Methods
    """

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

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

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

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

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

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

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

    """
    Properties
    """

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

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

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

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

    """
    Events
    """

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

        EventArgs type: WriteEventArgs

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

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

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

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

        EventArgs type: None

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

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

        EventArgs type: SubscriptionStateChangeEventArgs

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

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

    """
    Event Handling
    """

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

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

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

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

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

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

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

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

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

        # TODO More logic

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

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

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

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

    def _on_disconnect(self, peer, event_args):
        if self.cccd_handle and self.cccd_state != gatt.SubscriptionState.NOT_SUBSCRIBED:
            self.cccd_state = gatt.SubscriptionState.NOT_SUBSCRIBED
示例#29
0
文件: service.py 项目: jiemde/blatann
 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))
示例#30
0
文件: peer.py 项目: jiemde/blatann
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__()