From 839fb34206e2349ab94a8bf7c1606fcbe531ff62 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Fri, 31 May 2019 17:54:47 -0400 Subject: [PATCH 01/19] Do server advertisement construction in Python; fix numerous bugs --- adafruit_ble/advertising.py | 59 +++++++++++++++++++++++++++++++++---- adafruit_ble/beacon.py | 36 +++++++++++----------- adafruit_ble/uart.py | 5 ++-- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index 363de06..4f6a8b5 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -31,7 +31,7 @@ import struct -class AdvertisingData: +class Advertisement: """Build up a BLE advertising data packet.""" # BR/EDR flags not included here, since we don't support BR/EDR. FLAG_LIMITED_DISCOVERY = 0x01 @@ -53,7 +53,7 @@ class AdvertisingData: """Complete list of 128 bit service UUIDs.""" SHORT_LOCAL_NAME = 0x08 """Short local device name (shortened to fit).""" - COMPLETE_LOCALNAME = 0x09 + COMPLETE_LOCAL_NAME = 0x09 """Complete local device name.""" TX_POWER = 0x0A """Transmit power level""" @@ -87,9 +87,13 @@ def __init__(self, flags=(FLAG_GENERAL_DISCOVERY | FLAG_LE_ONLY), max_length=MAX self._max_length = max_length self._check_length() + @property + def bytes_remaining(self): + return self._max_length - len(self.data) + def _check_length(self): if len(self.data) > self._max_length: - raise IndexError("Advertising data exceeds max_length") + raise IndexError("Advertising data too long") def add_field(self, field_type, field_data): """Append an advertising data field to the current packet, of the given type. @@ -101,12 +105,57 @@ def add_field(self, field_type, field_data): def add_16_bit_uuids(self, uuids): """Add a complete list of 16 bit service UUIDs.""" - self.add_field(self.ALL_16_BIT_SERVICE_UUIDS, bytes(uuid.uuid16 for uuid in uuids)) + for uuid in uuids: + self.add_field(self.ALL_16_BIT_SERVICE_UUIDS, struct.pack("= len(name_bytes): + adv.add_field(Advertisement.COMPLETE_LOCAL_NAME, name_bytes) + else: + adv.add_field(Advertisement.SHORT_LOCAL_NAME, name_bytes[:bytes_available]) + + self._advertisement = adv + + @property + def data(self): + return self._advertisement.data diff --git a/adafruit_ble/beacon.py b/adafruit_ble/beacon.py index c5a9e3b..5c813de 100644 --- a/adafruit_ble/beacon.py +++ b/adafruit_ble/beacon.py @@ -32,22 +32,22 @@ import struct import bleio -from .advertising import AdvertisingData +from .advertising import Advertisement class Beacon: """Base class for Beacon advertisers.""" - def __init__(self, advertising_data, interval=1.0): - """Set up a beacon with the given AdvertisingData. + def __init__(self, advertisement, interval=1.0): + """Set up a beacon with the given Advertisement. - :param AdvertisingData advertising_data: The advertising packet + :param Advertisement advertisement: The advertising packet :param float interval: Advertising interval in seconds """ self.broadcaster = bleio.Broadcaster(interval) - self.advertising_data = advertising_data + self.advertisement = advertisement def start(self): """Turn on beacon.""" - self.broadcaster.start_advertising(self.advertising_data.data) + self.broadcaster.start_advertising(self.advertisement.data) def stop(self): """Turn off beacon.""" @@ -81,8 +81,8 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0): b.start() """ - adv_data = AdvertisingData() - adv_data.add_mfr_specific_data( + adv = Advertisement() + adv.add_mfr_specific_data( company_id, b''.join(( # 0x02 means a beacon. 0x15 (=21) is length (16 + 2 + 2 + 1) @@ -91,8 +91,8 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0): # iBeacon and similar expect big-endian UUIDS. Usually they are little-endian. bytes(reversed(uuid.uuid128)), # major and minor are big-endian. - struct.pack(">HHB", major, minor, rssi)))) - super().__init__(adv_data, interval=interval) + struct.pack(">HHb", major, minor, rssi)))) + super().__init__(adv, interval=interval) class EddystoneURLBeacon(Beacon): @@ -134,8 +134,8 @@ def __init__(self, url, tx_power=0, interval=1.0): :param float interval: Advertising interval in seconds """ - adv_data = AdvertisingData() - adv_data.add_field(AdvertisingData.ALL_16_BIT_SERVICE_UUIDS, self._EDDYSTONE_ID) + adv = Advertisement() + adv.add_field(Advertisement.ALL_16_BIT_SERVICE_UUIDS, self._EDDYSTONE_ID) short_url = None for idx, prefix in enumerate(self._URL_SCHEMES): if url.startswith(prefix): @@ -148,9 +148,9 @@ def __init__(self, url, tx_power=0, interval=1.0): short_url = short_url.replace(subst + '/', chr(code)) for code, subst in enumerate(self._SUBSTITUTIONS, 7): short_url = short_url.replace(subst, chr(code)) - adv_data.add_field(AdvertisingData.SERVICE_DATA_16_BIT_UUID, - b''.join((self._EDDYSTONE_ID, - b'\x10', - struct.pack(" Date: Sun, 2 Jun 2019 23:22:45 -0400 Subject: [PATCH 02/19] 1. To accomodate scan response packets: Rename Advertisement to AdvertisingPacket. Make adding advertising flags is now done explicitly by callers. 2. ServerAdvertisement creates a scan response packet to send the the full name, if it doesn't fit in the initial advertising data packet. 3. Allow UARTServer to specify peripheral name. --- adafruit_ble/advertising.py | 83 +++++++++++++++++++++++++------------ adafruit_ble/beacon.py | 28 +++++++------ adafruit_ble/uart.py | 9 ++-- 3 files changed, 77 insertions(+), 43 deletions(-) diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index 4f6a8b5..799794e 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -31,8 +31,8 @@ import struct -class Advertisement: - """Build up a BLE advertising data packet.""" +class AdvertisingPacket: + """Build up a BLE advertising data or scan response packet.""" # BR/EDR flags not included here, since we don't support BR/EDR. FLAG_LIMITED_DISCOVERY = 0x01 """Discoverable only for a limited time period.""" @@ -81,28 +81,38 @@ class Advertisement: MAX_DATA_SIZE = 31 """Data size in a regular BLE packet.""" - def __init__(self, flags=(FLAG_GENERAL_DISCOVERY | FLAG_LE_ONLY), max_length=MAX_DATA_SIZE): - """Initalize an advertising packet, with the given flags, no larger than max_length.""" - self.data = bytearray((2, self.FLAGS, flags)) + def __init__(self, *, max_length=MAX_DATA_SIZE): + """Create an empty advertising packet, no larger than max_length.""" + self._packet_bytes = bytearray() self._max_length = max_length self._check_length() + @property + def packet_bytes(self): + """The raw packet bytes.""" + return self._packet_bytes + @property def bytes_remaining(self): - return self._max_length - len(self.data) + """Number of bytes still available for use in the packet.""" + return self._max_length - len(self._packet_bytes) def _check_length(self): - if len(self.data) > self._max_length: + if len(self._packet_bytes) > self._max_length: raise IndexError("Advertising data too long") def add_field(self, field_type, field_data): """Append an advertising data field to the current packet, of the given type. The length field is calculated from the length of field_data.""" - self.data.append(1 + len(field_data)) - self.data.append(field_type) - self.data.extend(field_data) + self._packet_bytes.append(1 + len(field_data)) + self._packet_bytes.append(field_type) + self._packet_bytes.extend(field_data) self._check_length() + def add_flags(self, flags=(FLAG_GENERAL_DISCOVERY | FLAG_LE_ONLY)): + """Add default or custom advertising flags.""" + self.add_field(self.FLAGS, struct.pack("= len(name_bytes): - adv.add_field(Advertisement.COMPLETE_LOCAL_NAME, name_bytes) + packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME, name_bytes) else: - adv.add_field(Advertisement.SHORT_LOCAL_NAME, name_bytes[:bytes_available]) + packet.add_field(AdvertisingPacket.SHORT_LOCAL_NAME, name_bytes[:bytes_available]) + self._scan_response_packet = AdvertisingPacket() + try: + self._scan_response_packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME, + name_bytes) + except IndexError: + raise IndexError("Name too long") - self._advertisement = adv + self._advertising_data_packet = packet + + @property + def advertising_data_bytes(self): + """The raw bytes for the initial advertising data packet.""" + return self._advertising_data_packet.packet_bytes @property - def data(self): - return self._advertisement.data + def scan_response_bytes(self): + """The raw bytes for the scan response packet. None if there is no response packet.""" + if self._scan_response_packet: + return self._scan_response_packet.packet_bytes + return None diff --git a/adafruit_ble/beacon.py b/adafruit_ble/beacon.py index 5c813de..53de293 100644 --- a/adafruit_ble/beacon.py +++ b/adafruit_ble/beacon.py @@ -32,26 +32,26 @@ import struct import bleio -from .advertising import Advertisement +from .advertising import AdvertisingPacket class Beacon: """Base class for Beacon advertisers.""" - def __init__(self, advertisement, interval=1.0): - """Set up a beacon with the given Advertisement. + def __init__(self, advertising_packet, interval=1.0): + """Set up a beacon with the given AdvertisingPacket. - :param Advertisement advertisement: The advertising packet + :param AdvertisingPacket advertising_packet :param float interval: Advertising interval in seconds """ - self.broadcaster = bleio.Broadcaster(interval) - self.advertisement = advertisement + self._broadcaster = bleio.Broadcaster(interval) + self._advertising_packet = advertising_packet def start(self): """Turn on beacon.""" - self.broadcaster.start_advertising(self.advertisement.data) + self._broadcaster.start_advertising(self._advertising_packet.packet_bytes) def stop(self): """Turn off beacon.""" - self.broadcaster.stop_advertising() + self._broadcaster.stop_advertising() @@ -81,7 +81,8 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0): b.start() """ - adv = Advertisement() + adv = AdvertisingPacket() + adv.add_flags() adv.add_mfr_specific_data( company_id, b''.join(( @@ -131,11 +132,12 @@ def __init__(self, url, tx_power=0, interval=1.0): :param url URL to encode. Must be short enough to fit after encoding. :param int tx_power: transmit power in dBm at 0 meters (8 bit signed value) - :param float interval: Advertising interval in seconds + :param float interval: advertising interval in seconds """ - adv = Advertisement() - adv.add_field(Advertisement.ALL_16_BIT_SERVICE_UUIDS, self._EDDYSTONE_ID) + adv = AdvertisingPacket() + adv.add_flags() + adv.add_field(AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS, self._EDDYSTONE_ID) short_url = None for idx, prefix in enumerate(self._URL_SCHEMES): if url.startswith(prefix): @@ -148,7 +150,7 @@ def __init__(self, url, tx_power=0, interval=1.0): short_url = short_url.replace(subst + '/', chr(code)) for code, subst in enumerate(self._SUBSTITUTIONS, 7): short_url = short_url.replace(subst, chr(code)) - adv.add_field(Advertisement.SERVICE_DATA_16_BIT_UUID, + adv.add_field(AdvertisingPacket.SERVICE_DATA_16_BIT_UUID, b''.join((self._EDDYSTONE_ID, b'\x10', struct.pack(" Date: Mon, 3 Jun 2019 20:39:11 -0400 Subject: [PATCH 03/19] Replace Broadcaster with Peripheral --- adafruit_ble/beacon.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/adafruit_ble/beacon.py b/adafruit_ble/beacon.py index 53de293..0976e2f 100644 --- a/adafruit_ble/beacon.py +++ b/adafruit_ble/beacon.py @@ -40,14 +40,16 @@ def __init__(self, advertising_packet, interval=1.0): """Set up a beacon with the given AdvertisingPacket. :param AdvertisingPacket advertising_packet - :param float interval: Advertising interval in seconds """ - self._broadcaster = bleio.Broadcaster(interval) + self._broadcaster = bleio.Peripheral(name=None) self._advertising_packet = advertising_packet - def start(self): - """Turn on beacon.""" - self._broadcaster.start_advertising(self._advertising_packet.packet_bytes) + def start(self, interval=1.0): + """Turn on beacon. + + :param float interval: Advertising interval in seconds + """ + self._broadcaster.start_advertising(self._advertising_packet.packet_bytes, interval=interval) def stop(self): """Turn off beacon.""" @@ -60,7 +62,7 @@ class LocationBeacon(Beacon): Used for Apple iBeacon, Nordic nRF Beacon, etc. """ # pylint: disable=too-many-arguments - def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0): + def __init__(self, company_id, uuid, major, minor, rssi): """Create a beacon with the given values. :param int company_id: 16-bit company id designating beacon specification owner @@ -69,7 +71,6 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0): :param int major: 16-bit major number, such as a store number :param int minor: 16-bit minor number, such as a location within a store :param int rssi: Signal strength in dBm at 1m (signed 8-bit value) - :param float interval: Advertising interval in seconds Example:: @@ -93,7 +94,7 @@ def __init__(self, company_id, uuid, major, minor, rssi, interval=1.0): bytes(reversed(uuid.uuid128)), # major and minor are big-endian. struct.pack(">HHb", major, minor, rssi)))) - super().__init__(adv, interval=interval) + super().__init__(adv) class EddystoneURLBeacon(Beacon): @@ -127,12 +128,11 @@ class EddystoneURLBeacon(Beacon): '.gov', ) - def __init__(self, url, tx_power=0, interval=1.0): + def __init__(self, url, tx_power=0): """Create a URL beacon with an encoded version of the url and a transmit power. :param url URL to encode. Must be short enough to fit after encoding. :param int tx_power: transmit power in dBm at 0 meters (8 bit signed value) - :param float interval: advertising interval in seconds """ adv = AdvertisingPacket() @@ -155,4 +155,4 @@ def __init__(self, url, tx_power=0, interval=1.0): b'\x10', struct.pack(" Date: Fri, 7 Jun 2019 23:02:42 -0400 Subject: [PATCH 04/19] Add Scanner: WIP --- adafruit_ble/beacon.py | 5 +- adafruit_ble/scanner.py | 151 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 adafruit_ble/scanner.py diff --git a/adafruit_ble/beacon.py b/adafruit_ble/beacon.py index 0976e2f..c7891cd 100644 --- a/adafruit_ble/beacon.py +++ b/adafruit_ble/beacon.py @@ -36,7 +36,7 @@ class Beacon: """Base class for Beacon advertisers.""" - def __init__(self, advertising_packet, interval=1.0): + def __init__(self, advertising_packet): """Set up a beacon with the given AdvertisingPacket. :param AdvertisingPacket advertising_packet @@ -49,7 +49,8 @@ def start(self, interval=1.0): :param float interval: Advertising interval in seconds """ - self._broadcaster.start_advertising(self._advertising_packet.packet_bytes, interval=interval) + self._broadcaster.start_advertising(self._advertising_packet.packet_bytes, + interval=interval) def stop(self): """Turn off beacon.""" diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py new file mode 100644 index 0000000..7e9d1f6 --- /dev/null +++ b/adafruit_ble/scanner.py @@ -0,0 +1,151 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Dan Halbert for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_ble.scanner` +==================================================== + +UART-style communication. + +* Author(s): Dan Halbert for Adafruit Industries + +""" +import struct + +import bleio.ScanEntry +import bleio.Scanner +import bleio.UUID + +from .advertising import AdvertisingPacket + +class Scanner: + """ + Scan for BLE device advertisements for a period of time. Return what was received. + + Example:: + + from adafruit_ble.scanner import Scanner + scanner = Scanner() + scanner.Scan + + # Wait for a connection. + while not uart.connected: + pass + + uart.write('abc') + """ + + def __init__(self): + self._scanner = bleio.Scanner() + + def scan(self, timeout, *, interval=0.1, window=0.1): + """Scan for advertisements from BLE devices. + + :param int timeout: how long to scan for (in seconds) + :param float interval: the interval (in seconds) between the start + of two consecutive scan windows. + Must be in the range 0.0025 - 40.959375 seconds. + :param float window: the duration (in seconds) to scan a single BLE channel. + `window` must be <= `interval`. + :returns: advertising packets found + :rtype: list of `adafruit_ble.ScanEntry` objects + """ + return [ScanEntry(entry) for entry in self._scanner.scan(timeout, interval, window)] + + +class ScanEntry: + """ + Information about a single transmission of data from a BLE device received by a `Scanner`. + + :param bleio.ScanEntry entry: lower-level ScanEntry from a `bleio.Scanner`. + + This constructor would normally only be used by `Scanner`. + """ + + def __init__(self, entry): + self._bleio_entry = entry + + def item(self, item_type): + """Return the bytes in the advertising packet for given the element type. + + :param int element_type: An integer designating an element type. + A number are defined in `AdvertisingPacket`, such as `AdvertisingPacket.TX_POWER`. + :returns: bytes that are the value for the given element type. + If the element type is not present in the packet, return ``None``. + """ + i = 0 + adv_bytes = self.advertisement_bytes + while i < len(adv_bytes): + item_length = adv_bytes[i] + if item_type != adv_bytes[i+1]: + # Type doesn't match: skip to next item + i += item_length + 1 + else: + return adv_bytes[i + 2:i + item_length] + return None + + @property + def advertisement_bytes(self): + """The raw bytes of the received advertisement.""" + return self._bleio_entry.raw_data + + @property + def rssi(self): + """The signal strength of the device at the time of the scan. (read-only).""" + return self._bleio_entry.rssi + + @property + def address(self): + """The address of the device. (read-only).""" + return self._bleio_entry.address + + @property + def name(self): + """The name of the device. (read-only)""" + name = self.item(AdvertisingPacket.COMPLETE_LOCAL_NAME) + return name if name else self.item(AdvertisingPacket.SHORT_LOCAL_NAME) + + @property + def service_uuids(self): + """List of all the service UUIDs in the advertisement.""" + concat_uuids = self.item(AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS) + concat_uuids = concat_uuids if concat_uuids else self.item( + AdvertisingPacket.SOME_16_BIT_SERVICE_UUIDS) + + uuid_values = [] + if concat_uuids: + for i in range(0, len(uuid_values), 2): + uuid_values.append(struct.unpack(" Date: Mon, 10 Jun 2019 07:19:06 -0400 Subject: [PATCH 05/19] WIP --- adafruit_ble/scanner.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index 7e9d1f6..a0646f4 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -73,11 +73,10 @@ def scan(self, timeout, *, interval=0.1, window=0.1): class ScanEntry: """ - Information about a single transmission of data from a BLE device received by a `Scanner`. + Information about an advertising packet from a BLE device received by a `Scanner`. - :param bleio.ScanEntry entry: lower-level ScanEntry from a `bleio.Scanner`. - - This constructor would normally only be used by `Scanner`. + :param bleio.ScanEntry entry: lower-level ScanEntry returned from `bleio.Scanner`. + This constructor is normally used only `Scanner`. """ def __init__(self, entry): @@ -96,7 +95,7 @@ def item(self, item_type): while i < len(adv_bytes): item_length = adv_bytes[i] if item_type != adv_bytes[i+1]: - # Type doesn't match: skip to next item + # Type doesn't match: skip to next item. i += item_length + 1 else: return adv_bytes[i + 2:i + item_length] From 4710b1f3cee84086e6bc9c428f6ce843a75767b1 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 17 Jun 2019 23:17:31 -0400 Subject: [PATCH 06/19] Scanner fixes --- adafruit_ble/scanner.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index a0646f4..cd09e94 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -30,9 +30,7 @@ """ import struct -import bleio.ScanEntry -import bleio.Scanner -import bleio.UUID +import bleio from .advertising import AdvertisingPacket @@ -68,7 +66,7 @@ def scan(self, timeout, *, interval=0.1, window=0.1): :returns: advertising packets found :rtype: list of `adafruit_ble.ScanEntry` objects """ - return [ScanEntry(entry) for entry in self._scanner.scan(timeout, interval, window)] + return [ScanEntry(entry) for entry in self._scanner.scan(timeout, interval=interval, window=window)] class ScanEntry: From 121a070905735c5c9d6ced6d238ce1987910e5ad Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 22 Jun 2019 22:09:55 -0400 Subject: [PATCH 07/19] scanner work --- adafruit_ble/scanner.py | 44 +++++++++++++++++++++++++++++------------ adafruit_ble/uuid.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 adafruit_ble/uuid.py diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index cd09e94..117afe7 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -23,7 +23,7 @@ `adafruit_ble.scanner` ==================================================== -UART-style communication. +Scan for nearby BLE Devices * Author(s): Dan Halbert for Adafruit Industries @@ -63,22 +63,21 @@ def scan(self, timeout, *, interval=0.1, window=0.1): Must be in the range 0.0025 - 40.959375 seconds. :param float window: the duration (in seconds) to scan a single BLE channel. `window` must be <= `interval`. - :returns: advertising packets found - :rtype: list of `adafruit_ble.ScanEntry` objects - """ - return [ScanEntry(entry) for entry in self._scanner.scan(timeout, interval=interval, window=window)] + :returns a list of `adafruit_ble.ScanEntry` objects. + """ + return [ScanEntry(e) for e in self._scanner.scan(timeout, interval=interval, window=window)] class ScanEntry: """ Information about an advertising packet from a BLE device received by a `Scanner`. - :param bleio.ScanEntry entry: lower-level ScanEntry returned from `bleio.Scanner`. - This constructor is normally used only `Scanner`. + :param bleio.ScanEntry scan_entry: lower-level ScanEntry returned from `bleio.Scanner`. + This constructor is normally used only by `Scanner`. """ - def __init__(self, entry): - self._bleio_entry = entry + def __init__(self, scan_entry): + self._bleio_scan_entry = scan_entry def item(self, item_type): """Return the bytes in the advertising packet for given the element type. @@ -96,23 +95,23 @@ def item(self, item_type): # Type doesn't match: skip to next item. i += item_length + 1 else: - return adv_bytes[i + 2:i + item_length] + return adv_bytes[i + 2:i + 1 + item_length] return None @property def advertisement_bytes(self): """The raw bytes of the received advertisement.""" - return self._bleio_entry.raw_data + return self._bleio_scan_entry.advertisement_bytes @property def rssi(self): """The signal strength of the device at the time of the scan. (read-only).""" - return self._bleio_entry.rssi + return self._bleio_scan_entry.rssi @property def address(self): """The address of the device. (read-only).""" - return self._bleio_entry.address + return self._bleio_scan_entry.address @property def name(self): @@ -146,3 +145,22 @@ def service_uuids(self): def manufacturer_specific_data(self): """Manufacturer-specific data in the advertisement, returned as bytes.""" return self.item(AdvertisingPacket.MANUFACTURER_SPECIFIC_DATA) + + def matches(self, other): + """True if two scan entries appear to be from the same device. Their + addresses and advertisement_bytes must match. + """ + return self.address == other.address and self.advertisement_bytes == other.advertisement_bytes + + @classmethod + def unique(self, scan_entries): + """Discard duplicate scan entries that appear to be from the same device. + + :param sequence scan_entries: ScanEntry objects + :returns list: list with duplicates removed + """ + unique = [] + for entry in scan_entries: + if not any(entry.matches(unique_entry) for unique_entry in unique): + unique.append(entry); + return unique diff --git a/adafruit_ble/uuid.py b/adafruit_ble/uuid.py new file mode 100644 index 0000000..96805ee --- /dev/null +++ b/adafruit_ble/uuid.py @@ -0,0 +1,35 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Dan Halbert for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_ble.uuid` +==================================================== + +BLE UUIDs + +* Author(s): Dan Halbert for Adafruit Industries + +""" + +from bleio import UUID as bleio_UUID + +UUID = bleio_UUID +"""`adafruit_ble.UUID` is the same as `bleio.UUID`""" From ad84cc4cddc3d0e46d715b3176979c0ddfa4beb0 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 2 Jul 2019 22:33:58 -0400 Subject: [PATCH 08/19] Add UARTClient; refactor common code --- adafruit_ble/scanner.py | 37 ++++++++++++---- adafruit_ble/uart.py | 80 ++++++++++++----------------------- adafruit_ble/uart_client.py | 84 +++++++++++++++++++++++++++++++++++++ adafruit_ble/uart_server.py | 83 ++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 60 deletions(-) create mode 100644 adafruit_ble/uart_client.py create mode 100644 adafruit_ble/uart_server.py diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index 117afe7..5ed2d7d 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -54,6 +54,21 @@ class Scanner: def __init__(self): self._scanner = bleio.Scanner() + def scan_unique(self, timeout, *, interval=0.1, window=0.1): + """Scan for advertisements from BLE devices. Suppress duplicates + in returned `ScanEntry` objects. + + :param int timeout: how long to scan for (in seconds) + :param float interval: the interval (in seconds) between the start + of two consecutive scan windows. + Must be in the range 0.0025 - 40.959375 seconds. + :param float window: the duration (in seconds) to scan a single BLE channel. + `window` must be <= `interval`. + :returns a list of `adafruit_ble.ScanEntry` objects. + + """ + return ScanEntry.unique(self.scan(timeout, interval=interval, window=window)) + def scan(self, timeout, *, interval=0.1, window=0.1): """Scan for advertisements from BLE devices. @@ -122,13 +137,14 @@ def name(self): @property def service_uuids(self): """List of all the service UUIDs in the advertisement.""" + uuid_values = [] + concat_uuids = self.item(AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS) concat_uuids = concat_uuids if concat_uuids else self.item( AdvertisingPacket.SOME_16_BIT_SERVICE_UUIDS) - uuid_values = [] if concat_uuids: - for i in range(0, len(uuid_values), 2): + for i in range(0, len(concat_uuids), 2): uuid_values.append(struct.unpack(" Date: Sun, 7 Jul 2019 00:08:33 -0400 Subject: [PATCH 09/19] Re-refactor UART classes. --- adafruit_ble/uart.py | 86 ++++------------------------------- adafruit_ble/uart_client.py | 91 +++++++++++++++++++++++++++++++------ adafruit_ble/uart_server.py | 63 +++++++++++++++++++++---- 3 files changed, 139 insertions(+), 101 deletions(-) diff --git a/adafruit_ble/uart.py b/adafruit_ble/uart.py index 9181d87..f891358 100644 --- a/adafruit_ble/uart.py +++ b/adafruit_ble/uart.py @@ -28,81 +28,11 @@ * Author(s): Dan Halbert for Adafruit Industries """ -from bleio import UUID, CharacteristicBuffer - - - - -class UART: - """ - Common superclass for Nordic UART Service (NUS) clients or servers. - Not for general use: use `UARTServer` and `UARTClient` for Peripheral and Central, - respectively. - - :param read_characteristic Characteristic: Characteristic to read from - :param write_characteristic Characteristic: Characteristic to write to - :param int timeout: the timeout in seconds to wait - for the first character and between subsequent characters - :param int buffer_size: buffer up to this many bytes. - If more bytes are received, older bytes will be discarded. - """ - - NUS_SERVICE_UUID = UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") - """Nordic UART Service UUID""" - NUS_RX_CHAR_UUID = UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") - """Nordic UART Service RX Characteristic UUID""" - NUS_TX_CHAR_UUID = UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") - """Nordic UART Service TX Characteristic UUID""" - - def __init__(self, *, read_characteristic, write_characteristic, timeout=5.0, buffer_size=64): - self._read_char = read_characteristic - self._write_char = write_characteristic - self._read_buffer = CharacteristicBuffer(self._read_char, - timeout=timeout, buffer_size=buffer_size) - - def read(self, nbytes=None): - """ - Read characters. If ``nbytes`` is specified then read at most that many bytes. - Otherwise, read everything that arrives until the connection times out. - Providing the number of bytes expected is highly recommended because it will be faster. - - :return: Data read - :rtype: bytes or None - """ - return self._read_buffer.read(nbytes) - - def readinto(self, buf, nbytes=None): - """ - Read bytes into the ``buf``. If ``nbytes`` is specified then read at most - that many bytes. Otherwise, read at most ``len(buf)`` bytes. - - :return: number of bytes read and stored into ``buf`` - :rtype: int or None (on a non-blocking error) - """ - return self._read_buffer.readinto(buf, nbytes) - - def readline(self): - """ - Read a line, ending in a newline character. - - :return: the line read - :rtype: int or None - """ - return self._read_buffer.readline() - - @property - def in_waiting(self): - """The number of bytes in the input buffer, available to be read.""" - return self._read_buffer.in_waiting - - def reset_input_buffer(self): - """Discard any unread characters in the input buffer.""" - self._read_buffer.reset_input_buffer() - - def write(self, buf): - """Write a buffer of bytes.""" - # We can only write 20 bytes at a time. - offset = 0 - while offset < len(buf): - self._write_char.value = buf[offset:offset+20] - offset += 20 +from bleio import UUID + +NUS_SERVICE_UUID = UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") +"""Nordic UART Service UUID""" +NUS_RX_CHAR_UUID = UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") +"""Nordic UART Service RX Characteristic UUID""" +NUS_TX_CHAR_UUID = UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") +"""Nordic UART Service TX Characteristic UUID""" diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index d0841f2..3215e26 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -28,10 +28,10 @@ * Author(s): Dan Halbert for Adafruit Industries """ -from bleio import Characteristic, Central -from .uart import UART +from bleio import Central, CharacteristicBuffer +from .uart import NUS_SERVICE_UUID, NUS_RX_CHAR_UUID, NUS_TX_CHAR_UUID -class UARTClient(UART): +class UARTClient: """ Provide UART-like functionality via the Nordic NUS service. @@ -58,27 +58,88 @@ class UARTClient(UART): """ def __init__(self, *, timeout=5.0, buffer_size=64): - # Since we're remote we receive on tx and send on rx. The names - # are from the point of view of the server. - super().__init__(read_characteristic=Characteristic(UART.NUS_TX_CHAR_UUID), - write_characteristic=Characteristic(UART.NUS_RX_CHAR_UUID), - timeout=timeout, buffer_size=buffer_size) - + self._buffer_size = buffer_size + self._timeout = timeout + self._read_char = self._write_char = self._read_buffer = None self._central = Central() - @property - def connected(self): - """True if we are connected to a peripheral.""" - return self._central.connected - def connect(self, address, timeout): """Try to connect to the peripheral at the given address. :param bleio.Address address: The address of the peripheral to connect to :param float/int timeout: Try to connect for timeout seconds. """ - self._central.connect(address, timeout, service_uuids=(UART.NUS_SERVICE_UUID,)) + self._central.connect(address, timeout, service_uuids=(NUS_SERVICE_UUID,)) + + # Connect succeeded. Get the remote characteristics we need, which were + # found during discovery. + + for characteristic in self._central.remote_services[0].characteristics: + # Since we're remote we receive on tx and send on rx. + # The names are from the point of view of the server. + if characteristic.uuid == NUS_RX_CHAR_UUID: + self._write_char = characteristic + elif characteristic.uuid == NUS_TX_CHAR_UUID: + self._read_char = characteristic + if not self._write_char or not self._read_char: + raise OSError("Remote UART missing needed characteristic") + self._read_buffer = CharacteristicBuffer(self._read_char, + timeout=self._timeout, + buffer_size=self._buffer_size) def disconnect(self): """Disconnect from the peripheral.""" self._central.disconnect() + self._read_char = self._write_char = self._read_buffer = None + + @property + def connected(self): + """True if we are connected to a peripheral.""" + return self._central.connected + + def read(self, nbytes=None): + """ + Read characters. If ``nbytes`` is specified then read at most that many bytes. + Otherwise, read everything that arrives until the connection times out. + Providing the number of bytes expected is highly recommended because it will be faster. + + :return: Data read + :rtype: bytes or None + """ + return self._read_buffer.read(nbytes) + + def readinto(self, buf, nbytes=None): + """ + Read bytes into the ``buf``. If ``nbytes`` is specified then read at most + that many bytes. Otherwise, read at most ``len(buf)`` bytes. + + :return: number of bytes read and stored into ``buf`` + :rtype: int or None (on a non-blocking error) + """ + return self._read_buffer.readinto(buf, nbytes) + + def readline(self): + """ + Read a line, ending in a newline character. + + :return: the line read + :rtype: int or None + """ + return self._read_buffer.readline() + + @property + def in_waiting(self): + """The number of bytes in the input buffer, available to be read.""" + return self._read_buffer.in_waiting + + def reset_input_buffer(self): + """Discard any unread characters in the input buffer.""" + self._read_buffer.reset_input_buffer() + + def write(self, buf): + """Write a buffer of bytes.""" + # We can only write 20 bytes at a time. + offset = 0 + while offset < len(buf): + self._write_char.value = buf[offset:offset+20] + offset += 20 diff --git a/adafruit_ble/uart_server.py b/adafruit_ble/uart_server.py index 06dc1ff..d7e1c9f 100644 --- a/adafruit_ble/uart_server.py +++ b/adafruit_ble/uart_server.py @@ -28,11 +28,11 @@ * Author(s): Dan Halbert for Adafruit Industries """ -from bleio import Characteristic, Service, Peripheral +from bleio import Characteristic, CharacteristicBuffer, Peripheral, Service from .advertising import ServerAdvertisement -from .uart import UART +from .uart import NUS_SERVICE_UUID, NUS_RX_CHAR_UUID, NUS_TX_CHAR_UUID -class UARTServer(UART): +class UARTServer: """ Provide UART-like functionality via the Nordic NUS service. @@ -56,12 +56,12 @@ class UARTServer(UART): """ def __init__(self, *, timeout=1.0, buffer_size=64, name=None): - read_char = Characteristic(UART.NUS_RX_CHAR_UUID, write=True, write_no_response=True) - write_char = Characteristic(UART.NUS_TX_CHAR_UUID, notify=True) - super().__init__(read_characteristic=read_char, write_characteristic=write_char, - timeout=timeout, buffer_size=buffer_size) + self._read_char = Characteristic(NUS_RX_CHAR_UUID, write=True, write_no_response=True) + self._write_char = Characteristic(NUS_TX_CHAR_UUID, notify=True) + self._read_buffer = CharacteristicBuffer(self._read_char, + timeout=timeout, buffer_size=buffer_size) - nus_uart_service = Service(UART.NUS_SERVICE_UUID, (read_char, write_char)) + nus_uart_service = Service(NUS_SERVICE_UUID, (self._read_char, self._write_char)) self._periph = Peripheral((nus_uart_service,), name=name) self._advertisement = ServerAdvertisement(self._periph) @@ -81,3 +81,50 @@ def stop_advertising(self): def connected(self): """True if someone connected to the server.""" return self._periph.connected + + def read(self, nbytes=None): + """ + Read characters. If ``nbytes`` is specified then read at most that many bytes. + Otherwise, read everything that arrives until the connection times out. + Providing the number of bytes expected is highly recommended because it will be faster. + + :return: Data read + :rtype: bytes or None + """ + return self._read_buffer.read(nbytes) + + def readinto(self, buf, nbytes=None): + """ + Read bytes into the ``buf``. If ``nbytes`` is specified then read at most + that many bytes. Otherwise, read at most ``len(buf)`` bytes. + + :return: number of bytes read and stored into ``buf`` + :rtype: int or None (on a non-blocking error) + """ + return self._read_buffer.readinto(buf, nbytes) + + def readline(self): + """ + Read a line, ending in a newline character. + + :return: the line read + :rtype: int or None + """ + return self._read_buffer.readline() + + @property + def in_waiting(self): + """The number of bytes in the input buffer, available to be read.""" + return self._read_buffer.in_waiting + + def reset_input_buffer(self): + """Discard any unread characters in the input buffer.""" + self._read_buffer.reset_input_buffer() + + def write(self, buf): + """Write a buffer of bytes.""" + # We can only write 20 bytes at a time. + offset = 0 + while offset < len(buf): + self._write_char.value = buf[offset:offset+20] + offset += 20 From 672f51c40b552cf316d595be1899d001c9383aad Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 9 Jul 2019 00:22:19 -0400 Subject: [PATCH 10/19] UARTClient now works both directions --- adafruit_ble/scanner.py | 3 +-- adafruit_ble/uart_client.py | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index 5ed2d7d..5b2c4e8 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -145,7 +145,7 @@ def service_uuids(self): if concat_uuids: for i in range(0, len(concat_uuids), 2): - uuid_values.append(struct.unpack(" Date: Wed, 10 Jul 2019 13:37:35 -0400 Subject: [PATCH 11/19] add UART_Client.scan() convenience method --- adafruit_ble/uart_client.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index 42778b7..08b0c2e 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -30,6 +30,7 @@ """ from bleio import Central, CharacteristicBuffer from .uart import NUS_SERVICE_UUID, NUS_RX_CHAR_UUID, NUS_TX_CHAR_UUID +from .scanner import ScanEntry class UARTClient: """ @@ -46,13 +47,12 @@ class UARTClient: from adafruit_ble.uart_client import UARTClient from adafruit_ble.scanner import Scanner, ScanEntry - scanner = Scanner() - uarts = ScanEntry.with_service_uuid(scanner.scan_unique(3), UART.NUS_SERVICE_UUID) - if not uarts: - raise ValueError("No UART for connection") - uart_client = UARTClient() - uart_client.connect(uarts[0].address, 5, service_uuids=(UART.NUS_SERVICE_UUID,)) + uart_addresses = uart_client.scan() + if uart_addresses: + uart_client.connect(uarts[0].address, 5, service_uuids=(UART.NUS_SERVICE_UUID,)) + else: + raise Error("No UART servers found.") uart_client.write('abc') """ @@ -91,6 +91,17 @@ def connect(self, address, timeout): timeout=self._timeout, buffer_size=self._buffer_size) + def scan(self, scanner=None, scan_time=2): + """Scan for Peripherals advertising the Nordic UART Service, + and return their addresses. + + :param int scanner: Scanner to use. If not supplied, a local one will + be created. + :param float scan_time: scan for this many seconds. + :return list of Address objects, or an empty list, if none found + """ + return [se.address for se in + ScanEntry.with_service_uuid(scanner.scan_unique(scan_time), NUS_SERVICE_UUID)] def disconnect(self): """Disconnect from the peripheral.""" From 579c116f040771f01007492a2fb277c6f34a9c5b Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 11 Jul 2019 12:11:08 -0400 Subject: [PATCH 12/19] pre-PR linting --- adafruit_ble/uart_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index 08b0c2e..e12e0e7 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -91,7 +91,8 @@ def connect(self, address, timeout): timeout=self._timeout, buffer_size=self._buffer_size) - def scan(self, scanner=None, scan_time=2): + @staticmethod + def scan(scanner=None, scan_time=2): """Scan for Peripherals advertising the Nordic UART Service, and return their addresses. From cd2b7ddbf88c7aaec0ac1e9cdf810b87573150f6 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 11 Jul 2019 19:43:36 -0400 Subject: [PATCH 13/19] Add and update examples --- adafruit_ble/uart_client.py | 3 +++ examples/ble_demo_central.py | 48 ++++++++++++++++++++++++++++++++++ examples/ble_demo_periph.py | 36 +++++++++++++++++++++++++ examples/ble_uart_echo_test.py | 2 +- 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 examples/ble_demo_central.py create mode 100644 examples/ble_demo_periph.py diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index e12e0e7..c80a672 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -101,6 +101,9 @@ def scan(scanner=None, scan_time=2): :param float scan_time: scan for this many seconds. :return list of Address objects, or an empty list, if none found """ + if not scanner: + scanner = Scanner() + return [se.address for se in ScanEntry.with_service_uuid(scanner.scan_unique(scan_time), NUS_SERVICE_UUID)] diff --git a/examples/ble_demo_central.py b/examples/ble_demo_central.py new file mode 100644 index 0000000..54b72c3 --- /dev/null +++ b/examples/ble_demo_central.py @@ -0,0 +1,48 @@ +""" +Demonstration of a Bluefruit BLE Central. Connects to the first BLE UART peripheral it finds. +Sends Bluefruit ColorPackets, read from three potentiometers, to the peripheral. +""" + +import time + +import board +from analogio import AnalogIn + +#from adafruit_bluefruit_connect.packet import Packet +# Only the packet classes that are imported will be known to Packet. +from adafruit_bluefruit_connect.color_packet import ColorPacket + +from adafruit_ble.scanner import Scanner +from adafruit_ble.uart_client import UARTClient + +def scale(value): + """Scale an value from 0-65535 (AnalogIn range) to 0-255 (RGB range)""" + return int(value / 65535 * 255) + +scanner = Scanner() +uart_client = UARTClient() +uart_addresses = [] + +# Keep trying to find a UART peripheral +while not uart_addresses: + uart_addresses = uart_client.scan(scanner) + +a0 = AnalogIn(board.A0) +a1 = AnalogIn(board.A1) +a2 = AnalogIn(board.A2) + +while True: + uart_client.connect(uart_addresses[0], 5) + while uart_client.connected: + r = scale(a0.value) + g = scale(a1.value) + b = scale(a2.value) + + color = (r, g, b) + print(color) + color_packet = ColorPacket(color) + try: + uart_client.write(color_packet.to_bytes()) + except OSError: + pass + time.sleep(0.3) diff --git a/examples/ble_demo_periph.py b/examples/ble_demo_periph.py new file mode 100644 index 0000000..83bb610 --- /dev/null +++ b/examples/ble_demo_periph.py @@ -0,0 +1,36 @@ +""" +Used with ble_demo_central.py. Receives Bluefruit LE ColorPackets from a central, +and updates a NeoPixel FeatherWing to show the history of the received packets. +""" + +import board +import neopixel + +from adafruit_ble.uart_server import UARTServer +from adafruit_bluefruit_connect.packet import Packet +# Only the packet classes that are imported will be known to Packet. +from adafruit_bluefruit_connect.color_packet import ColorPacket + +uart_server = UARTServer() + +NUM_PIXELS = 32 +np = neopixel.NeoPixel(board.D10, NUM_PIXELS, brightness=0.1) +next_pixel = 0 + +def mod(i): + """Wrap i to modulus NUM_PIXELS.""" + return i % NUM_PIXELS + +while True: + # Advertise when not connected. + uart_server.start_advertising() + while not uart_server.connected: + pass + + while uart_server.connected: + packet = Packet.from_stream(uart_server) + if isinstance(packet, ColorPacket): + print(packet.color) + np[next_pixel] = packet.color + np[mod(next_pixel + 1)] = (0, 0, 0) + next_pixel = (next_pixel + 1) % NUM_PIXELS diff --git a/examples/ble_uart_echo_test.py b/examples/ble_uart_echo_test.py index 40f1173..82bec77 100644 --- a/examples/ble_uart_echo_test.py +++ b/examples/ble_uart_echo_test.py @@ -1,4 +1,4 @@ -from adafruit_ble.uart import UARTServer +from adafruit_ble.uart_server import UARTServer uart = UARTServer() From cf11c77dfa8d0d35696fd42a107e0fb206784184 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 15 Jul 2019 07:41:41 -0400 Subject: [PATCH 14/19] missing import --- adafruit_ble/uart_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index c80a672..62e9082 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -30,7 +30,7 @@ """ from bleio import Central, CharacteristicBuffer from .uart import NUS_SERVICE_UUID, NUS_RX_CHAR_UUID, NUS_TX_CHAR_UUID -from .scanner import ScanEntry +from .scanner import Scanner, ScanEntry class UARTClient: """ @@ -45,7 +45,6 @@ class UARTClient: Example:: from adafruit_ble.uart_client import UARTClient - from adafruit_ble.scanner import Scanner, ScanEntry uart_client = UARTClient() uart_addresses = uart_client.scan() From 456c553205b82daae7e5b5812500c623266f802c Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 16 Jul 2019 20:03:21 -0400 Subject: [PATCH 15/19] UARTClient: service_uuids -> service_uuids_whitelist --- adafruit_ble/uart_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index 62e9082..31ed230 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -49,7 +49,7 @@ class UARTClient: uart_client = UARTClient() uart_addresses = uart_client.scan() if uart_addresses: - uart_client.connect(uarts[0].address, 5, service_uuids=(UART.NUS_SERVICE_UUID,)) + uart_client.connect(uarts[0].address, 5, service_uuids_whitelist=(UART.NUS_SERVICE_UUID,)) else: raise Error("No UART servers found.") @@ -69,7 +69,7 @@ def connect(self, address, timeout): :param float/int timeout: Try to connect for ``timeout`` seconds. Not related to the timeout passed to ``UARTClient()``. """ - self._central.connect(address, timeout, service_uuids=(NUS_SERVICE_UUID,)) + self._central.connect(address, timeout, service_uuids_whitelist=(NUS_SERVICE_UUID,)) # Connect succeeded. Get the remote characteristics we need, which were # found during discovery. From c028f0a8d05bacdb02117168138c9fb5a099649d Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 16 Jul 2019 21:00:39 -0400 Subject: [PATCH 16/19] pylint --- adafruit_ble/uart_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index 31ed230..932ccb3 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -49,7 +49,8 @@ class UARTClient: uart_client = UARTClient() uart_addresses = uart_client.scan() if uart_addresses: - uart_client.connect(uarts[0].address, 5, service_uuids_whitelist=(UART.NUS_SERVICE_UUID,)) + uart_client.connect(uarts[0].address, 5, + service_uuids_whitelist=(UART.NUS_SERVICE_UUID,)) else: raise Error("No UART servers found.") From 24b427e4bb0bc207d8a6669b1431b98f6341c3aa Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 18 Jul 2019 21:08:20 -0400 Subject: [PATCH 17/19] Move advertisement field lookup to AdvertisingPacket. Rename various methods for clarity --- adafruit_ble/advertising.py | 45 ++++++++++++++++++++++-- adafruit_ble/scanner.py | 69 +++++++++++++++---------------------- adafruit_ble/uart_client.py | 2 +- 3 files changed, 70 insertions(+), 46 deletions(-) diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index 799794e..e507061 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -81,9 +81,15 @@ class AdvertisingPacket: MAX_DATA_SIZE = 31 """Data size in a regular BLE packet.""" - def __init__(self, *, max_length=MAX_DATA_SIZE): - """Create an empty advertising packet, no larger than max_length.""" - self._packet_bytes = bytearray() + def __init__(self, data=None, *, max_length=MAX_DATA_SIZE): + """Create an advertising packet, no larger than max_length. + + :param buf data: if not supplied (None), create an empty packet + if supplied, create a packet with supplied data. This is usually used + to parse an existing packet. + :param int max_length: maximum length of packet + """ + self._packet_bytes = bytearray(data) if data else bytearray() self._max_length = max_length self._check_length() @@ -92,6 +98,39 @@ def packet_bytes(self): """The raw packet bytes.""" return self._packet_bytes + @packet_bytes.setter + def packet_bytes(self, value): + self._packet_bytes = value + + def __getitem__(self, element_type): + """Return the bytes stored in the advertising packet for the given element type. + + :param int element_type: An integer designating an advertising element type. + A number of types are defined in `AdvertisingPacket`, + such as `AdvertisingPacket.TX_POWER`. + :returns: bytes that are the value for the given element type. + If the element type is not present in the packet, raise KeyError. + """ + i = 0 + adv_bytes = self.packet_bytes + while i < len(adv_bytes): + item_length = adv_bytes[i] + if element_type != adv_bytes[i+1]: + # Type doesn't match: skip to next item. + i += item_length + 1 + else: + return adv_bytes[i + 2:i + 1 + item_length] + raise KeyError + + def get(self, element_type, default=None): + """Return the bytes stored in the advertising packet for the given element type, + returning the default value if not found. + """ + try: + return self.__getitem__(element_type) + except KeyError: + return default + @property def bytes_remaining(self): """Number of bytes still available for use in the packet.""" diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index 5b2c4e8..9980553 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -54,9 +54,9 @@ class Scanner: def __init__(self): self._scanner = bleio.Scanner() - def scan_unique(self, timeout, *, interval=0.1, window=0.1): + def scan(self, timeout, *, interval=0.1, window=0.1): """Scan for advertisements from BLE devices. Suppress duplicates - in returned `ScanEntry` objects. + in returned `ScanEntry` objects, so there is only one entry per address (device). :param int timeout: how long to scan for (in seconds) :param float interval: the interval (in seconds) between the start @@ -67,10 +67,11 @@ def scan_unique(self, timeout, *, interval=0.1, window=0.1): :returns a list of `adafruit_ble.ScanEntry` objects. """ - return ScanEntry.unique(self.scan(timeout, interval=interval, window=window)) + return ScanEntry.unique_devices(self.raw_scan(timeout, interval=interval, window=window)) - def scan(self, timeout, *, interval=0.1, window=0.1): - """Scan for advertisements from BLE devices. + def raw_scan(self, timeout, *, interval=0.1, window=0.1): + """Scan for advertisements from BLE devices. Include every scan entry, + even duplicates. :param int timeout: how long to scan for (in seconds) :param float interval: the interval (in seconds) between the start @@ -92,63 +93,46 @@ class ScanEntry: """ def __init__(self, scan_entry): - self._bleio_scan_entry = scan_entry - - def item(self, item_type): - """Return the bytes in the advertising packet for given the element type. - - :param int element_type: An integer designating an element type. - A number are defined in `AdvertisingPacket`, such as `AdvertisingPacket.TX_POWER`. - :returns: bytes that are the value for the given element type. - If the element type is not present in the packet, return ``None``. - """ - i = 0 - adv_bytes = self.advertisement_bytes - while i < len(adv_bytes): - item_length = adv_bytes[i] - if item_type != adv_bytes[i+1]: - # Type doesn't match: skip to next item. - i += item_length + 1 - else: - return adv_bytes[i + 2:i + 1 + item_length] - return None + self._rssi = scan_entry.rssi + self._address = scan_entry.address + self._packet = AdvertisingPacket(scan_entry.advertisement_bytes) @property - def advertisement_bytes(self): - """The raw bytes of the received advertisement.""" - return self._bleio_scan_entry.advertisement_bytes + def advertisement_packet(self): + """The received advertising packet.""" + return self._packet @property def rssi(self): """The signal strength of the device at the time of the scan. (read-only).""" - return self._bleio_scan_entry.rssi + return self._rssi @property def address(self): """The address of the device. (read-only).""" - return self._bleio_scan_entry.address + return self._address @property def name(self): """The name of the device. (read-only)""" - name = self.item(AdvertisingPacket.COMPLETE_LOCAL_NAME) - return name if name else self.item(AdvertisingPacket.SHORT_LOCAL_NAME) + name = self._packet.get(AdvertisingPacket.COMPLETE_LOCAL_NAME) + return name if name else self._packet.get(AdvertisingPacket.SHORT_LOCAL_NAME) @property def service_uuids(self): """List of all the service UUIDs in the advertisement.""" uuid_values = [] - concat_uuids = self.item(AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS) - concat_uuids = concat_uuids if concat_uuids else self.item( + concat_uuids = self._packet.get(AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS) + concat_uuids = concat_uuids if concat_uuids else self._packet.get( AdvertisingPacket.SOME_16_BIT_SERVICE_UUIDS) if concat_uuids: for i in range(0, len(concat_uuids), 2): uuid_values.extend(struct.unpack(" Date: Thu, 18 Jul 2019 21:52:46 -0400 Subject: [PATCH 18/19] fix example --- adafruit_ble/scanner.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index 9980553..1092604 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -42,13 +42,7 @@ class Scanner: from adafruit_ble.scanner import Scanner scanner = Scanner() - scanner.Scan - - # Wait for a connection. - while not uart.connected: - pass - - uart.write('abc') + scan_entries = scanner.scan() """ def __init__(self): From 960688c13d7455fc91b7319b44f3a1d5546c32e5 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 18 Jul 2019 21:53:39 -0400 Subject: [PATCH 19/19] fix example again --- adafruit_ble/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_ble/scanner.py b/adafruit_ble/scanner.py index 1092604..d11b804 100644 --- a/adafruit_ble/scanner.py +++ b/adafruit_ble/scanner.py @@ -42,7 +42,7 @@ class Scanner: from adafruit_ble.scanner import Scanner scanner = Scanner() - scan_entries = scanner.scan() + scan_entries = scanner.scan(3) # scan for 3 seconds """ def __init__(self):