def _query_systemstate(self): """Query the maximum number of connections supported by this adapter """ def status_filter_func(event): if event.command_class == 3 and event.command == 0: return True return False try: response = self._send_command(0, 6, []) maxconn, = unpack("<B", response.payload) except InternalTimeoutError: return False, {'reason': 'Timeout waiting for command response'} events = self._wait_process_events(0.5, status_filter_func, lambda x: False) conns = [] for event in events: handle, flags, addr, addr_type, interval, timeout, lat, bond = unpack( "<BB6sBHHHB", event.payload) if flags != 0: conns.append(handle) return True, {'max_connections': maxconn, 'active_connections': conns}
def notified_header(event): if event.command_class == 4 and event.command == 5: event_handle, att_handle = unpack("<BH", event.payload[0:3]) return event_handle == conn and att_handle == receive_header elif event.command_class == 3 and event.command == 4: event_handle, reason = unpack("<BH", event.payload) return event_handle == conn
def _probe_services(self, handle): """Probe for all primary services and characteristics in those services Args: handle (int): the connection handle to probe """ code = 0x2800 def event_filter_func(event): if (event.command_class == 4 and event.command == 2): event_handle, = unpack("B", event.payload[0:1]) return event_handle == handle return False def end_filter_func(event): if (event.command_class == 4 and event.command == 1): event_handle, = unpack("B", event.payload[0:1]) return event_handle == handle return False payload = struct.pack('<BHHBH', handle, 1, 0xFFFF, 2, code) try: response = self._send_command(4, 1, payload) except InternalTimeoutError: return False, {'reason': 'Timeout waiting for command response'} handle, result = unpack("<BH", response.payload) if result != 0: return False, None events = self._wait_process_events(0.5, event_filter_func, end_filter_func) gatt_events = [x for x in events if event_filter_func(x)] end_events = [x for x in events if end_filter_func(x)] if len(end_events) == 0: return False, None #Make sure we successfully probed the gatt table end_event = end_events[0] _, result, _ = unpack("<BHH", end_event.payload) if result != 0: self._logger.warn( "Error enumerating GATT table, protocol error code = %d (0x%X)" % (result, result)) return False, None services = {} for event in gatt_events: process_gatt_service(services, event) return True, {'services': services}
def _connect(self, address): """Connect to a device given its uuid """ latency = 0 conn_interval_min = 6 conn_interval_max = 100 timeout = 1.0 try: #Allow passing either a binary address or a hex string if isinstance(address, basestring) and len(address) > 6: address = address.replace(':', '') address = bytes(bytearray.fromhex(address)[::-1]) except ValueError: return False, None #Allow simple determination of whether a device has a public or private address #This is not foolproof private_bits = bytearray(address)[-1] >> 6 if private_bits == 0b11: address_type = 1 else: address_type = 0 payload = struct.pack("<6sBHHHH", address, address_type, conn_interval_min, conn_interval_max, int(timeout * 100.0), latency) response = self._send_command(6, 3, payload) result, handle = unpack("<HB", response.payload) if result != 0: return False, None #Now wait for the connection event that says we connected or kill the attempt after timeout def conn_succeeded(event): if event.command_class == 3 and event.command == 0: event_handle, = unpack("B", event.payload[0:1]) return event_handle == handle #FIXME Hardcoded timeout events = self._wait_process_events(4.0, lambda x: False, conn_succeeded) if len(events) != 1: self._stop_scan() return False, None handle, _, addr, _, interval, timeout, latency, _ = unpack( "<BB6sBHHHB", events[0].payload) formatted_addr = ":".join(["%02X" % x for x in bytearray(addr)]) self._logger.info( 'Connected to device %s with interval=%d, timeout=%d, latency=%d', formatted_addr, interval, timeout, latency) connection = {"handle": handle} return True, connection
def _write_handle(self, conn, handle, ack, value, timeout=1.0): """Write to a BLE device characteristic by its handle Args: conn (int): The connection handle for the device we should read from handle (int): The characteristics handle we should read ack (bool): Should this be an acknowledges write or unacknowledged timeout (float): How long to wait before failing value (bytearray): The value that we should write """ conn_handle = conn char_handle = handle def write_handle_acked(event): if event.command_class == 4 and event.command == 1: conn, _, char = unpack("<BHH", event.payload) return conn_handle == conn and char_handle == char data_len = len(value) if data_len > 20: return False, {'reason': 'Data too long to write'} payload = struct.pack("<BHB%ds" % data_len, conn_handle, char_handle, data_len, value) try: if ack: response = self._send_command(4, 5, payload) else: response = self._send_command(4, 6, payload) except InternalTimeoutError: return False, {'reason': 'Timeout waiting for response to command in _write_handle'} _, result = unpack("<BH", response.payload) if result != 0: return False, {'reason': 'Error writing to handle', 'error_code': result} if ack: events = self._wait_process_events(timeout, lambda x: False, write_handle_acked) self._logger.info("Num events in _write_handle: %d", len(events)) if len(events) == 0: return False, {'reason': 'Timeout waiting for acknowledge on write'} _, result, _ = unpack("<BHH", events[0].payload) if result != 0: return False, {'reason': 'Error received during write to handle', 'error_code': result} return True, None
def _disconnect(self, handle): """Disconnect from a device that we have previously connected to """ payload = struct.pack('<B', handle) response = self._send_command(3, 0, payload) conn_handle, result = unpack("<BH", response.payload) if result != 0: self._logger.info("Disconnection failed result=%d", result) return False, None assert conn_handle == handle def disconnect_succeeded(event): if event.command_class == 3 and event.command == 4: event_handle, = unpack("B", event.payload[0:1]) return event_handle == handle return False #FIXME Hardcoded timeout events = self._wait_process_events(3.0, lambda x: False, disconnect_succeeded) if len(events) != 1: return False, None return True, {'handle': handle}
def process_read_handle(event): length = len(event.payload) - 5 conn, att_handle, att_type, act_length, value = unpack("<BHBB%ds" % length, event.payload) assert act_length == length return att_type, bytearray(value)
def _send_notification(self, handle, value): """Send a notification to all connected clients on a characteristic Args: handle (int): The handle we wish to notify on value (bytearray): The value we wish to send """ value_len = len(value) value = bytes(value) payload = struct.pack("<BHB%ds" % value_len, 0xFF, handle, value_len, value) response = self._send_command(2, 5, payload) result, = unpack("<H", response.payload) if result != 0: return False, { 'reason': 'Error code from BLED112 notifying a value', 'code': result, 'handle': handle, 'value': value } return True, None
def _read_handle(self, conn, handle, timeout=1.0): conn_handle = conn payload = struct.pack("<BH", conn_handle, handle) try: response = self._send_command(4, 4, payload) ignored_handle, result = unpack("<BH", response.payload) except InternalTimeoutError: return False, {'reason': 'Timeout sending read handle command'} if result != 0: self._logger.warn("Error reading handle %d, result=%d" % (handle, result)) return False, None def handle_value_func(event): if (event.command_class == 4 and event.command == 5): event_handle, = unpack("B", event.payload[0:1]) return event_handle == conn_handle def handle_error_func(event): if (event.command_class == 4 and event.command == 1): event_handle, = unpack("B", event.payload[0:1]) return event_handle == conn_handle events = self._wait_process_events(5.0, lambda x: False, lambda x: handle_value_func(x) or handle_error_func(x)) if len(events) != 1: return False, None if handle_error_func(events[0]): return False, None handle_event = events[0] handle_type, handle_data = process_read_handle(handle_event) return True, {'type': handle_type, 'data': handle_data}
def parse_characteristic_declaration(value): length = len(value) if length == 5: uuid_len = 2 elif length == 19: uuid_len = 16 else: raise ValueError( "Value has improper length for ble characteristic definition, length was %d" % len(value)) propval, handle, uuid = unpack("<BH%ds" % uuid_len, value) #Process the properties properties = CharacteristicProperties(bool(propval & 0x1), bool(propval & 0x2), bool(propval & 0x4), bool(propval & 0x8), bool(propval & 0x10), bool(propval & 0x20), bool(propval & 0x40), bool(propval & 0x80)) uuid = process_uuid(uuid) char = {} char['uuid'] = uuid char['properties'] = properties char['handle'] = handle return char
def ReportLength(cls, header): """Given a header of HeaderLength bytes, calculate the size of this report""" first_word, = unpack("<L", header[:4]) length = (first_word >> 8) return length
def _probe_characteristics(self, conn, services, timeout=5.0): """Probe gatt services for all associated characteristics in a BLE device Args: conn (int): the connection handle to probe services (dict): a dictionary of services produced by probe_services() timeout (float): the maximum number of seconds to spend in any single task """ for service in viewvalues(services): success, result = self._enumerate_handles(conn, service['start_handle'], service['end_handle']) if not success: return False, None attributes = result['attributes'] service['characteristics'] = {} last_char = None for handle, attribute in viewitems(attributes): if attribute['uuid'].hex[-4:] == '0328': success, result = self._read_handle(conn, handle, timeout) if not success: return False, None value = result['data'] char = parse_characteristic_declaration(value) service['characteristics'][char['uuid']] = char last_char = char elif attribute['uuid'].hex[-4:] == '0229': if last_char is None: return False, None success, result = self._read_handle(conn, handle, timeout) if not success: return False, None value = result['data'] assert len(value) == 2 value, = unpack("<H", value) last_char['client_configuration'] = { 'handle': handle, 'value': value } return True, {'services': services}
def _process_scan_event(self, response): """Parse the BLE advertisement packet. If it's an IOTile device, parse and add to the scanned devices. Then, parse advertisement and determine if it matches V1 or V2. There are two supported type of advertisements: v1: There is both an advertisement and a scan response (if active scanning is enabled). v2: There is only an advertisement and no scan response. """ payload = response.payload length = len(payload) - 10 if length < 0: return rssi, packet_type, sender, _addr_type, _bond, data = unpack( "<bB6sBB%ds" % length, payload) string_address = ':'.join( [format(x, "02X") for x in bytearray(sender[::-1])]) # Scan data is prepended with a length if len(data) > 0: data = bytearray(data[1:]) else: data = bytearray([]) self._scan_event_count += 1 # If this is an advertisement packet, see if its an IOTile device # packet_type = 4 is scan_response, 0, 2 and 6 are advertisements if packet_type in (0, 2, 6): if len(data) != 31: return if data[22] == 0xFF and data[23] == 0xC0 and data[24] == 0x3: self._v1_scan_count += 1 self._parse_v1_advertisement(rssi, string_address, data) elif data[3] == 27 and data[4] == 0x16 and data[ 5] == 0xdd and data[6] == 0xfd: self._v2_scan_count += 1 self._parse_v2_advertisement(rssi, string_address, data) else: pass # This just means the advertisement was from a non-IOTile device elif packet_type == 4: self._v1_scan_response_count += 1 self._parse_v1_scan_response(string_address, data)
def process_gatt_service(services, event): """Process a BGAPI event containing a GATT service description and add it to a dictionary Args: services (dict): A dictionary of discovered services that is updated with this event event (BGAPIPacket): An event containing a GATT service """ length = len(event.payload) - 5 handle, start, end, uuid = unpack('<BHH%ds' % length, event.payload) uuid = process_uuid(uuid) services[uuid] = {'uuid_raw': uuid, 'start_handle': start, 'end_handle': end}
def _set_advertising_data(self, packet_type, data): """Set the advertising data for advertisements sent out by this bled112 Args: packet_type (int): 0 for advertisement, 1 for scan response data (bytearray): the data to set """ payload = struct.pack("<BB%ss" % (len(data)), packet_type, len(data), bytes(data)) response = self._send_command(6, 9, payload) result, = unpack("<H", response.payload) if result != 0: return False, {'reason': 'Error code from BLED112 setting advertising data', 'code': result} return True, None
def _parse_v2_advertisement(self, rssi, sender, data): """ Parse the IOTile Specific advertisement packet""" if len(data) != 31: return None, None, None, None, None, None, None # We have already verified that the device is an IOTile device # by checking its service data uuid in _process_scan_event so # here we just parse out the required information device_id, reboot_low, reboot_high_packed, flags, timestamp, \ battery, counter_packed, broadcast_stream_packed, broadcast_value, \ _mac = unpack("<LHBBLBBHLL", data[7:]) reboots = (reboot_high_packed & 0xF) << 16 | reboot_low counter = counter_packed & ((1 << 5) - 1) broadcast_multiplex = counter_packed >> 5 broadcast_toggle = broadcast_stream_packed >> 15 broadcast_stream = broadcast_stream_packed & ((1 << 15) - 1) # Flags for version 2 are: # bit 0: Has pending data to stream # bit 1: Low voltage indication # bit 2: User connected # bit 3: Broadcast data is encrypted # bit 4: Encryption key is device key # bit 5: Encryption key is user key # bit 6: broadcast data is time synchronized to avoid leaking # information about when it changes self._device_scan_counts.setdefault(device_id, {'v1': 0, 'v2': 0})['v2'] += 1 info = {'connection_string': sender, 'uuid': device_id, 'pending_data': bool(flags & (1 << 0)), 'low_voltage': bool(flags & (1 << 1)), 'user_connected': bool(flags & (1 << 2)), 'signal_strength': rssi, 'reboot_counter': reboots, 'sequence': counter, 'broadcast_toggle': broadcast_toggle, 'timestamp': timestamp, 'battery': battery / 32.0, 'advertising_version':2} return info, timestamp, broadcast_stream, broadcast_value, \ broadcast_toggle, counter, broadcast_multiplex
def rpc(self, feature, cmd, *args, **kw): """ Send an RPC call to this module, interpret the return value according to the result_type kw argument. Unless raise keyword is passed with value False, raise an RPCException if the command is not successful. """ if 'arg_format' in kw: packed_args = struct.pack("<{}".format(kw['arg_format']), *args) status, payload = self.stream.send_rpc(self.addr, feature, cmd, packed_args, **kw) else: status, payload = self.stream.send_rpc(self.addr, feature, cmd, *args, **kw) unpack_flag = False if "result_type" in kw: res_type = kw['result_type'] elif "result_format" in kw: unpack_flag = True res_type = (0, True) else: res_type = (0, False) try: res = self._parse_rpc_result(status, payload, *res_type, command=(feature << 8) | cmd) if unpack_flag: return unpack("<%s" % kw["result_format"], res['buffer']) return res except ModuleBusyError: pass if "retries" not in kw: kw['retries'] = 10 #Sleep 100 ms and try again unless we've exhausted our retry attempts if kw["retries"] > 0: kw['retries'] -= 1 sleep(0.1) return self.rpc(feature, cmd, *args, **kw)
def _set_mode(self, discover_mode, connect_mode): """Set the mode of the BLED112, used to enable and disable advertising To enable advertising, use 4, 2. To disable advertising use 0, 0. Args: discover_mode (int): The discoverability mode, 0 for off, 4 for on (user data) connect_mode (int): The connectability mode, 0 for of, 2 for undirected connectable """ payload = struct.pack("<BB", discover_mode, connect_mode) response = self._send_command(6, 1, payload) result, = unpack("<H", response.payload) if result != 0: return False, {'reason': 'Error code from BLED112 setting mode', 'code': result} return True, None
def _parse_v1_advertisement(self, rssi, sender, advert): if len(advert) != 31: return # Make sure the scan data comes back with an incomplete UUID list if advert[3] != 17 or advert[4] != 6: return # Make sure the uuid is our tilebus UUID if advert[5:21] == TileBusService.bytes_le: # Now parse out the manufacturer specific data manu_data = advert[21:] _length, _datatype, _manu_id, device_uuid, flags = unpack( "<BBHLH", manu_data) self._device_scan_counts.setdefault(device_uuid, { 'v1': 0, 'v2': 0 })['v1'] += 1 # Flags for version 1 are: # bit 0: whether we have pending data # bit 1: whether we are in a low voltage state # bit 2: whether another user is connected # bit 3: whether we support robust reports # bit 4: whether we allow fast writes info = { 'connection_string': sender, 'uuid': device_uuid, 'pending_data': bool(flags & (1 << 0)), 'low_voltage': bool(flags & (1 << 1)), 'user_connected': bool(flags & (1 << 2)), 'signal_strength': rssi, 'advertising_version': 1 } if self._active_scan: self.partial_scan_responses[sender] = info else: self._trigger_callback('on_scan', self.id, info, self.ExpirationTime)
def _parse_scan_response(self, response): """ Parse the BLE advertisement packet. If it's an IOTile device, parse and add to the scanned devices. Then, parse advertisement and determine if it matches V1 or V2. """ payload = response.payload length = len(payload) - 10 if length < 0: return rssi, packet_type, sender, addr_type, bond, data = unpack( "<bB6sBB%ds" % length, payload) # Scan data is prepended with a length if len(data) > 0: scan_data = bytearray(data[1:]) else: scan_data = bytearray([]) # If this is an advertisement response, see if its an IOTile device if packet_type in (0, 6): if (len(scan_data) > 4 and scan_data[3] == 17 and scan_data[4] == 6): self._parse_v1_scan_response(response) # See if the data length is 27 (0x1B), service = 0x16, ArchUUID = 0x03C0 elif (len(scan_data) > 6 and scan_data[3] == 27 and scan_data[4] == 0x16 and scan_data[5] == 0xc0 and scan_data[6] == 0x03): self._parse_v2_scan_response(response) else: return else: self._parse_v1_scan_response(response) return
def decode(self): """Decode this report into a single reading """ fmt, _, stream, uuid, sent_timestamp, reading_timestamp, reading_value = unpack( "<BBHLLLL", self.raw_report) assert fmt == 0 #Estimate the UTC time when this device was turned on time_base = self.received_time - datetime.timedelta( seconds=sent_timestamp) reading = IOTileReading(reading_timestamp, stream, reading_value, time_base=time_base) self.origin = uuid self.sent_timestamp = sent_timestamp return [reading], []
def _parse_scan_response(self, response): """Parse the IOTile specific data structures in the BLE advertisement packets and add the device to our list of scanned devices Parse advertisement and determine if it matches V1 or V2. """ payload = response.payload length = len(payload) - 10 if length < 0: return rssi, packet_type, sender, addr_type, bond, data = unpack("<bB6sBB%ds" % length, payload) #Scan data is prepended with a length if len(data) > 0: scan_data = bytearray(data[1:]) else: scan_data = bytearray([]) #If this is an advertisement response, see if its an IOTile device if packet_type == 0 or packet_type == 6: if (scan_data[3] == 17 and scan_data[4] == 6): self._parse_v1_scan_response(response) # See if the data length is 27 (0x1B), service = 0x16, ArchUUID = 0x03C0 elif (scan_data[3] == 27 and scan_data[4] == 0x16 and scan_data[5] == 0xc0 and scan_data[6] == 0x03): self._parse_v2_scan_response(response) else: self._logger.error("Invalid scan data: {0}".format(binascii.hexlify(scan_data))) else: self._parse_v1_scan_response(response) return
def _enumerate_handles(self, conn, start_handle, end_handle, timeout=1.0): conn_handle = conn def event_filter_func(event): if event.command_class == 4 and event.command == 4: event_handle, = unpack("B", event.payload[0:1]) return event_handle == conn_handle return False def end_filter_func(event): if event.command_class == 4 and event.command == 1: event_handle, = unpack("B", event.payload[0:1]) return event_handle == conn_handle return False payload = struct.pack("<BHH", conn_handle, start_handle, end_handle) try: response = self._send_command(4, 3, payload) handle, result = unpack("<BH", response.payload) except InternalTimeoutError: return False, {'reason': "Timeout enumerating handles"} if result != 0: return False, None events = self._wait_process_events(timeout, event_filter_func, end_filter_func) handle_events = [x for x in events if event_filter_func(x)] attrs = {} for event in handle_events: process_attribute(attrs, event) return True, {'attributes': attrs}
def process_attribute(attributes, event): length = len(event.payload) - 3 handle, chrhandle, uuid = unpack("<BH%ds" % length, event.payload) uuid = process_uuid(uuid) attributes[chrhandle] = {'uuid': uuid}
def handle_error_func(event): if (event.command_class == 4 and event.command == 1): event_handle, = unpack("B", event.payload[0:1]) return event_handle == conn_handle
def decode(self): """Decode this report into a list of readings """ fmt, len_low, len_high, device_id, report_id, sent_timestamp, signature_flags, origin_streamer, streamer_selector = unpack("<BBHLLLBBH", self.raw_report[:20]) assert fmt == 1 length = (len_high << 8) | len_low self.origin = device_id self.report_id = report_id self.sent_timestamp = sent_timestamp self.origin_streamer = origin_streamer self.streamer_selector = streamer_selector self.signature_flags = signature_flags assert len(self.raw_report) == length remaining = self.raw_report[20:] assert len(remaining) >= 24 readings = remaining[:-24] footer = remaining[-24:] lowest_id, highest_id, signature = unpack("<LL16s", footer) signature = bytearray(signature) self.lowest_id = lowest_id self.highest_id = highest_id self.signature = signature signed_data = self.raw_report[:-16] signer = ChainedAuthProvider() if signature_flags == AuthProvider.NoKey: self.encrypted = False else: self.encrypted = True try: verification = signer.verify_report(device_id, signature_flags, signed_data, signature, report_id=report_id, sent_timestamp=sent_timestamp) self.verified = verification['verified'] except NotFoundError: self.verified = False # If we were not able to verify the report, do not try to parse or decrypt it since we # can't guarantee who it came from. if not self.verified: return [], [] # If the report is encrypted, try to decrypt it before parsing the readings if self.encrypted: try: result = signer.decrypt_report(device_id, signature_flags, readings, report_id=report_id, sent_timestamp=sent_timestamp) readings = result['data'] except NotFoundError: return [], [] # Now parse all of the readings # Make sure this report has an integer number of readings assert (len(readings) % 16) == 0 time_base = self.received_time - datetime.timedelta(seconds=sent_timestamp) parsed_readings = [] for i in range(0, len(readings), 16): reading = readings[i:i+16] stream, _, reading_id, timestamp, value = unpack("<HHLLL", reading) parsed = IOTileReading(timestamp, stream, value, time_base=time_base, reading_id=reading_id) parsed_readings.append(parsed) return parsed_readings, []
def disconnect_succeeded(event): if event.command_class == 3 and event.command == 4: event_handle, = unpack("B", event.payload[0:1]) return event_handle == handle return False
def write_handle_acked(event): if event.command_class == 4 and event.command == 1: conn, _, char = unpack("<BHH", event.payload) return conn_handle == conn and char_handle == char
def conn_succeeded(event): if event.command_class == 3 and event.command == 0: event_handle, = unpack("B", event.payload[0:1]) return event_handle == handle
def notified_payload(event): if event.command_class == 4 and event.command == 5: event_handle, att_handle = unpack("<BH", event.payload[0:3]) return event_handle == conn and att_handle == receive_payload