From 8291ee9b1bd2206cb384483183c30358be40e6c5 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 23 Sep 2019 14:33:07 -0700 Subject: [PATCH 1/4] Rework the API to use descriptors. This makes Advertisement and Service definitions declarative by factoring out parsing logic out into shareable descriptor classes similar to how the Register library works. This also introduces SmartAdapter and SmartConnection which will auto-create the correct Advertisements and Services without requiring any direct use of UUIDs. Instead, classes are used to identify relevant objects to "recognize". This requires https://github.com/adafruit/circuitpython/pull/2236 and relates to https://github.com/adafruit/circuitpython/issues/586. --- .gitignore | 1 + .travis.yml | 4 +- README.rst | 22 +- adafruit_ble/__init__.py | 161 ++++++++- adafruit_ble/advertising.py | 282 --------------- adafruit_ble/advertising/__init__.py | 287 +++++++++++++++ adafruit_ble/advertising/adafruit.py | 58 +++ adafruit_ble/advertising/apple.py | 8 + adafruit_ble/advertising/standard.py | 230 ++++++++++++ adafruit_ble/beacon.py | 159 -------- adafruit_ble/characteristics/__init__.py | 0 adafruit_ble/characteristics/core.py | 120 ++++++ .../{uuid.py => characteristics/int.py} | 32 +- adafruit_ble/characteristics/stream.py | 91 +++++ adafruit_ble/characteristics/string.py | 75 ++++ adafruit_ble/current_time_client.py | 148 -------- adafruit_ble/device_information_service.py | 97 ----- adafruit_ble/hid_server.py | 341 ------------------ adafruit_ble/scanner.py | 168 --------- adafruit_ble/services/__init__.py | 0 adafruit_ble/{uart.py => services/apple.py} | 42 ++- adafruit_ble/services/circuitpython.py | 56 +++ adafruit_ble/services/core.py | 93 +++++ adafruit_ble/services/microbit.py | 0 adafruit_ble/services/midi.py | 72 ++++ adafruit_ble/services/nordic.py | 122 +++++++ .../{address.py => services/sphero.py} | 19 +- adafruit_ble/services/standard/__init__.py | 0 adafruit_ble/services/standard/device_info.py | 85 +++++ adafruit_ble/services/standard/hid.py | 288 +++++++++++++++ adafruit_ble/services/standard/standard.py | 88 +++++ adafruit_ble/uart_client.py | 166 --------- adafruit_ble/uart_server.py | 143 -------- adafruit_ble/uuid/__init__.py | 70 ++++ docs/advertising.rst | 14 + docs/api.rst | 16 +- docs/bleio_mock.py | 12 + docs/characteristics.rst | 14 + docs/conf.py | 10 +- docs/examples.rst | 8 +- docs/services.rst | 17 + docs/top_level.rst | 3 + docs/uuid.rst | 3 + examples/ble_color_proximity.py | 82 +++++ examples/ble_demo_central.py | 37 +- examples/ble_demo_periph.py | 16 +- examples/ble_eddystone_test.py | 7 +- examples/ble_hid_central.py | 31 ++ examples/ble_hid_periph.py | 153 ++++++++ examples/ble_scan_everything.py | 12 + examples/ble_uart_echo_client.py | 37 ++ examples/ble_uart_echo_test.py | 25 +- 52 files changed, 2423 insertions(+), 1602 deletions(-) mode change 100644 => 100755 adafruit_ble/__init__.py delete mode 100644 adafruit_ble/advertising.py create mode 100644 adafruit_ble/advertising/__init__.py create mode 100755 adafruit_ble/advertising/adafruit.py create mode 100644 adafruit_ble/advertising/apple.py create mode 100644 adafruit_ble/advertising/standard.py delete mode 100644 adafruit_ble/beacon.py create mode 100644 adafruit_ble/characteristics/__init__.py create mode 100755 adafruit_ble/characteristics/core.py rename adafruit_ble/{uuid.py => characteristics/int.py} (54%) mode change 100644 => 100755 create mode 100755 adafruit_ble/characteristics/stream.py create mode 100755 adafruit_ble/characteristics/string.py delete mode 100644 adafruit_ble/current_time_client.py delete mode 100644 adafruit_ble/device_information_service.py delete mode 100644 adafruit_ble/hid_server.py delete mode 100644 adafruit_ble/scanner.py create mode 100644 adafruit_ble/services/__init__.py rename adafruit_ble/{uart.py => services/apple.py} (50%) mode change 100644 => 100755 create mode 100755 adafruit_ble/services/circuitpython.py create mode 100755 adafruit_ble/services/core.py create mode 100644 adafruit_ble/services/microbit.py create mode 100644 adafruit_ble/services/midi.py create mode 100755 adafruit_ble/services/nordic.py rename adafruit_ble/{address.py => services/sphero.py} (73%) create mode 100644 adafruit_ble/services/standard/__init__.py create mode 100644 adafruit_ble/services/standard/device_info.py create mode 100755 adafruit_ble/services/standard/hid.py create mode 100755 adafruit_ble/services/standard/standard.py delete mode 100644 adafruit_ble/uart_client.py delete mode 100644 adafruit_ble/uart_server.py create mode 100644 adafruit_ble/uuid/__init__.py create mode 100644 docs/advertising.rst create mode 100644 docs/bleio_mock.py create mode 100644 docs/characteristics.rst create mode 100644 docs/services.rst create mode 100644 docs/top_level.rst create mode 100644 docs/uuid.rst create mode 100644 examples/ble_color_proximity.py create mode 100644 examples/ble_hid_central.py create mode 100644 examples/ble_hid_periph.py create mode 100644 examples/ble_scan_everything.py create mode 100644 examples/ble_uart_echo_client.py diff --git a/.gitignore b/.gitignore index 0dd8629..7a57b52 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ _build .env build* bundles +.DS_Store diff --git a/.travis.yml b/.travis.yml index f2d4036..59f76f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,10 @@ deploy: install: - pip install -r requirements.txt - pip install circuitpython-build-tools Sphinx sphinx-rtd-theme -- pip install --force-reinstall pylint==1.9.2 +- pip install --force-reinstall "pylint<3" script: -- pylint adafruit_ble/*.py +- pylint --disable=too-few-public-methods adafruit_ble/**/*.py adafruit_ble/*.py - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-ble diff --git a/README.rst b/README.rst index eb508fa..4a5533e 100644 --- a/README.rst +++ b/README.rst @@ -31,16 +31,18 @@ Usage Example .. code-block:: python - from adafruit_ble.uart import UARTServer - - uart_server = UARTServer() - uart_server.start_advertising() - - # Wait for a connection. - while not uart_server.connected: - pass - - uart_server.write('abc') + from adafruit_ble import SmartAdapter + + adapter = SmartAdapter() + print("scanning") + found = set() + for entry in adapter.start_scan(timeout=60, minimum_rssi=-80): + addr = entry.address + if addr not in found: + print(entry) + found.add(addr) + + print("scan done") Contributing diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py old mode 100644 new mode 100755 index 20a3ca4..9e6d9b4 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -1,6 +1,7 @@ # The MIT License (MIT) # # Copyright (c) 2019 Dan Halbert for Adafruit Industries +# Copyright (c) 2019 Scott Shawcroft 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 @@ -26,7 +27,7 @@ This module provides higher-level BLE (Bluetooth Low Energy) functionality, building on the native `_bleio` module. -* Author(s): Dan Halbert for Adafruit Industries +* Author(s): Dan Halbert and Scott Shawcroft for Adafruit Industries Implementation Notes -------------------- @@ -42,7 +43,163 @@ """ -# imports +import _bleio +import board + +from .services.core import Service +from .advertising import Advertisement __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +# These are internal data structures used throughout the library to recognize certain Services and +# Advertisements. +# pylint: disable=invalid-name +all_services_by_name = {} +all_services_by_uuid = {} +known_advertisements = set() +# pylint: enable=invalid-name + +def recognize_services(*service_classes): + """Instruct the adafruit_ble library to recognize the given Services. + + This will cause the Service related advertisements to show the corresponding class. + `SmartConnection` will automatically have attributes for any recognized service available + from the peer.""" + for service_class in service_classes: + if not issubclass(service_class, Service): + raise ValueError("Can only detect subclasses of Service") + all_services_by_name[service_class.default_field_name] = service_class + all_services_by_uuid[service_class.uuid] = service_class + +def recognize_advertisement(*advertisements): + """Instruct the adafruit_ble library to recognize the given `Advertisement` types. + + When an advertisement is recognized by the `SmartAdapter`, it will be returned from the + start_scan iterator instead of a generic `Advertisement`.""" + known_advertisements.add(*advertisements) + +class SmartConnection: + """This represents a connection to a peer BLE device. + + Its smarts come from its ability to recognize Services available on the peer and make them + available as attributes on the Connection. Use `recognize_services` to register all services + of interest. All subsequent Connections will then recognize the service. + + ``dir(connection)`` will show all attributes including recognized Services. + """ + def __init__(self, connection): + self._connection = connection + + def __dir__(self): + discovered = [] + results = self._connection.discover_remote_services() + for service in results: + uuid = service.uuid + if uuid in all_services_by_uuid: + service = all_services_by_uuid[uuid] + discovered.append(service.default_field_name) + super_dir = dir(super()) + super_dir.extend(discovered) + return super_dir + + def __getattr__(self, name): + if name in self.__dict__: + return self.__dict__[name] + if name in all_services_by_name: + service = all_services_by_name[name] + uuid = service.uuid._uuid + results = self._connection.discover_remote_services((uuid,)) + if results: + remote_service = service(service=results[0]) + setattr(self, name, remote_service) + return remote_service + raise AttributeError() + + @property + def connected(self): + """True if the connection to the peer is still active.""" + return self._connection.connected + + def disconnect(self): + """Disconnect from peer.""" + self._connection.disconnect() + +class SmartAdapter: + """This BLE Adapter class enhances the normal `_bleio.Adapter`. + + It uses the library's `Advertisement` classes and the `SmartConnection` class.""" + def __init__(self, adapter=None): + if not adapter: + adapter = _bleio.adapter + self._adapter = adapter + self._current_advertisement = None + self._connection_cache = {} + + def start_advertising(self, advertisement, scan_response=None, **kwargs): + """Starts advertising the given advertisement. + + It takes most kwargs of `_bleio.Adapter.start_advertising`.""" + scan_response_data = None + if scan_response: + scan_response_data = bytes(scan_response) + print(advertisement.connectable) + self._adapter.start_advertising(bytes(advertisement), + scan_response=scan_response_data, + connectable=advertisement.connectable, + **kwargs) + + def stop_advertising(self): + """Stops advertising.""" + self._adapter.stop_advertising() + + def start_scan(self, advertisement_types=None, **kwargs): + """Starts scanning. Returns an iterator of Advertisements that are either recognized or + in advertisment_types (which will be subsequently recognized.) The iterator will block + until an advertisement is heard or the scan times out. + + If a list ``advertisement_types`` is given, only Advertisements of that type are produced + by the returned iterator.""" + prefixes = b"" + if advertisement_types: + recognize_advertisement(*advertisement_types) + if len(advertisement_types) == 1: + prefixes = advertisement_types[0].prefix + for entry in self._adapter.start_scan(prefixes=prefixes, **kwargs): + adv_type = Advertisement + for possible_type in known_advertisements: + if possible_type.matches(entry) and issubclass(possible_type, adv_type): + adv_type = possible_type + advertisement = adv_type.from_entry(entry) + if advertisement: + yield advertisement + + def stop_scan(self): + """Stops any active scan. + + The scan results iterator will return any buffered results and then raise StopIteration + once empty.""" + self._adapter.stop_scan() + + def connect(self, advertisement, *, timeout=4): + """Initiates a `SmartConnection` to the peer that advertised the given advertisement.""" + connection = self._adapter.connect(advertisement.address, timeout=timeout) + self._connection_cache[connection] = SmartConnection(connection) + return self._connection_cache[connection] + + @property + def connected(self): + """True if any peers are connected to the adapter.""" + return self._adapter.connected + + @property + def connections(self): + """A tuple of active `SmartConnection` objects.""" + connections = self._adapter.connections + smart_connections = [None] * len(connections) + for i, connection in enumerate(self._adapter.connections): + if connection not in self._connection_cache: + self._connection_cache[connection] = SmartConnection(connection) + smart_connections[i] = self._connection_cache[connection] + + return tuple(smart_connections) diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py deleted file mode 100644 index 3dd06eb..0000000 --- a/adafruit_ble/advertising.py +++ /dev/null @@ -1,282 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2018 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.advertising` -==================================================== - -Advertising-related classes. - -* Author(s): Dan Halbert for Adafruit Industries - -""" - -import struct - -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.""" - FLAG_GENERAL_DISCOVERY = 0x02 - """Will advertise until discovered.""" - FLAG_LE_ONLY = 0x04 - """BR/EDR not supported.""" - - FLAGS = 0x01 - """Discoverability flags.""" - SOME_16_BIT_SERVICE_UUIDS = 0x02 - """Incomplete list of 16 bit service UUIDs.""" - ALL_16_BIT_SERVICE_UUIDS = 0x03 - """Complete list of 16 bit service UUIDs.""" - SOME_128_BIT_SERVICE_UUIDS = 0x06 - """Incomplete list of 128 bit service UUIDs.""" - ALL_128_BIT_SERVICE_UUIDS = 0x07 - """Complete list of 128 bit service UUIDs.""" - SOLICITED_16_BIT_SERVICE_UUIDS = 0x14 - """List of 16 bit service UUIDs solicited by a peripheral.""" - SOLICITED_128_BIT_SERVICE_UUIDS = 0x15 - """List of 128 bit service UUIDs solicited by a peripheral.""" - SHORT_LOCAL_NAME = 0x08 - """Short local device name (shortened to fit).""" - COMPLETE_LOCAL_NAME = 0x09 - """Complete local device name.""" - TX_POWER = 0x0A - """Transmit power level""" - DEVICE_ID = 0x10 - """Device identifier.""" - SLAVE_CONN_INTERVAL_RANGE = 0x12 - """Slave connection interval range.""" - SERVICE_DATA_16_BIT_UUID = 0x16 - """Service data with 16 bit UUID.""" - PUBLIC_TARGET_ADDRESS = 0x17 - """Public target address.""" - RANDOM_TARGET_ADDRESS = 0x18 - """Random target address (chosen randomly).""" - APPEARANCE = 0x19 - """Appearance.""" - DEVICE_ADDRESS = 0x1B - """LE Bluetooth device address.""" - ROLE = 0x1C - """LE Role.""" - SERVICE_DATA_128BIT_UUID = 0x21 - """Service data with 128 bit UUID.""" - MANUFACTURER_SPECIFIC_DATA = 0xFF - """Manufacturer-specific data.""" - - MAX_DATA_SIZE = 31 - """Data size in a regular BLE packet.""" - - 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() - - @property - 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 length(self): - """Current number of bytes in packet.""" - return len(self._packet_bytes) - - @property - def bytes_remaining(self): - """Number of bytes still available for use in the packet.""" - return self._max_length - self.length - - def _check_length(self): - if self.length > self._max_length: - raise IndexError("Advertising data too long") - - def add_flags(self, flags=(FLAG_GENERAL_DISCOVERY | FLAG_LE_ONLY)): - """Add advertising flags.""" - self.add_field(self.FLAGS, struct.pack("= len(name_bytes): - self._packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME, name_bytes) - else: - self._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") - - def add_uuids(self, uuids, field_type_16_bit_uuids, field_type_128_bit_uuids): - """Add 16-bit and 128-bit uuids to the packet, using the given field types.""" - concatenated_16_bit_uuids = b''.join( - struct.pack(" 1: - raise ValueError("Only one 128 bit UUID will fit") - if uuids_128_bits: - self._packet.add_field(field_type_128_bit_uuids, uuids_128_bits[0].uuid128) - - @property - def advertising_data_bytes(self): - """The raw bytes for the initial advertising data packet.""" - return self._packet.packet_bytes - - @property - 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 - - -class ServerAdvertisement(Advertisement): - """Build an advertisement for a peripheral's services. - - There is room in the packet for only one 128-bit UUID. Giving UUIDs in the scan response - is not yet implemented. - - :param Peripheral peripheral: the Peripheral to advertise. Use its services and name. - :param int tx_power: transmit power in dBm at 0 meters (8 bit signed value). Default 0 dBm - :param int appearance: If not None, add appearance value to advertisement. - """ - - def __init__(self, peripheral, *, tx_power=0, appearance=None): - super().__init__(tx_power=tx_power, appearance=appearance) - uuids = [service.uuid for service in peripheral.services if not service.secondary] - self.add_uuids(uuids, - AdvertisingPacket.ALL_16_BIT_SERVICE_UUIDS, - AdvertisingPacket.ALL_128_BIT_SERVICE_UUIDS) - self.add_name(peripheral.name) - - -class SolicitationAdvertisement(Advertisement): - """Build an advertisement for a peripheral to solicit one or more services from a central. - - There is room in the packet for only one 128-bit UUID. Giving UUIDs in the scan response - is not yet implemented. - - :param string name: Name to use in advertisement. - :param iterable service_uuids: One or more services requested from a central - :param int tx_power: transmit power in dBm at 0 meters (8 bit signed value). Default 0 dBm. - """ - - def __init__(self, name, service_uuids, *, tx_power=0): - super().__init__() - self.add_uuids(service_uuids, - AdvertisingPacket.SOLICITED_16_BIT_SERVICE_UUIDS, - AdvertisingPacket.SOLICITED_128_BIT_SERVICE_UUIDS) - self.add_name(name) diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py new file mode 100644 index 0000000..5c0f9f0 --- /dev/null +++ b/adafruit_ble/advertising/__init__.py @@ -0,0 +1,287 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 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. +""" +Advertising is the first phase of BLE where devices can broadcast +""" + +import struct +import gc + +def to_hex(b): + """Pretty prints a byte sequence as hex values.""" + # pylint: disable=invalid-name + return " ".join(["{:02x}".format(v) for v in b]) + +def to_bytes_literal(b): + """Prints a byte sequence as a Python bytes literal that only uses hex encoding.""" + # pylint: disable=invalid-name + return "b\"" + "".join(["\\x{:02x}".format(v) for v in b]) + "\"" + +def decode_data(data, *, key_encoding="B"): + """Helper which decodes length encoded structures into a dictionary with the given key + encoding.""" + i = 0 + data_dict = {} + if len(data) > 255: + print("original", data) + raise RuntimeError() + key_size = struct.calcsize(key_encoding) + while i < len(data): + item_length = data[i] + i += 1 + if item_length == 0: + break + key = struct.unpack_from(key_encoding, data, i)[0] + value = data[i + key_size:i + item_length] + if key in data_dict: + if not isinstance(data_dict[key], list): + data_dict[key] = [data_dict[key]] + data_dict[key].append(value) + else: + data_dict[key] = value + i += item_length + return data_dict + +def compute_length(data_dict, *, key_encoding="B"): + """Computes the length of the encoded data dictionary.""" + value_size = 0 + for value in data_dict.values(): + if isinstance(value, list): + for subv in value: + value_size += len(subv) + else: + value_size += len(value) + return len(data_dict) + len(data_dict) * struct.calcsize(key_encoding) + value_size + +def encode_data(data_dict, *, key_encoding="B"): + """Helper which encodes dictionaries into length encoded structures with the given key + encoding.""" + length = compute_length(data_dict, key_encoding=key_encoding) + data = bytearray(length) + key_size = struct.calcsize(key_encoding) + i = 0 + for key, value in data_dict.items(): + if isinstance(value, list): + value = b"".join(value) + item_length = key_size + len(value) + struct.pack_into("B", data, i, item_length) + struct.pack_into(key_encoding, data, i + 1, key) + data[i + 1 + key_size: i + 1 + item_length] = bytes(value) + i += 1 + item_length + return data + +class AdvertisingDataField: + """Top level class for any descriptor classes that live in Advertisement or it's subclasses.""" + +class AdvertisingFlag: + """A single bit flag within an AdvertisingFlags object.""" + def __init__(self, bit_position): + self._bitmask = 1 << bit_position + + def __get__(self, obj, cls): + return (obj.flags & self._bitmask) != 0 + + def __set__(self, obj, value): + if value: + obj.flags |= self._bitmask + else: + obj.flags &= ~self._bitmask + +class AdvertisingFlags(AdvertisingDataField): + """Standard advertising flags""" + + limited_discovery = AdvertisingFlag(0) + """Discoverable only for a limited time period.""" + general_discovery = AdvertisingFlag(1) + """Will advertise until discovered.""" + le_only = AdvertisingFlag(2) + """BR/EDR not supported.""" + # BR/EDR flags not included here, since we don't support BR/EDR. + + def __init__(self, advertisement, advertising_data_type): + self._advertisement = advertisement + self._adt = advertising_data_type + self.flags = None + if self._adt in self._advertisement.data_dict: + self.flags = self._advertisement.data_dict[self._adt][0] + elif self._advertisement.mutable: + self.flags = 0b110 # Default to General discovery and LE Only + else: + self.flags = 0 + + def __bytes__(self): + encoded = bytearray(1) + encoded[0] = self.flags + return encoded + + def __str__(self): + parts = ["") + return " ".join(parts) + +class String(AdvertisingDataField): + """UTF-8 encoded string in an Advertisement. + + Not null terminated once encoded because length is always transmitted.""" + def __init__(self, *, advertising_data_type): + self._adt = advertising_data_type + + def __get__(self, obj, cls): + if self._adt not in obj.data_dict: + return None + return str(obj.data_dict[self._adt], "utf-8") + + def __set__(self, obj, value): + obj.data_dict[self._adt] = value.encode("utf-8") + +class Struct(AdvertisingDataField): + """`struct` encoded data in an Advertisement.""" + def __init__(self, struct_format, *, advertising_data_type): + self._format = struct_format + self._adt = advertising_data_type + + def __get__(self, obj, cls): + if self._adt not in obj.data_dict: + return None + return struct.unpack(self._format, obj.data_dict[self._adt])[0] + + def __set__(self, obj, value): + obj.data_dict[self._adt] = struct.pack(self._format, value) + + +class LazyField(AdvertisingDataField): + """Non-data descriptor useful for lazily binding a complex object to an advertisement object.""" + def __init__(self, cls, attribute_name, *, advertising_data_type, **kwargs): + self._cls = cls + self._attribute_name = attribute_name + self._adt = advertising_data_type + self._kwargs = kwargs + + def __get__(self, obj, cls): + # Return None if our object is immutable and the data is not present. + if not obj.mutable and self._adt not in obj.data_dict: + return None + print(self._adt, self._cls, repr(obj)) + bound_class = self._cls(obj, advertising_data_type=self._adt, **self._kwargs) + setattr(obj, self._attribute_name, bound_class) + obj.data_dict[self._adt] = bound_class + return bound_class + + # TODO: Add __set_name__ support to CircuitPython so that we automatically tell the descriptor + # instance the attribute name it has and the class it is on. + +class Advertisement: + """Core Advertisement type""" + prefix = b"\x00" + flags = LazyField(AdvertisingFlags, "flags", advertising_data_type=0x01) + short_name = String(advertising_data_type=0x08) + """Short local device name (shortened to fit).""" + complete_name = String(advertising_data_type=0x09) + """Complete local device name.""" + tx_power = Struct("") + return " ".join(parts) + + def __len__(self): + return compute_length(self.data_dict) + + def __repr__(self): + return "Advertisement(data={})".format(to_bytes_literal(encode_data(self.data_dict))) diff --git a/adafruit_ble/advertising/adafruit.py b/adafruit_ble/advertising/adafruit.py new file mode 100755 index 0000000..a580e71 --- /dev/null +++ b/adafruit_ble/advertising/adafruit.py @@ -0,0 +1,58 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft 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` +==================================================== + +This module provides Adafruit defined advertisements. + +Adafruit manufacturing data is key encoded like advertisement data and the Apple manufacturing data. +However, the keys are 16-bits to enable many different uses. Keys above 0xf000 can be used by +Adafruit customers for their own data. + +""" + +from . import Advertisement, LazyField +from .standard import ManufacturerData, ManufacturerDataField + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class AdafruitColor(Advertisement): + """Broadcast a single RGB color.""" + prefix = b"\x06\xff\xff\xff\x06\x00\x00" + manufacturer_data = LazyField(ManufacturerData, + "manufacturer_data", + advertising_data_type=0xff, + company_id=0x0822, + key_encoding="".format(self.uuid) + +class BoundServiceList: + """Sequence-like object of `Service` objects.""" + def __init__(self, advertisement, *, standard_services, vendor_services): + self._advertisement = advertisement + self._standard_service_fields = standard_services + self._vendor_service_fields = vendor_services + self._standard_services = [] + self._vendor_services = [] + for adt in standard_services: + if adt in self._advertisement.data_dict: + data = self._advertisement.data_dict[adt] + for i in range(len(data) // 2): + uuid = StandardUUID(data[2*i:2*(i+1)]) + if uuid in all_services_by_uuid: + self._standard_services.append(all_services_by_uuid[uuid]) + else: + self._standard_services.append(UnknownService(uuid)) + for adt in vendor_services: + if adt in self._advertisement.data_dict: + data = self._advertisement.data_dict[adt] + for i in range(len(data) // 16): + uuid = VendorUUID(data[16*i:16*(i+1)]) + if uuid in all_services_by_uuid: + self._vendor_services.append(all_services_by_uuid[uuid]) + else: + self._vendor_services.append(UnknownService(uuid)) + + def _update(self, adt, service_set): + if len(service_set) == 0: + del self._advertisement.data_dict[adt] + uuid_length = service_set[0].uuid.size // 8 + b = bytearray(len(service_set) * uuid_length) + i = 0 + for service in service_set: + service.uuid.pack_into(b, i) + i += uuid_length + self._advertisement.data_dict[adt] = b + + def __iter__(self): + all_services = list(self._standard_services) + all_services.extend(self._vendor_services) + return iter(all_services) + + # TODO: Differentiate between complete and incomplete lists. + def append(self, service): + """Append a service to the list.""" + if isinstance(service.uuid, StandardUUID) and service not in self._standard_services: + self._standard_services.append(service) + self._update(self._standard_service_fields[0], self._standard_services) + + # TODO: Differentiate between complete and incomplete lists. + def extend(self, services): + """Appends all services in the iterable to the list.""" + standard = False + vendor = False + for service in services: + if isinstance(service.uuid, StandardUUID) and service not in self._standard_services: + self._standard_services.append(service) + standard = True + elif isinstance(service.uuid, VendorUUID) and service not in self._vendor_services: + self._vendor_services.append(service) + vendor = True + + if standard: + self._update(self._standard_service_fields[0], self._standard_services) + if vendor: + self._update(self._vendor_service_fields[0], self._vendor_services) + + def __str__(self): + data = [] + for service in self._standard_services: + data.append(str(service)) + for service in self._vendor_services: + data.append(str(service)) + return " ".join(data) + +class ServiceList(AdvertisingDataField): + """Descriptor for a list of Service UUIDs that lazily binds a corresponding BoundServiceList.""" + def __init__(self, *, standard_services, vendor_services): + self.standard_services = standard_services + self.vendor_services = vendor_services + + def _present(self, obj): + for adt in self.standard_services: + if adt in obj.data_dict: + return True + for adt in self.vendor_services: + if adt in obj.data_dict: + return True + return False + + def __get__(self, obj, cls): + if not self._present(obj) and not obj.mutable: + return None + if not hasattr(obj, "_service_lists"): + obj._service_lists = {} + first_adt = self.standard_services[0] + if first_adt not in obj._service_lists: + obj._service_lists[first_adt] = BoundServiceList(obj, **self.__dict__) + return obj._service_lists[first_adt] + +class ProvideServiceAdvertisement(Advertisement): + """Advertise what services that the device makes available upon connection.""" + prefix = b"\x01\x02\x01\x03\x01\x06\x01\x07" + services = ServiceList(standard_services=[0x02, 0x03], vendor_services=[0x06, 0x07]) + """List of services the device can provide.""" + + def __init__(self, *services): + super().__init__() + if services: + self.services.extend(services) + self.connectable = True + + @classmethod + def matches(cls, entry): + return entry.matches(cls.prefix, all=False) + +class SolicitServiceAdvertisement(Advertisement): + """Advertise what services the device would like to use over a connection.""" + + solicited_services = ServiceList(standard_services=[0x14], vendor_services=[0x15]) + """List of services the device would like to use.""" + + def __init__(self, *services): + super().__init__() + self.solicited_services.extend(services) + self.connectable = True + + +class ManufacturerData: + """Encapsulates manufacturer specific keyed data bytes. The manufacturer is identified by the + company_id and the data is structured like an advertisement with a configurable key + format.""" + def __init__(self, obj, *, advertising_data_type=0xff, company_id, key_encoding="B"): + self._obj = obj + self._company_id = company_id + self._adt = advertising_data_type + + self.data = {} + self.company_id = company_id + encoded_company = struct.pack('".format(self.company_id, hex_data) + +class ManufacturerDataField: + """A single piece of data within the manufacturer specific data.""" + def __init__(self, key, key_format): + self._key = key + self._format = key_format + + def __get__(self, obj, cls): + return struct.unpack_from(self._format, obj.manufacturer_data.data[self._key])[0] + + def __set__(self, obj, value): + if not obj.mutable: + raise AttributeError() + obj.manufacturer_data.data[self._key] = struct.pack(self._format, value) + +# TODO: Handle service data. + +# SERVICE_DATA_128BIT_UUID = 0x21 +# """Service data with 128 bit UUID.""" + +# SERVICE_DATA_16_BIT_UUID = 0x16 +# """Service data with 16 bit UUID.""" diff --git a/adafruit_ble/beacon.py b/adafruit_ble/beacon.py deleted file mode 100644 index 7193b1a..0000000 --- a/adafruit_ble/beacon.py +++ /dev/null @@ -1,159 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2018 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.beacon` -==================================================== - -BLE Beacon-related classes. - -* Author(s): Dan Halbert for Adafruit Industries - -""" - -import struct -from _bleio import Peripheral - -from .advertising import AdvertisingPacket - -class Beacon: - """Base class for Beacon advertisers.""" - def __init__(self, advertising_packet): - """Set up a beacon with the given AdvertisingPacket. - - :param AdvertisingPacket advertising_packet - """ - self._broadcaster = Peripheral() - self._advertising_packet = advertising_packet - - 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.""" - self._broadcaster.stop_advertising() - - - -class LocationBeacon(Beacon): - """Advertising Beacon used for position location. - Used for Apple iBeacon, Nordic nRF Beacon, etc. - """ - # pylint: disable=too-many-arguments - 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 - e.g., 0x004c Apple, 0x0059 Nordic, etc. - :param UUID uuid: 128-bit UUID unique to application and use case, such as a vendor uuid - :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) - - Example:: - - from adafruit_ble.beacon import LocationBeacon - from adafruit_ble.uuid import UUID - test_uuid = UUID('12345678-1234-1234-1234-123456789abc') - test_company = 0xFFFF - b = LocationBeacon(test_company, test_uuid, 123, 234, -54) - b.start() - """ - - adv = AdvertisingPacket() - adv.add_flags() - adv.add_mfr_specific_data( - company_id, - b''.join(( - # 0x02 means a beacon. 0x15 (=21) is length (16 + 2 + 2 + 1) - # of the rest of the data. - b'\x02\x15', - # 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) - - -class EddystoneURLBeacon(Beacon): - """Eddystone-URL protocol beacon. - - Example:: - - from adafruit_ble.beacon import EddystoneURLBeacon - - b = EddystoneURLBeacon('https://adafru.it/4062') - b.start() - """ - _EDDYSTONE_ID = b'\xAA\xFE' - # These prefixes are replaced with a single one-byte scheme number. - _URL_SCHEMES = ( - 'http://www.', - 'https://www.', - 'http://', - 'https://' - ) - # These common domains are replaced with a single non-printing byte. - # Byte value is 0-6 for these with a '/' suffix. - # Byte value is 7-13 for these without the '/' suffix. - _SUBSTITUTIONS = ( - '.com', - '.org', - '.edu' - '.net', - '.info', - '.biz', - '.gov', - ) - - 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) - """ - - 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): - short_url = url[len(prefix):] - url_scheme_num = idx - break - if not short_url: - raise ValueError("url does not start with one of: ", self._URL_SCHEMES) - for code, subst in enumerate(self._SUBSTITUTIONS): - 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(AdvertisingPacket.SERVICE_DATA_16_BIT_UUID, - b''.join((self._EDDYSTONE_ID, - b'\x10', - struct.pack("Bluetooth on your iOS device. - After the program starts advertising, ``CIRCUITPYxxxx` will show up as a Bluetooth - device for possible connection. Tap it, and then accept the pairing request. - Then the time should print. - """ - - CTS_UUID = UUID(0x1805) - CURRENT_TIME_UUID = UUID(0x2A2B) - LOCAL_TIME_INFORMATION_UUID = UUID(0x2A0F) - - def __init__(self, name=None, tx_power=0): - self._periph = Peripheral(name) - self._advertisement = SolicitationAdvertisement(self._periph.name, - (self.CTS_UUID,), tx_power=tx_power) - self._current_time_char = self._local_time_char = None - - - def start_advertising(self): - """Start advertising to solicit a central that supports Current Time Service.""" - self._periph.start_advertising(self._advertisement.advertising_data_bytes, - scan_response=self._advertisement.scan_response_bytes) - - def stop_advertising(self): - """Stop advertising the service.""" - self._periph.stop_advertising() - - @property - def connected(self): - """True if a central connected to this peripheral.""" - return self._periph.connected - - def disconnect(self): - """Disconnect from central.""" - self._periph.disconnect() - - def _check_connected(self): - if not self.connected: - raise OSError("Not connected") - # Do discovery and pairing if not already done. - if not self._current_time_char: - self._discover() - self._periph.pair() - - def _discover(self): - """Discover service information.""" - services = self._periph.discover_remote_services((self.CTS_UUID,)) - if not services: - raise OSError("Unable to discover service") - for characteristic in services[0].characteristics: - if characteristic.uuid == self.CURRENT_TIME_UUID: - self._current_time_char = characteristic - elif characteristic.uuid == self.LOCAL_TIME_INFORMATION_UUID: - self._local_time_char = characteristic - if not self._current_time_char or not self._local_time_char: - raise OSError("Remote service missing needed characteristic") - - @property - def current_time(self): - """Get a tuple describing the current time from the server: - (year, month, day, hour, minute, second, weekday, subsecond, adjust_reason) - """ - self._check_connected() - if self._current_time_char: - # year, month, day, hour, minute, second, weekday, subsecond, adjust_reason - values = struct.unpack('> 8 + uuid128[-4] = uuid16 & 0xff + super().__init__(uuid128) + +class CircuitPythonService(Service): + """Core CircuitPython service that allows for file modification and REPL access. + Unimplemented.""" + uuid = CircuitPythonUUID(0x0100) + default_field_name = "circuitpython" + filename = StringCharacteristic(uuid=CircuitPythonUUID(0x0200), + properties=(_bleio.Characteristic.READ | + _bleio.Characteristic.WRITE)) + contents = StreamOut(uuid=CircuitPythonUUID(0x0201)) diff --git a/adafruit_ble/services/core.py b/adafruit_ble/services/core.py new file mode 100755 index 0000000..2883e3e --- /dev/null +++ b/adafruit_ble/services/core.py @@ -0,0 +1,93 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft 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. +""" +:py:mod:`~adafruit_ble.services.core` +==================================================== + +This module provides the top level Service definition. + +""" + +import _bleio + +from ..characteristics.core import Characteristic, ComplexCharacteristic + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class Service: + """Top level Service class that handles the hard work of binding to a local or remote service. + + Providers of a local service should instantiate their Service with service=None, the default. + + To use a remote Service, read the corresponding attribute on the `SmartConnection`. The + attribute names match the Service's ``default_field_name``. + """ + def __init__(self, *, service=None, secondary=False, **initial_values): + if service is None: + # pylint: disable=no-member + self._service = _bleio.Service(self.uuid._uuid, secondary=secondary) + elif not service.remote: + raise ValueError("Can only create services with a remote service or None") + else: + self._service = service + + if self._service.remote: + self.__init_remote() + else: + self.__init_local(initial_values) + + def __init_local(self, initial_values): + self._bound_characteristics = {} + for class_attr in dir(self.__class__): + if class_attr.startswith("__"): + continue + value = getattr(self.__class__, class_attr) + if isinstance(value, ComplexCharacteristic): + setattr(self, class_attr, value.bind(self._service)) + elif isinstance(value, Characteristic): + initial_value = initial_values.get(class_attr, None) + self._bound_characteristics[class_attr] = value.bind(self._service, + initial_value=initial_value) + value.field_name = class_attr + + def __init_remote(self): + remote_service = self._service + uuid_to_bc = {} + for characteristic in remote_service.characteristics: + uuid_to_bc[characteristic.uuid] = characteristic + + self._bound_characteristics = {} + for class_attr in dir(self.__class__): + if class_attr.startswith("__"): + continue + value = getattr(self.__class__, class_attr) + if not isinstance(value, Characteristic): + continue + uuid = value.uuid._uuid # pylint: disable=protected-access + if isinstance(value, ComplexCharacteristic): + setattr(self, class_attr, value.bind(uuid_to_bc[uuid])) + elif isinstance(value, Characteristic): + if uuid in uuid_to_bc: + self._bound_characteristics[class_attr] = uuid_to_bc[uuid] + value.field_name = class_attr + return self diff --git a/adafruit_ble/services/microbit.py b/adafruit_ble/services/microbit.py new file mode 100644 index 0000000..e69de29 diff --git a/adafruit_ble/services/midi.py b/adafruit_ble/services/midi.py new file mode 100644 index 0000000..66f91eb --- /dev/null +++ b/adafruit_ble/services/midi.py @@ -0,0 +1,72 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft 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. +""" +`midi` +==================================================== + +This module provides Services defined by the MIDI group. + +""" + +import _bleio + +from .core import Service +from ..uuid import VendorUUID +from ..characteristics.core import Characteristic + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class MidiIOCharacteristic(Characteristic): + """Workhorse MIDI Characteristic that carries midi messages both directions. Unimplemented.""" + uuid = VendorUUID("7772E5DB-3868-4112-A1A9-F2669D106BF3") + def __init__(self, **kwargs): + super().__init__(properties=(_bleio.Characteristic.NOTIFY | + _bleio.Characteristic.READ | + _bleio.Characteristic.WRITE | + _bleio.Characteristic.WRITE_NO_RESPONSE), **kwargs) + +class MidiService(Service): + """BLE Service that transports MIDI messages. Unimplemented.""" + uuid = VendorUUID("03B80E5A-EDE8-4B33-A751-6CE34EC4C700") + io = MidiIOCharacteristic() # pylint: disable=invalid-name + + # pylint: disable=unnecessary-pass + def write(self): + """Placeholder for transmitting midi bytes to the other device.""" + pass + # add timestamp to writes 13-bit millisecond resolution + # prepend one header byte with high timestamp bits 0b10xxxxxx + # 1. A Running Status MIDI message is allowed within the packet after at least one full MIDI + # message. + # 2. Every MIDI Status byte must be preceded by a timestamp byte. Running Status MIDI may be + # preceded by a timestamp byte. If a Running Status MIDI message is not preceded by a + # timestamp byte, the timestamp byte of the most recently preceding message in the same + # packet is used. + # 0b1xxxxxxx + # We need to be able to pack into fixed packet sizes because each packet must start with the + # high timestamp header byte. Low timestamp packets may wrap around and the decoder is + # responsible for incrementing the high bits + + def read(self): + """Placeholder for receiving midi bytes from the other device.""" + pass diff --git a/adafruit_ble/services/nordic.py b/adafruit_ble/services/nordic.py new file mode 100755 index 0000000..4f90b7c --- /dev/null +++ b/adafruit_ble/services/nordic.py @@ -0,0 +1,122 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Dan Halbert for Adafruit Industries +# Copyright (c) 2019 Scott Shawcroft 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. +""" +`nordic` +==================================================== + +This module provides Services used by Nordic Semiconductors. + +""" + +from .core import Service +from ..uuid import VendorUUID +from ..characteristics.stream import StreamOut, StreamIn + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class UARTService(Service): + """ + Provide UART-like functionality via the Nordic NUS service. + + :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. + + Example:: + + from adafruit_ble.uart_client import 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,)) + else: + raise Error("No UART servers found.") + + uart_client.write('abc') + """ + # pylint: disable=no-member + uuid = VendorUUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") + _server_tx = StreamOut(uuid=VendorUUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), + timeout=1.0, buffer_size=64) + _server_rx = StreamIn(uuid=VendorUUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), + timeout=1.0, buffer_size=64) + + default_field_name = "uart" + + def __init__(self, service=None): + super().__init__(service=service) + self.connectable = True + if not service: + self._rx = self._server_rx + self._tx = self._server_tx + else: + # If we're a client then swap the characteristics we use. + self._tx = self._server_rx + self._rx = self._server_tx + + 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._rx.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._rx.readinto(buf, nbytes) + + def readline(self): + """ + Read a line, ending in a newline character. + + :return: the line read + :rtype: int or None + """ + return self._rx.readline() + + @property + def in_waiting(self): + """The number of bytes in the input buffer, available to be read.""" + return self._rx.in_waiting + + def reset_input_buffer(self): + """Discard any unread characters in the input buffer.""" + self._rx.reset_input_buffer() + + def write(self, buf): + """Write a buffer of bytes.""" + self._tx.write(buf) diff --git a/adafruit_ble/address.py b/adafruit_ble/services/sphero.py similarity index 73% rename from adafruit_ble/address.py rename to adafruit_ble/services/sphero.py index 8c93b01..4906d97 100644 --- a/adafruit_ble/address.py +++ b/adafruit_ble/services/sphero.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # -# Copyright (c) 2019 Dan Halbert for Adafruit Industries +# Copyright (c) 2019 Scott Shawcroft 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 @@ -20,16 +20,19 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_ble.address` +`sphero` ==================================================== -BLE Address - -* Author(s): Dan Halbert for Adafruit Industries +This module provides Services used by Sphero robots. """ -from _bleio import UUID as _bleio_Address +from .core import Service +from ..uuid import VendorUUID + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" -UUID = _bleio_Address -"""`adafruit_ble.Address` is the same as `_bleio.Address`""" +class SpheroService(Service): + """Core Sphero Service. Unimplemented.""" + uuid = VendorUUID("!!orehpS OOW\x01\x00\x01\x00") diff --git a/adafruit_ble/services/standard/__init__.py b/adafruit_ble/services/standard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adafruit_ble/services/standard/device_info.py b/adafruit_ble/services/standard/device_info.py new file mode 100644 index 0000000..77dedf2 --- /dev/null +++ b/adafruit_ble/services/standard/device_info.py @@ -0,0 +1,85 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Dan Halbert for Adafruit Industries +# Copyright (c) 2019 Scott Shawcroft 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` +==================================================== + +This module provides higher-level BLE (Bluetooth Low Energy) functionality, +building on the native `_bleio` module. + +* Author(s): Dan Halbert for Adafruit Industries + +Implementation Notes +-------------------- + +**Hardware:** + + Adafruit Feather nRF52840 Express + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import binascii +import os +import sys +import microcontroller + +from ..core import Service +from ...core.uuid import StandardUUID +from ...characteristics.string import FixedStringCharacteristic + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class DeviceInfoService(Service): + """Device information""" + uuid = StandardUUID(0x180a) + default_field_name = "device_info" + model_number = FixedStringCharacteristic(uuid=StandardUUID(0x2a24)) + serial_number = FixedStringCharacteristic(uuid=StandardUUID(0x2a25)) + firmware_revision = FixedStringCharacteristic(uuid=StandardUUID(0x2a26)) + hardware_revision = FixedStringCharacteristic(uuid=StandardUUID(0x2a27)) + software_revision = FixedStringCharacteristic(uuid=StandardUUID(0x2a28)) + manufacturer = FixedStringCharacteristic(uuid=StandardUUID(0x2a29)) + + def __init__(self, *, manufacturer, + software_revision, + model_number=None, + serial_number=None, + firmware_revision=None): + if model_number is None: + model_number = sys.platform + if serial_number is None: + serial_number = binascii.hexlify(microcontroller.cpu.uid).decode('utf-8') # pylint: disable=no-member + + if firmware_revision is None: + firmware_revision = os.uname().version + super().__init__(manufacturer=manufacturer, + software_revision=software_revision, + model_number=model_number, + serial_number=serial_number, + firmware_revision=firmware_revision) diff --git a/adafruit_ble/services/standard/hid.py b/adafruit_ble/services/standard/hid.py new file mode 100755 index 0000000..2ba63c0 --- /dev/null +++ b/adafruit_ble/services/standard/hid.py @@ -0,0 +1,288 @@ +# 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.services.standard.hid` +==================================================== + +BLE Human Interface Device (HID) + +* Author(s): Dan Halbert for Adafruit Industries + +""" +import struct + +from micropython import const + +import _bleio +from adafruit_ble.characteristics.core import BytesCharacteristic +from adafruit_ble.characteristics.int import Uint8Characteristic +from adafruit_ble.uuid import StandardUUID + +from ..core import Service + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +_HID_SERVICE_UUID_NUM = const(0x1812) +_REPORT_UUID_NUM = const(0x2A4D) +_REPORT_MAP_UUID_NUM = const(0x2A4B) +_HID_INFORMATION_UUID_NUM = const(0x2A4A) +_HID_CONTROL_POINT_UUID_NUM = const(0x2A4C) +_REPORT_REF_DESCR_UUID_NUM = const(0x2908) +_REPORT_REF_DESCR_UUID = _bleio.UUID(_REPORT_REF_DESCR_UUID_NUM) +_PROTOCOL_MODE_UUID_NUM = const(0x2A4E) + +_APPEARANCE_HID_KEYBOARD = const(961) +_APPEARANCE_HID_MOUSE = const(962) +_APPEARANCE_HID_JOYSTICK = const(963) +_APPEARANCE_HID_GAMEPAD = const(964) + + +# Boot keyboard and mouse not currently supported. +_BOOT_KEYBOARD_INPUT_REPORT_UUID_NUM = const(0x2A22) +_BOOT_KEYBOARD_OUTPUT_REPORT_UUID_NUM = const(0x2A32) +_BOOT_MOUSE_INPUT_REPORT_UUID_NUM = const(0x2A33) + +# Output reports not currently implemented (e.g. LEDs on keyboard) +_REPORT_TYPE_INPUT = const(1) +_REPORT_TYPE_OUTPUT = const(2) + +# Boot Protocol mode not currently implemented +_PROTOCOL_MODE_BOOT = b'\x00' +_PROTOCOL_MODE_REPORT = b'\x01' + +class ReportIn: + """A single HID report that transmits HID data into a client.""" + uuid = StandardUUID(0x24ad) + def __init__(self, service, report_id, usage_page, usage, *, max_length): + self._characteristic = _bleio.Characteristic.add_to_service( + service._service, + self.uuid._uuid, + properties=_bleio.Characteristic.READ | _bleio.Characteristic.NOTIFY, + read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, write_perm=_bleio.Attribute.NO_ACCESS, + max_length=max_length, fixed_length=True) + self._report_id = report_id + self.usage_page = usage_page + self.usage = usage + + _bleio.Descriptor.add_to_characteristic( + self._characteristic, _REPORT_REF_DESCR_UUID, + read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, write_perm=_bleio.Attribute.NO_ACCESS, + initial_value=struct.pack('> 4 + _type = (b & 0b1100) >> 2 + size = b & 0b11 + size = 4 if size == 3 else size + i += 1 + data = hid_descriptor[i:i+size] + if _type == _ITEM_TYPE_GLOBAL: + global_table[tag] = data + elif _type == _ITEM_TYPE_MAIN: + if tag == _MAIN_ITEM_TAG_START_COLLECTION: + collections.append({"type": data, + "locals": list(local_table), + "globals": list(global_table), + "mains": []}) + elif tag == _MAIN_ITEM_TAG_END_COLLECTION: + collection = collections.pop() + # This is a top level collection if the collections list is now empty. + if not collections: + top_level_collections.append(collection) + else: + collections[-1]["mains"].append(collection) + elif tag == _MAIN_ITEM_TAG_INPUT: + collections[-1]["mains"].append({"tag": "input", + "locals": list(local_table), + "globals": list(global_table)}) + elif tag == _MAIN_ITEM_TAG_OUTPUT: + collections[-1]["mains"].append({"tag": "output", + "locals": list(local_table), + "globals": list(global_table)}) + else: + raise RuntimeError("Unsupported main item in HID descriptor") + local_table = [None] * 3 + else: + local_table[tag] = data + + i += size + + def get_report_info(collection, reports): + for main in collection["mains"]: + if "type" in main: + get_report_info(main, reports) + else: + report_size, report_id, report_count = [x[0] for x in main["globals"][7:10]] + if report_id not in reports: + reports[report_id] = {"input_size": 0, "output_size": 0} + if main["tag"] == "input": + reports[report_id]["input_size"] += report_size * report_count + elif main["tag"] == "output": + reports[report_id]["output_size"] += report_size * report_count + + + for collection in top_level_collections: + if collection["type"][0] != 1: + raise NotImplementedError("Only Application top level collections supported.") + usage_page = collection["globals"][0][0] + usage = collection["locals"][0][0] + reports = {} + get_report_info(collection, reports) + if len(reports) > 1: + raise NotImplementedError("Only on report id per Application collection supported") + + report_id, report = list(reports.items())[0] + output_size = report["output_size"] + if output_size > 0: + self.devices.append(ReportOut(self, report_id, usage_page, usage, + max_length=output_size // 8)) + + input_size = reports[report_id]["input_size"] + if input_size > 0: + self.devices.append(ReportIn(self, report_id, usage_page, usage, + max_length=input_size // 8)) + + + @classmethod + def from_remote_service(cls, remote_service): + """Creates a HIDService from a remote service""" + self = super(cls).from_remote_service(remote_service) + self._init_devices() # pylint: disable=protected-access diff --git a/adafruit_ble/services/standard/standard.py b/adafruit_ble/services/standard/standard.py new file mode 100755 index 0000000..d29c5de --- /dev/null +++ b/adafruit_ble/services/standard/standard.py @@ -0,0 +1,88 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft 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.standard.standard` +==================================================== + +This module provides Service classes for BLE defined standard services. + +""" + +import time + +from ..core import Service +from ...core.uuid import StandardUUID +from ..characteristics.string import StringCharacteristic +from ..characteristics.core import StructCharacteristic +from ..characteristics.int import Uint8Characteristic + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class AppearanceCharacteristic(StructCharacteristic): + """What type of device it is""" + uuid = StandardUUID(0x2a01) + + def __init__(self, **kwargs): + super().__init__(" closest_rssi or now - closest_last_time > 0.4: + closest = entry.address + else: + continue + closest_rssi = entry.rssi + closest_last_time = now + neopixels.fill(entry.color) + adapter.stop_scan() diff --git a/examples/ble_demo_central.py b/examples/ble_demo_central.py index eb56a9c..1482594 100644 --- a/examples/ble_demo_central.py +++ b/examples/ble_demo_central.py @@ -8,41 +8,42 @@ import board from analogio import AnalogIn +# This hasn't been updated. + #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 +import adafruit_ble +from adafruit_ble.services.nordic import UARTService 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() a3 = AnalogIn(board.A3) a4 = AnalogIn(board.A4) a5 = AnalogIn(board.A5) while True: - uart_addresses = [] - # Keep trying to find a UART peripheral - while not uart_addresses: - uart_addresses = uart_client.scan(scanner) - uart_client.connect(uart_addresses[0], 5) - - while uart_client.connected: - r = scale(a3.value) - g = scale(a4.value) - b = scale(a5.value) - - color = (r, g, b) - print(color) - color_packet = ColorPacket(color) + for peer in scanner.scan(UARTService): + peer.connect() + + r = scale(a3.value) + g = scale(a4.value) + b = scale(a5.value) + + color = (r, g, b) + print(color) + color_packet = ColorPacket(color) + for peer in adafruit_ble.peers: + if not hasattr(peer, "uart"): + continue try: - uart_client.write(color_packet.to_bytes()) + peer.uart.write(color_packet.to_bytes()) except OSError: pass - time.sleep(0.3) + time.sleep(0.3) diff --git a/examples/ble_demo_periph.py b/examples/ble_demo_periph.py index 83bb610..c67547b 100644 --- a/examples/ble_demo_periph.py +++ b/examples/ble_demo_periph.py @@ -6,12 +6,13 @@ import board import neopixel -from adafruit_ble.uart_server import UARTServer +import adafruit_ble +from adafruit_ble.services.nordic import UARTService 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() +# This hasn't been updated. NUM_PIXELS = 32 np = neopixel.NeoPixel(board.D10, NUM_PIXELS, brightness=0.1) @@ -21,14 +22,11 @@ def mod(i): """Wrap i to modulus NUM_PIXELS.""" return i % NUM_PIXELS +adafruit_ble.add_local_service(UARTService) +adafruit_ble.advertise(UARTService) 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) + for peer in adafruit_ble.peers: + packet = Packet.from_stream(peer.local.uart) if isinstance(packet, ColorPacket): print(packet.color) np[next_pixel] = packet.color diff --git a/examples/ble_eddystone_test.py b/examples/ble_eddystone_test.py index ed0b8c5..5e70c34 100644 --- a/examples/ble_eddystone_test.py +++ b/examples/ble_eddystone_test.py @@ -1,4 +1,9 @@ + +# This hasn't been updated. + from adafruit_ble.beacon import EddystoneURLBeacon beacon = EddystoneURLBeacon('https://adafru.it/4062') -beacon.start() + +while True: + pass diff --git a/examples/ble_hid_central.py b/examples/ble_hid_central.py new file mode 100644 index 0000000..5713811 --- /dev/null +++ b/examples/ble_hid_central.py @@ -0,0 +1,31 @@ +""" +Demonstration of a Bluefruit BLE Central. Connects to the first BLE HID peripheral it finds. +""" + +import time + +import board + +import adafruit_ble +from adafruit_ble.services.standard.hid import HIDService +from adafruit_ble.core.scanner import Scanner + +# This hasn't been updated. + +adafruit_ble.detect_service(HIDService) + +scanner = Scanner() + +while True: + print("scanning") + results = [] + while not results: + results = scanner.scan(HIDService, timeout=4) + + peer = results[0].connect(timeout=10, pair=True) + print(peer) + print(peer.hid.protocol_mode) + print(peer.hid.report_map) + print(peer.hid.devices) + + time.sleep(0.2) diff --git a/examples/ble_hid_periph.py b/examples/ble_hid_periph.py new file mode 100644 index 0000000..39093b6 --- /dev/null +++ b/examples/ble_hid_periph.py @@ -0,0 +1,153 @@ +""" +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 adafruit_ble +import board +import sys +import time +from adafruit_ble import SmartAdapter +from adafruit_ble.advertising import to_hex +from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble.services.standard.hid import HIDService +from adafruit_ble.services.standard.device_info import DeviceInfoService +from adafruit_hid.keyboard import Keyboard +from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS + +# This has been updated but isn't working. + +#pylint: disable=line-too-long +HID_DESCRIPTOR = ( + b'\x05\x01' # Usage Page (Generic Desktop Ctrls) + b'\x09\x06' # Usage (Keyboard) + b'\xA1\x01' # Collection (Application) + b'\x85\x01' # Report ID (1) + b'\x05\x07' # Usage Page (Kbrd/Keypad) + b'\x19\xE0' # Usage Minimum (\xE0) + b'\x29\xE7' # Usage Maximum (\xE7) + b'\x15\x00' # Logical Minimum (0) + b'\x25\x01' # Logical Maximum (1) + b'\x75\x01' # Report Size (1) + b'\x95\x08' # Report Count (8) + b'\x81\x02' # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + b'\x81\x01' # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + b'\x19\x00' # Usage Minimum (\x00) + b'\x29\x65' # Usage Maximum (\x65) + b'\x15\x00' # Logical Minimum (0) + b'\x25\x65' # Logical Maximum (101) + b'\x75\x08' # Report Size (8) + b'\x95\x06' # Report Count (6) + b'\x81\x00' # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + b'\x05\x08' # Usage Page (LEDs) + b'\x19\x01' # Usage Minimum (Num Lock) + b'\x29\x05' # Usage Maximum (Kana) + b'\x15\x00' # Logical Minimum (0) + b'\x25\x01' # Logical Maximum (1) + b'\x75\x01' # Report Size (1) + b'\x95\x05' # Report Count (5) + b'\x91\x02' # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + b'\x95\x03' # Report Count (3) + b'\x91\x01' # Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + b'\xC0' # End Collection + b'\x05\x01' # Usage Page (Generic Desktop Ctrls) + b'\x09\x02' # Usage (Mouse) + b'\xA1\x01' # Collection (Application) + b'\x09\x01' # Usage (Pointer) + b'\xA1\x00' # Collection (Physical) + b'\x85\x02' # Report ID (2) + b'\x05\x09' # Usage Page (Button) + b'\x19\x01' # Usage Minimum (\x01) + b'\x29\x05' # Usage Maximum (\x05) + b'\x15\x00' # Logical Minimum (0) + b'\x25\x01' # Logical Maximum (1) + b'\x95\x05' # Report Count (5) + b'\x75\x01' # Report Size (1) + b'\x81\x02' # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + b'\x95\x01' # Report Count (1) + b'\x75\x03' # Report Size (3) + b'\x81\x01' # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + b'\x05\x01' # Usage Page (Generic Desktop Ctrls) + b'\x09\x30' # Usage (X) + b'\x09\x31' # Usage (Y) + b'\x15\x81' # Logical Minimum (-127) + b'\x25\x7F' # Logical Maximum (127) + b'\x75\x08' # Report Size (8) + b'\x95\x02' # Report Count (2) + b'\x81\x06' # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + b'\x09\x38' # Usage (Wheel) + b'\x15\x81' # Logical Minimum (-127) + b'\x25\x7F' # Logical Maximum (127) + b'\x75\x08' # Report Size (8) + b'\x95\x01' # Report Count (1) + b'\x81\x06' # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) + b'\xC0' # End Collection + b'\xC0' # End Collection + b'\x05\x0C' # Usage Page (Consumer) + b'\x09\x01' # Usage (Consumer Control) + b'\xA1\x01' # Collection (Application) + b'\x85\x03' # Report ID (3) + b'\x75\x10' # Report Size (16) + b'\x95\x01' # Report Count (1) + b'\x15\x01' # Logical Minimum (1) + b'\x26\x8C\x02' # Logical Maximum (652) + b'\x19\x01' # Usage Minimum (Consumer Control) + b'\x2A\x8C\x02' # Usage Maximum (AC Send) + b'\x81\x00' # Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + b'\xC0' # End Collection + # b'\x05\x01' # Usage Page (Generic Desktop Ctrls) + # b'\x09\x05' # Usage (Game Pad) + # b'\xA1\x01' # Collection (Application) + # b'\x85\x05' # Report ID (5) + # b'\x05\x09' # Usage Page (Button) + # b'\x19\x01' # Usage Minimum (\x01) + # b'\x29\x10' # Usage Maximum (\x10) + # b'\x15\x00' # Logical Minimum (0) + # b'\x25\x01' # Logical Maximum (1) + # b'\x75\x01' # Report Size (1) + # b'\x95\x10' # Report Count (16) + # b'\x81\x02' # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + # b'\x05\x01' # Usage Page (Generic Desktop Ctrls) + # b'\x15\x81' # Logical Minimum (-127) + # b'\x25\x7F' # Logical Maximum (127) + # b'\x09\x30' # Usage (X) + # b'\x09\x31' # Usage (Y) + # b'\x09\x32' # Usage (Z) + # b'\x09\x35' # Usage (Rz) + # b'\x75\x08' # Report Size (8) + # b'\x95\x04' # Report Count (4) + # b'\x81\x02' # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + # b'\xC0' # End Collection +) +#pylint: enable=line-too-long + +hid = HIDService(HID_DESCRIPTOR) +device_info = DeviceInfoService(software_revision=adafruit_ble.__version__, + manufacturer="Adafruit Industries") +print(device_info.manufacturer) +advertisement = ProvideServiceAdvertisement(hid) +advertisement.appearance = 961 + +adapter = SmartAdapter() +print(to_hex(bytes(advertisement))) +if not adapter.connected: + print("advertising") + adapter.start_advertising(advertisement) +else: + print("already connected") + print(adapter.connections) + +k = Keyboard(hid.devices) +kl = KeyboardLayoutUS(k) +while True: + while not adapter.connected: + pass + #print("Start typing:") + while adapter.connected: + # c = sys.stdin.read(1) + # sys.stdout.write(c) + #kl.write(c) + kl.write("h") + # print("sleeping") + time.sleep(0.1) + adapter.start_advertising(advertisement) diff --git a/examples/ble_scan_everything.py b/examples/ble_scan_everything.py new file mode 100644 index 0000000..cd91416 --- /dev/null +++ b/examples/ble_scan_everything.py @@ -0,0 +1,12 @@ +from adafruit_ble import SmartAdapter + +adapter = SmartAdapter() +print("scanning") +found = set() +for entry in adapter.start_scan(timeout=60, minimum_rssi=-80): + addr = entry.address + if addr not in found: + print(entry) + found.add(addr) + +print("scan done") diff --git a/examples/ble_uart_echo_client.py b/examples/ble_uart_echo_client.py new file mode 100644 index 0000000..e816af8 --- /dev/null +++ b/examples/ble_uart_echo_client.py @@ -0,0 +1,37 @@ +import time + +import adafruit_ble + +from adafruit_ble import SmartAdapter +from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble.services.nordic import UARTService + +# This should work. + +adapter = SmartAdapter() + +adafruit_ble.recognize_services(UARTService) + +connection = None +if adapter.connected: + connection = adapter.connections[0] + +while True: + print(connection, connection.connected if connection is not None else False) + while connection is not None and connection.connected: + print("echo") + connection.uart.write(b"echo") + # Returns b'' if nothing was read. + one_byte = connection.uart.read(4) + if one_byte: + print(one_byte) + print() + time.sleep(1) + + print("disconnected, scanning") + for entry in adapter.start_scan((ProvideServiceAdvertisement,), timeout=1): + if UARTService in entry.services: + connection = adapter.connect(entry) + break + + adapter.stop_scan() diff --git a/examples/ble_uart_echo_test.py b/examples/ble_uart_echo_test.py index 82bec77..d9cf9df 100644 --- a/examples/ble_uart_echo_test.py +++ b/examples/ble_uart_echo_test.py @@ -1,19 +1,22 @@ -from adafruit_ble.uart_server import UARTServer +import adafruit_ble -uart = UARTServer() +from adafruit_ble import SmartAdapter +from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble.services.nordic import UARTService -while True: - uart.start_advertising() +# This should work. - # Wait for a connection - while not uart.connected: - pass +adapter = SmartAdapter() +uart = UARTService() +advertisement = ProvideServiceAdvertisement(uart) - while uart.connected: +while True: + adapter.start_advertising(advertisement) + while not adapter.connected: + pass + while adapter.connected: # Returns b'' if nothing was read. one_byte = uart.read(1) if one_byte: + print(one_byte) uart.write(one_byte) - - # When disconnected, arrive here. Go back to the top - # and start advertising again. From 8e0d39bfdb2e07f88dad78b8a35e11ec1d264177 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 30 Oct 2019 14:07:16 -0400 Subject: [PATCH 2/4] Get RGB central and periph demo working --- adafruit_ble/advertising/__init__.py | 2 +- examples/ble_demo_central.py | 64 +++++++++++++++++----------- examples/ble_demo_periph.py | 22 ++++++---- 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index 5c0f9f0..cdc787b 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -90,7 +90,7 @@ def encode_data(data_dict, *, key_encoding="B"): return data class AdvertisingDataField: - """Top level class for any descriptor classes that live in Advertisement or it's subclasses.""" + """Top level class for any descriptor classes that live in Advertisement or its subclasses.""" class AdvertisingFlag: """A single bit flag within an AdvertisingFlags object.""" diff --git a/examples/ble_demo_central.py b/examples/ble_demo_central.py index 1482594..89c23f6 100644 --- a/examples/ble_demo_central.py +++ b/examples/ble_demo_central.py @@ -7,43 +7,57 @@ import board from analogio import AnalogIn - -# This hasn't been updated. - -#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 - import adafruit_ble +from adafruit_ble import SmartAdapter +from adafruit_ble.advertising.standard import ProvideServiceAdvertisement from adafruit_ble.services.nordic import UARTService -from adafruit_ble.scanner import Scanner + +from adafruit_bluefruit_connect.color_packet import ColorPacket def scale(value): """Scale an value from 0-65535 (AnalogIn range) to 0-255 (RGB range)""" return int(value / 65535 * 255) -scanner = Scanner() +adapter = SmartAdapter() +adafruit_ble.recognize_services(UARTService) a3 = AnalogIn(board.A3) a4 = AnalogIn(board.A4) a5 = AnalogIn(board.A5) +uart_connection = None +# See if any existing connections are providing UARTService. +if adapter.connected: + for conn in adapter.connections: + if hasattr(conn, "uart"): + uart_connection = conn + break + while True: - for peer in scanner.scan(UARTService): - peer.connect() - - r = scale(a3.value) - g = scale(a4.value) - b = scale(a5.value) - - color = (r, g, b) - print(color) - color_packet = ColorPacket(color) - for peer in adafruit_ble.peers: - if not hasattr(peer, "uart"): - continue + if not uart_connection: + print("Scanning...") + for adv in adapter.start_scan((ProvideServiceAdvertisement,), timeout=5): + if UARTService in adv.services: + print("found a UARTService advertisement") + uart_connection = adapter.connect(adv) + break + # Stop scanning whether or not we are connected. + adapter.stop_scan() + + while uart_connection and uart_connection.connected: + r = scale(a3.value) + g = scale(a4.value) + b = scale(a5.value) + + color = (r, g, b) + print(color) + color_packet = ColorPacket(color) try: - peer.uart.write(color_packet.to_bytes()) + uart_connection.uart.write(color_packet.to_bytes()) except OSError: - pass - time.sleep(0.3) + try: + uart_connection.disconnect() + except: + pass + uart_connection = None + time.sleep(0.3) diff --git a/examples/ble_demo_periph.py b/examples/ble_demo_periph.py index c67547b..20786dc 100644 --- a/examples/ble_demo_periph.py +++ b/examples/ble_demo_periph.py @@ -6,14 +6,14 @@ import board import neopixel -import adafruit_ble +from adafruit_ble import SmartAdapter +from adafruit_ble.advertising.standard import ProvideServiceAdvertisement from adafruit_ble.services.nordic import UARTService -from adafruit_bluefruit_connect.packet import Packet + # Only the packet classes that are imported will be known to Packet. +from adafruit_bluefruit_connect.packet import Packet from adafruit_bluefruit_connect.color_packet import ColorPacket -# This hasn't been updated. - NUM_PIXELS = 32 np = neopixel.NeoPixel(board.D10, NUM_PIXELS, brightness=0.1) next_pixel = 0 @@ -22,11 +22,17 @@ def mod(i): """Wrap i to modulus NUM_PIXELS.""" return i % NUM_PIXELS -adafruit_ble.add_local_service(UARTService) -adafruit_ble.advertise(UARTService) + +adapter = SmartAdapter() +uart = UARTService() +advertisement = ProvideServiceAdvertisement(uart) + while True: - for peer in adafruit_ble.peers: - packet = Packet.from_stream(peer.local.uart) + adapter.start_advertising(advertisement) + while not adapter.connected: + pass + while adapter.connected: + packet = Packet.from_stream(uart) if isinstance(packet, ColorPacket): print(packet.color) np[next_pixel] = packet.color From 3434bfa5d31c195a4f07276f0e894ef13a9ef85b Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Fri, 1 Nov 2019 11:48:34 -0700 Subject: [PATCH 3/4] Tweaks based on feedback from Dan and Thea This renames Smart* to BLE* and removes the smart recognition. It is replaced by knowing the type of what we're interested at use time only. Only printing Service lists is now dumber. Interal variables to _bleio classes are now public as bleio_* instead so that other classes in the library can access them and its clearer what they are. --- adafruit_ble/__init__.py | 145 ++++++++---------- adafruit_ble/advertising/__init__.py | 18 +-- adafruit_ble/advertising/adafruit.py | 22 ++- adafruit_ble/advertising/standard.py | 63 ++++---- adafruit_ble/characteristics/__init__.py | 141 +++++++++++++++++ adafruit_ble/characteristics/core.py | 120 --------------- adafruit_ble/characteristics/int.py | 2 +- adafruit_ble/characteristics/stream.py | 35 ++--- adafruit_ble/characteristics/string.py | 21 +-- adafruit_ble/services/__init__.py | 88 +++++++++++ adafruit_ble/services/apple.py | 6 +- adafruit_ble/services/circuitpython.py | 3 +- adafruit_ble/services/core.py | 93 ----------- adafruit_ble/services/midi.py | 4 +- adafruit_ble/services/nordic.py | 4 +- adafruit_ble/services/sphero.py | 2 +- adafruit_ble/services/standard/hid.py | 57 ++++--- adafruit_ble/uuid/__init__.py | 16 +- docs/characteristics.rst | 2 +- docs/conf.py | 2 + docs/examples.rst | 14 +- docs/micropython_mock.py | 2 + docs/services.rst | 2 +- ...icker.py => ble_bluefruit_color_picker.py} | 15 +- examples/ble_bluefruit_connect_plotter.py | 15 +- examples/ble_color_proximity.py | 91 +++++------ examples/ble_demo_central.py | 57 +++---- examples/ble_demo_periph.py | 20 +-- examples/ble_detailed_scan.py | 27 ++++ examples/ble_eddystone_test.py | 9 -- examples/ble_hid_central.py | 38 ++--- examples/ble_hid_periph.py | 26 ++-- examples/ble_scan_everything.py | 12 -- examples/ble_simpletest.py | 23 ++- examples/ble_uart_echo_client.py | 55 ++++--- examples/ble_uart_echo_test.py | 20 +-- 36 files changed, 658 insertions(+), 612 deletions(-) delete mode 100755 adafruit_ble/characteristics/core.py delete mode 100755 adafruit_ble/services/core.py create mode 100644 docs/micropython_mock.py rename examples/{ble_color_picker.py => ble_bluefruit_color_picker.py} (55%) create mode 100644 examples/ble_detailed_scan.py delete mode 100644 examples/ble_eddystone_test.py delete mode 100644 examples/ble_scan_everything.py diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 9e6d9b4..3fce1b0 100755 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -46,75 +46,61 @@ import _bleio import board -from .services.core import Service +from .services import Service from .advertising import Advertisement __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" -# These are internal data structures used throughout the library to recognize certain Services and -# Advertisements. -# pylint: disable=invalid-name -all_services_by_name = {} -all_services_by_uuid = {} -known_advertisements = set() -# pylint: enable=invalid-name - -def recognize_services(*service_classes): - """Instruct the adafruit_ble library to recognize the given Services. - - This will cause the Service related advertisements to show the corresponding class. - `SmartConnection` will automatically have attributes for any recognized service available - from the peer.""" - for service_class in service_classes: - if not issubclass(service_class, Service): - raise ValueError("Can only detect subclasses of Service") - all_services_by_name[service_class.default_field_name] = service_class - all_services_by_uuid[service_class.uuid] = service_class - -def recognize_advertisement(*advertisements): - """Instruct the adafruit_ble library to recognize the given `Advertisement` types. - - When an advertisement is recognized by the `SmartAdapter`, it will be returned from the - start_scan iterator instead of a generic `Advertisement`.""" - known_advertisements.add(*advertisements) - -class SmartConnection: +class BLEConnection: """This represents a connection to a peer BLE device. - Its smarts come from its ability to recognize Services available on the peer and make them - available as attributes on the Connection. Use `recognize_services` to register all services - of interest. All subsequent Connections will then recognize the service. - - ``dir(connection)`` will show all attributes including recognized Services. - """ + It acts as a map from a Service type to a Service instance for the connection. + """ def __init__(self, connection): self._connection = connection - - def __dir__(self): - discovered = [] - results = self._connection.discover_remote_services() - for service in results: - uuid = service.uuid - if uuid in all_services_by_uuid: - service = all_services_by_uuid[uuid] - discovered.append(service.default_field_name) - super_dir = dir(super()) - super_dir.extend(discovered) - return super_dir - - def __getattr__(self, name): - if name in self.__dict__: - return self.__dict__[name] - if name in all_services_by_name: - service = all_services_by_name[name] - uuid = service.uuid._uuid - results = self._connection.discover_remote_services((uuid,)) + self._discovered_services = {} + """These are the bare remote services from _bleio.""" + + self._constructed_services = {} + """These are the Service instances from the library that wrap the remote services.""" + + def _discover_remote(self, uuid): + remote_service = None + if uuid in self._discovered_services: + remote_service = self._discovered_services[uuid] + else: + results = self._connection.discover_remote_services((uuid.bleio_uuid,)) if results: - remote_service = service(service=results[0]) - setattr(self, name, remote_service) - return remote_service - raise AttributeError() + remote_service = results[0] + self._discovered_services[uuid] = remote_service + return remote_service + + def __contains__(self, key): + uuid = key + if hasattr(key, "uuid"): + uuid = key.uuid + return self._discover_remote(uuid) is not None + + def __getitem__(self, key): + uuid = key + maybe_service = False + if hasattr(key, "uuid"): + uuid = key.uuid + maybe_service = True + + remote_service = self._discover_remote(uuid) + + if uuid in self._constructed_services: + return self._constructed_services[uuid] + if remote_service: + constructed_service = None + if maybe_service: + constructed_service = key(service=remote_service) + self._constructed_services[uuid] = constructed_service + return constructed_service + + raise KeyError("{!r} object has no service {}".format(self, key)) @property def connected(self): @@ -125,10 +111,11 @@ def disconnect(self): """Disconnect from peer.""" self._connection.disconnect() -class SmartAdapter: - """This BLE Adapter class enhances the normal `_bleio.Adapter`. +class BLERadio: + """The BLERadio class enhances the normal `_bleio.Adapter`. + + It uses the library's `Advertisement` classes and the `BLEConnection` class.""" - It uses the library's `Advertisement` classes and the `SmartConnection` class.""" def __init__(self, adapter=None): if not adapter: adapter = _bleio.adapter @@ -143,7 +130,6 @@ def start_advertising(self, advertisement, scan_response=None, **kwargs): scan_response_data = None if scan_response: scan_response_data = bytes(scan_response) - print(advertisement.connectable) self._adapter.start_advertising(bytes(advertisement), scan_response=scan_response_data, connectable=advertisement.connectable, @@ -153,21 +139,20 @@ def stop_advertising(self): """Stops advertising.""" self._adapter.stop_advertising() - def start_scan(self, advertisement_types=None, **kwargs): - """Starts scanning. Returns an iterator of Advertisements that are either recognized or - in advertisment_types (which will be subsequently recognized.) The iterator will block - until an advertisement is heard or the scan times out. + def start_scan(self, *advertisement_types, **kwargs): + """Starts scanning. Returns an iterator of advertisement objects of the types given in + advertisement_types. The iterator will block until an advertisement is heard or the scan + times out. - If a list ``advertisement_types`` is given, only Advertisements of that type are produced - by the returned iterator.""" + If any ``advertisement_types`` are given, only Advertisements of those types are produced + by the returned iterator. If none are given then `Advertisement` objects will be + returned.""" prefixes = b"" if advertisement_types: - recognize_advertisement(*advertisement_types) - if len(advertisement_types) == 1: - prefixes = advertisement_types[0].prefix + prefixes = b"".join(adv.prefix for adv in advertisement_types) for entry in self._adapter.start_scan(prefixes=prefixes, **kwargs): adv_type = Advertisement - for possible_type in known_advertisements: + for possible_type in advertisement_types: if possible_type.matches(entry) and issubclass(possible_type, adv_type): adv_type = possible_type advertisement = adv_type.from_entry(entry) @@ -182,9 +167,9 @@ def stop_scan(self): self._adapter.stop_scan() def connect(self, advertisement, *, timeout=4): - """Initiates a `SmartConnection` to the peer that advertised the given advertisement.""" + """Initiates a `BLEConnection` to the peer that advertised the given advertisement.""" connection = self._adapter.connect(advertisement.address, timeout=timeout) - self._connection_cache[connection] = SmartConnection(connection) + self._connection_cache[connection] = BLEConnection(connection) return self._connection_cache[connection] @property @@ -194,12 +179,12 @@ def connected(self): @property def connections(self): - """A tuple of active `SmartConnection` objects.""" + """A tuple of active `BLEConnection` objects.""" connections = self._adapter.connections - smart_connections = [None] * len(connections) + wrapped_connections = [None] * len(connections) for i, connection in enumerate(self._adapter.connections): if connection not in self._connection_cache: - self._connection_cache[connection] = SmartConnection(connection) - smart_connections[i] = self._connection_cache[connection] + self._connection_cache[connection] = BLEConnection(connection) + wrapped_connections[i] = self._connection_cache[connection] - return tuple(smart_connections) + return tuple(wrapped_connections) diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index cdc787b..8125a25 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -24,7 +24,6 @@ """ import struct -import gc def to_hex(b): """Pretty prints a byte sequence as hex values.""" @@ -41,9 +40,6 @@ def decode_data(data, *, key_encoding="B"): encoding.""" i = 0 data_dict = {} - if len(data) > 255: - print("original", data) - raise RuntimeError() key_size = struct.calcsize(key_encoding) while i < len(data): item_length = data[i] @@ -128,6 +124,9 @@ def __init__(self, advertisement, advertising_data_type): else: self.flags = 0 + def __len__(self): + return 1 + def __bytes__(self): encoded = bytearray(1) encoded[0] = self.flags @@ -185,7 +184,6 @@ def __get__(self, obj, cls): # Return None if our object is immutable and the data is not present. if not obj.mutable and self._adt not in obj.data_dict: return None - print(self._adt, self._cls, repr(obj)) bound_class = self._cls(obj, advertising_data_type=self._adt, **self._kwargs) setattr(obj, self._attribute_name, bound_class) obj.data_dict[self._adt] = bound_class @@ -196,7 +194,7 @@ def __get__(self, obj, cls): class Advertisement: """Core Advertisement type""" - prefix = b"\x00" + prefix = b"\x00" # This is an empty prefix and will match everything. flags = LazyField(AdvertisingFlags, "flags", advertising_data_type=0x01) short_name = String(advertising_data_type=0x08) """Short local device name (shortened to fit).""" @@ -240,7 +238,7 @@ def __init__(self): @classmethod def from_entry(cls, entry): """Create an Advertisement based on the given ScanEntry. This is done automatically by - `SmartAdapter` for all scan results.""" + `BLERadio` for all scan results.""" self = cls() self.data_dict = decode_data(entry.advertisement_bytes) self.address = entry.address @@ -258,10 +256,10 @@ def rssi(self): @classmethod def matches(cls, entry): - """Returns true if the given `_bleio.ScanEntry` matches all portions of the Advertisement type's - prefix.""" + """Returns true if the given `_bleio.ScanEntry` matches all portions of the Advertisement + type's prefix.""" if not hasattr(cls, "prefix"): - return False + return True return entry.matches(cls.prefix) diff --git a/adafruit_ble/advertising/adafruit.py b/adafruit_ble/advertising/adafruit.py index a580e71..44f65c0 100755 --- a/adafruit_ble/advertising/adafruit.py +++ b/adafruit_ble/advertising/adafruit.py @@ -31,21 +31,35 @@ """ +import struct +from micropython import const + from . import Advertisement, LazyField from .standard import ManufacturerData, ManufacturerDataField __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" +_MANUFACTURING_DATA_ADT = const(0xff) +_ADAFRUIT_COMPANY_ID = const(0x0822) +_COLOR_DATA_ID = const(0x0000) + class AdafruitColor(Advertisement): """Broadcast a single RGB color.""" - prefix = b"\x06\xff\xff\xff\x06\x00\x00" + # This prefix matches all + prefix = struct.pack("".format(self.uuid) - class BoundServiceList: - """Sequence-like object of `Service` objects.""" + """Sequence-like object of Service UUID objects. It stores both standard and vendor UUIDs.""" def __init__(self, advertisement, *, standard_services, vendor_services): self._advertisement = advertisement self._standard_service_fields = standard_services @@ -58,28 +49,28 @@ def __init__(self, advertisement, *, standard_services, vendor_services): data = self._advertisement.data_dict[adt] for i in range(len(data) // 2): uuid = StandardUUID(data[2*i:2*(i+1)]) - if uuid in all_services_by_uuid: - self._standard_services.append(all_services_by_uuid[uuid]) - else: - self._standard_services.append(UnknownService(uuid)) + self._standard_services.append(uuid) for adt in vendor_services: if adt in self._advertisement.data_dict: data = self._advertisement.data_dict[adt] for i in range(len(data) // 16): uuid = VendorUUID(data[16*i:16*(i+1)]) - if uuid in all_services_by_uuid: - self._vendor_services.append(all_services_by_uuid[uuid]) - else: - self._vendor_services.append(UnknownService(uuid)) + self._vendor_services.append(uuid) + + def __contains__(self, key): + uuid = key + if hasattr(key, "uuid"): + uuid = key.uuid + return uuid in self._vendor_services or uuid in self._standard_services - def _update(self, adt, service_set): - if len(service_set) == 0: + def _update(self, adt, uuids): + if len(uuids) == 0: del self._advertisement.data_dict[adt] - uuid_length = service_set[0].uuid.size // 8 - b = bytearray(len(service_set) * uuid_length) + uuid_length = uuids[0].size // 8 + b = bytearray(len(uuids) * uuid_length) i = 0 - for service in service_set: - service.uuid.pack_into(b, i) + for uuid in uuids: + uuid.pack_into(b, i) i += uuid_length self._advertisement.data_dict[adt] = b @@ -101,11 +92,12 @@ def extend(self, services): standard = False vendor = False for service in services: - if isinstance(service.uuid, StandardUUID) and service not in self._standard_services: - self._standard_services.append(service) + if (isinstance(service.uuid, StandardUUID) and + service.uuid not in self._standard_services): + self._standard_services.append(service.uuid) standard = True - elif isinstance(service.uuid, VendorUUID) and service not in self._vendor_services: - self._vendor_services.append(service) + elif isinstance(service.uuid, VendorUUID) and service.uuid not in self._vendor_services: + self._vendor_services.append(service.uuid) vendor = True if standard: @@ -115,10 +107,10 @@ def extend(self, services): def __str__(self): data = [] - for service in self._standard_services: - data.append(str(service)) - for service in self._vendor_services: - data.append(str(service)) + for service_uuid in self._standard_services: + data.append(str(service_uuid)) + for service_uuid in self._vendor_services: + data.append(str(service_uuid)) return " ".join(data) class ServiceList(AdvertisingDataField): @@ -146,8 +138,9 @@ def __get__(self, obj, cls): obj._service_lists[first_adt] = BoundServiceList(obj, **self.__dict__) return obj._service_lists[first_adt] -class ProvideServiceAdvertisement(Advertisement): +class ProvideServicesAdvertisement(Advertisement): """Advertise what services that the device makes available upon connection.""" + # This is four prefixes, one for each ADT that can carry service UUIDs. prefix = b"\x01\x02\x01\x03\x01\x06\x01\x07" services = ServiceList(standard_services=[0x02, 0x03], vendor_services=[0x06, 0x07]) """List of services the device can provide.""" @@ -162,8 +155,10 @@ def __init__(self, *services): def matches(cls, entry): return entry.matches(cls.prefix, all=False) -class SolicitServiceAdvertisement(Advertisement): +class ServicesSolicitationAdvertisement(Advertisement): """Advertise what services the device would like to use over a connection.""" + # This is two prefixes, one for each ADT that can carry solicited service UUIDs. + prefix = b"\x01\x14\x01\x15" solicited_services = ServiceList(standard_services=[0x14], vendor_services=[0x15]) """List of services the device would like to use.""" diff --git a/adafruit_ble/characteristics/__init__.py b/adafruit_ble/characteristics/__init__.py index e69de29..fb2b3f3 100644 --- a/adafruit_ble/characteristics/__init__.py +++ b/adafruit_ble/characteristics/__init__.py @@ -0,0 +1,141 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft 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. +""" +:py:mod:`~adafruit_ble.characteristics` +==================================================== + +This module provides core BLE characteristic classes that are used within Services. + +""" + +import struct +import _bleio + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + + +class Characteristic: + """Top level Characteristic class that does basic binding.""" + def __init__(self, *, uuid=None, initial_value=None, max_length=None, **kwargs): + if uuid: + self.uuid = uuid + self.kwargs = kwargs + self.initial_value = initial_value + self.max_length = max_length + self.field_name = None # Set by Service during basic binding + + def _ensure_bound(self, service, initial_value=None): + """Binds the characteristic to the local Service or remote Characteristic object given.""" + if self.field_name in service.bleio_characteristics: + return + if service.remote: + bleio_characteristic = None + remote_characteristics = service.bleio_service.characteristics + for characteristic in remote_characteristics: + if characteristic.uuid == self.uuid.bleio_uuid: + bleio_characteristic = characteristic + break + if not bleio_characteristic: + raise AttributeError("Characteristic not available on remote service") + else: + bleio_characteristic = self.__bind_locally(service, initial_value) + + service.bleio_characteristics[self.field_name] = bleio_characteristic + + def __bind_locally(self, service, initial_value): + if initial_value is None: + initial_value = self.initial_value + if initial_value is None and self.max_length: + initial_value = bytes(self.max_length) + max_length = self.max_length + if max_length is None and initial_value is None: + max_length = 20 + initial_value = bytes(max_length) + if max_length is None: + max_length = len(initial_value) + return _bleio.Characteristic.add_to_service( + service, + self.uuid.bleio_uuid, + initial_value=initial_value, + max_length=max_length, + **self.kwargs + ) + + def __get__(self, service, cls=None): + self._ensure_bound(service) + bleio_characteristic = service.bleio_characteristics[self.field_name] + raw_data = bleio_characteristic.value + return raw_data + + def __set__(self, service, value): + self._ensure_bound(service, value) + bleio_characteristic = service.bleio_characteristics[self.field_name] + bleio_characteristic.value = value + +class ComplexCharacteristic: + """Characteristic class that does complex binding where the subclass returns a full object for + interacting with the characteristic data. The Characteristic itself will be shadowed once it + has been bound to the corresponding instance attribute.""" + def __init__(self, *, uuid=None, **kwargs): + if uuid: + self.uuid = uuid + self.kwargs = kwargs + self.field_name = None # Set by Service + + def bind(self, service): + """Binds the characteristic to the local Service or remote Characteristic object given.""" + if service.remote: + remote_characteristics = service.bleio_service.characteristics + for characteristic in remote_characteristics: + if characteristic.uuid == self.uuid.bleio_uuid: + return characteristic + raise AttributeError("Characteristic not available on remote service") + return _bleio.Characteristic.add_to_service( + service.bleio_service, + self.uuid.bleio_uuid, + **self.kwargs + ) + + def __get__(self, service, cls=None): + bound_object = self.bind(service) + setattr(service, self.field_name, bound_object) + return bound_object + +class StructCharacteristic(Characteristic): + """Data descriptor for a structure with a fixed format.""" + def __init__(self, struct_format, **kwargs): + self._struct_format = struct_format + self._expected_size = struct.calcsize(struct_format) + if "initial_value" in kwargs: + kwargs["initial_value"] = struct.pack(self._struct_format, *kwargs["initial_value"]) + super().__init__(**kwargs, max_length=self._expected_size, fixed_length=True) + + def __get__(self, obj, cls=None): + raw_data = super().__get__(obj, cls) + if len(raw_data) < self._expected_size: + return None + return struct.unpack(self._struct_format, raw_data) + + def __set__(self, obj, value): + encoded = struct.pack(self._struct_format, *value) + super.__set__(obj, encoded) diff --git a/adafruit_ble/characteristics/core.py b/adafruit_ble/characteristics/core.py deleted file mode 100755 index ff9c08f..0000000 --- a/adafruit_ble/characteristics/core.py +++ /dev/null @@ -1,120 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2019 Scott Shawcroft 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. -""" -:py:mod:`~adafruit_ble.characteristics.core` -==================================================== - -This module provides core BLE characteristic classes that are used within Services. - -""" - -import struct -import _bleio - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" - - -class Characteristic: - """Top level Characteristic class that does basic binding.""" - def __init__(self, *, uuid=None, initial_value=None, max_length=None, **kwargs): - if uuid: - self.uuid = uuid - self.kwargs = kwargs - self.initial_value = initial_value - self.max_length = max_length - self.field_name = None # Set by Service during basic binding - - def bind(self, obj, *, initial_value=None): - """Binds the characteristic to the local Service or remote Characteristic object given.""" - if initial_value is None: - initial_value = self.initial_value - if initial_value is None and self.max_length: - initial_value = bytes(self.max_length) - max_length = self.max_length - if max_length is None and initial_value is None: - max_length = 20 - initial_value = bytes(max_length) - if max_length is None: - max_length = len(initial_value) - return _bleio.Characteristic.add_to_service( - obj, - self.uuid._uuid, # pylint: disable=protected-access - initial_value=initial_value, - max_length=max_length, - **self.kwargs - ) - - def write_raw_data(self, bound_characteristic, data): - """Writes the data out to the bound characteristic.""" - # pylint: disable=no-self-use - bound_characteristic.value = data - - def read_raw_data(self, bound_characteristic): - """Reads data out of the bound characteristic.""" - # pylint: disable=no-self-use - return bound_characteristic.value - -class ComplexCharacteristic(Characteristic): - """Characteristic class that does complex binding where the subclass returns a full object for - interacting with the characteristic data. The Characteristic itself will be shadowed once it - has been bound to the corresponding instance attribute.""" - -class BytesCharacteristic(Characteristic): - """Data descriptor for a variable length byte array.""" - def __get__(self, obj, cls=None): - if not hasattr(obj, "_bound_characteristics"): - return self.initial_value - bound_characteristic = obj._bound_characteristics[self.field_name] - raw_data = self.read_raw_data(bound_characteristic) - return raw_data - - def __set__(self, obj, value): - if not hasattr(obj, "_bound_characteristics"): - self.initial_value = value - if "fixed_length" in self.kwargs and self.kwargs["fixed_length"]: - self.kwargs["max_length"] = len(value) - return - bound_characteristic = obj._bound_characteristics[self.field_name] - self.write_raw_data(bound_characteristic, value) - -class StructCharacteristic(Characteristic): - """Data descriptor for a structure with a fixed format.""" - def __init__(self, struct_format, **kwargs): - self._struct_format = struct_format - self._expected_size = struct.calcsize(struct_format) - if "initial_value" in kwargs: - kwargs["initial_value"] = struct.pack(self._struct_format, *kwargs["initial_value"]) - super().__init__(**kwargs, max_length=self._expected_size, fixed_length=True) - - def __get__(self, obj, cls=None): - bound_characteristic = obj._bound_characteristics[self.field_name] - raw_data = self.read_raw_data(bound_characteristic) - if len(raw_data) < self._expected_size: - print(raw_data) - return None - return struct.unpack(self._struct_format, raw_data) - - def __set__(self, obj, value): - bound_characteristic = obj._bound_characteristics[self.field_name] - encoded = struct.pack(self._struct_format, *value) - self.write_raw_data(bound_characteristic, encoded) diff --git a/adafruit_ble/characteristics/int.py b/adafruit_ble/characteristics/int.py index 7954d3d..046324a 100755 --- a/adafruit_ble/characteristics/int.py +++ b/adafruit_ble/characteristics/int.py @@ -27,7 +27,7 @@ """ -from .core import StructCharacteristic +from . import StructCharacteristic __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" diff --git a/adafruit_ble/characteristics/stream.py b/adafruit_ble/characteristics/stream.py index f1e5df7..37d8554 100755 --- a/adafruit_ble/characteristics/stream.py +++ b/adafruit_ble/characteristics/stream.py @@ -29,15 +29,14 @@ """ import _bleio -from .core import ComplexCharacteristic +from . import ComplexCharacteristic __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" class BoundWriteStream: """Writes data out to the peer.""" - def __init__(self, stream_out, bound_characteristic): - self.stream_out = stream_out + def __init__(self, bound_characteristic): self.bound_characteristic = bound_characteristic def write(self, buf): @@ -45,7 +44,7 @@ def write(self, buf): # We can only write 20 bytes at a time. offset = 0 while offset < len(buf): - self.stream_out.write_raw_data(self.bound_characteristic, buf[offset:offset+20]) + self.bound_characteristic.value = buf[offset:offset+20] offset += 20 class StreamOut(ComplexCharacteristic): @@ -57,16 +56,16 @@ def __init__(self, *, timeout=1.0, buffer_size=64, **kwargs): read_perm=_bleio.Attribute.OPEN, **kwargs) - def bind(self, obj, *, initial_value=None): - """Binds the characteristic to the local Service or remote Characteristic object given.""" - # If we're given a characteristic then we're the client and need to buffer in. - if isinstance(obj, _bleio.Characteristic): - obj.set_cccd(notify=True) - return _bleio.CharacteristicBuffer(obj, + def bind(self, service): + """Binds the characteristic to the given Service.""" + bound_characteristic = super().bind(service) + # If we're given a remote service then we're the client and need to buffer in. + if service.remote: + bound_characteristic.set_cccd(notify=True) + return _bleio.CharacteristicBuffer(bound_characteristic, timeout=self._timeout, buffer_size=self._buffer_size) - bound_characteristic = super().bind(obj, initial_value=initial_value) - return BoundWriteStream(self, bound_characteristic) + return BoundWriteStream(bound_characteristic) class StreamIn(ComplexCharacteristic): """Input stream into the Service server.""" @@ -79,13 +78,13 @@ def __init__(self, *, timeout=1.0, buffer_size=64, **kwargs): write_perm=_bleio.Attribute.OPEN, **kwargs) - def bind(self, obj, *, initial_value=None): - """Binds the characteristic to the local Service or remote Characteristic object given.""" - # If we're given a characteristic then we're the client and need to write out. - if isinstance(obj, _bleio.Characteristic): - return BoundWriteStream(self, obj) + def bind(self, service): + """Binds the characteristic to the given Service.""" + bound_characteristic = super().bind(service) + # If the service is remote need to write out. + if service.remote: + return BoundWriteStream(bound_characteristic) # We're the server so buffer incoming writes. - bound_characteristic = super().bind(obj, initial_value=initial_value) return _bleio.CharacteristicBuffer(bound_characteristic, timeout=self._timeout, buffer_size=self._buffer_size) diff --git a/adafruit_ble/characteristics/string.py b/adafruit_ble/characteristics/string.py index 45da1bc..5485cdd 100755 --- a/adafruit_ble/characteristics/string.py +++ b/adafruit_ble/characteristics/string.py @@ -28,7 +28,7 @@ """ import _bleio -from .core import Characteristic +from . import Characteristic __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" @@ -43,19 +43,12 @@ def __init__(self, *, properties=_bleio.Characteristic.READ, uuid=None): uuid=uuid) def __get__(self, obj, cls=None): - if hasattr(obj, "_bound_characteristics"): - bound_characteristic = obj._bound_characteristics[self.field_name] - raw_data = self.read_raw_data(bound_characteristic) - else: - raw_data = self.initial_value + raw_data = super().__get__(obj, cls) return str(raw_data, "utf-8") def __set__(self, obj, value): - if hasattr(obj, "_bound_characteristics"): - bound_characteristic = obj._bound_characteristics[self.field_name] - self.write_raw_data(bound_characteristic, value.encode("utf-8")) - else: - self.initial_value = value.encode("utf-8") + encoded_value = value.encode("utf-8") + super().__set__(obj, encoded_value) class FixedStringCharacteristic(Characteristic): """Fixed strings are set once when bound and unchanged after.""" @@ -67,9 +60,5 @@ def __init__(self, *, uuid=None): uuid=uuid) def __get__(self, obj, cls=None): - if hasattr(obj, "_bound_characteristics"): - bound_characteristic = obj._bound_characteristics[self.field_name] - raw_data = self.read_raw_data(bound_characteristic) - else: - raw_data = self.initial_value + raw_data = super().__get__(obj, cls) return str(raw_data, "utf-8") diff --git a/adafruit_ble/services/__init__.py b/adafruit_ble/services/__init__.py index e69de29..c890646 100644 --- a/adafruit_ble/services/__init__.py +++ b/adafruit_ble/services/__init__.py @@ -0,0 +1,88 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Scott Shawcroft 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. +""" +:py:mod:`~adafruit_ble.services` +==================================================== + +This module provides the top level Service definition. + +""" + +import _bleio + +from ..characteristics import Characteristic, ComplexCharacteristic + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class Service: + """Top level Service class that handles the hard work of binding to a local or remote service. + + Providers of a local service should instantiate their Service with service=None, the default. + The local Service's characteristics will be lazily made available to clients as they are used + locally. In other words, a characteristic won't be available to remote clients until it has + been read or written locally. + + To use a remote Service, get the item with the key of the Service type on the + `BLEConnection`. For example, ``connection[UartService]`` will return the UartService + instance for the connection's peer. + """ + def __init__(self, *, service=None, secondary=False, **initial_values): + if service is None: + # pylint: disable=no-member + self.bleio_service = _bleio.Service(self.uuid.bleio_uuid, secondary=secondary) + elif not service.remote: + raise ValueError("Can only create services with a remote service or None") + else: + self.bleio_service = service + + # This internal dictionary is manipulated by the Characteristic descriptors to store their + # per-Service state. It is NOT managed by the Service itself. It is an attribute of the + # Service so that the lifetime of the objects is the same as the Service. + self.bleio_characteristics = {} + + # Set the field name on all of the characteristic objects so they can replace themselves if + # they choose. + # TODO: Replace this with __set_name__ support. + for class_attr in dir(self.__class__): + if class_attr.startswith("__"): + continue + value = getattr(self.__class__, class_attr) + if (not isinstance(value, Characteristic) and + not isinstance(value, ComplexCharacteristic)): + continue + value.field_name = class_attr + + # Get or set every attribute to ensure that they are all bound up front. We could lazily + # init them but the Nordic Soft Device requires characteristics be added immediately + # after the Service. In other words, only characteristics for the most recently added + # service can be added. + if not self.remote: + if class_attr in initial_values: + setattr(self, class_attr, initial_values[class_attr]) + else: + getattr(self, class_attr) + + @property + def remote(self): + """True if the service is provided by a peer and accessed remotely.""" + return self.bleio_service.remote diff --git a/adafruit_ble/services/apple.py b/adafruit_ble/services/apple.py index 9ac5939..eeaede0 100755 --- a/adafruit_ble/services/apple.py +++ b/adafruit_ble/services/apple.py @@ -27,7 +27,7 @@ """ -from .core import Service +from . import Service from ..uuid import VendorUUID __version__ = "0.0.0-auto.0" @@ -36,19 +36,15 @@ class ContinuityService(Service): """Service used for cross-Apple device functionality like AirDrop. Unimplemented.""" uuid = VendorUUID("d0611e78-bbb4-4591-a5f8-487910ae4366") - default_field_name = "continuity" class UnknownApple1Service(Service): """Unknown service. Unimplemented.""" uuid = VendorUUID("9fa480e0-4967-4542-9390-d343dc5d04ae") - default_field_name = "unknown_apple1" class AppleNotificationService(Service): """Notification service. Unimplemented.""" uuid = VendorUUID("7905F431-B5CE-4E99-A40F-4B1E122D00D0") - default_field_name = "apple_notification" class AppleMediaService(Service): """View and control currently playing media. Unimplemented.""" uuid = VendorUUID("89D3502B-0F36-433A-8EF4-C502AD55F8DC") - default_field_name = "apple_media" diff --git a/adafruit_ble/services/circuitpython.py b/adafruit_ble/services/circuitpython.py index 209a62f..d846a2f 100755 --- a/adafruit_ble/services/circuitpython.py +++ b/adafruit_ble/services/circuitpython.py @@ -29,7 +29,7 @@ import _bleio -from .core import Service +from . import Service from ..characteristics.string import StringCharacteristic from ..characteristics.stream import StreamOut from ..uuid import VendorUUID @@ -49,7 +49,6 @@ class CircuitPythonService(Service): """Core CircuitPython service that allows for file modification and REPL access. Unimplemented.""" uuid = CircuitPythonUUID(0x0100) - default_field_name = "circuitpython" filename = StringCharacteristic(uuid=CircuitPythonUUID(0x0200), properties=(_bleio.Characteristic.READ | _bleio.Characteristic.WRITE)) diff --git a/adafruit_ble/services/core.py b/adafruit_ble/services/core.py deleted file mode 100755 index 2883e3e..0000000 --- a/adafruit_ble/services/core.py +++ /dev/null @@ -1,93 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2019 Scott Shawcroft 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. -""" -:py:mod:`~adafruit_ble.services.core` -==================================================== - -This module provides the top level Service definition. - -""" - -import _bleio - -from ..characteristics.core import Characteristic, ComplexCharacteristic - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" - -class Service: - """Top level Service class that handles the hard work of binding to a local or remote service. - - Providers of a local service should instantiate their Service with service=None, the default. - - To use a remote Service, read the corresponding attribute on the `SmartConnection`. The - attribute names match the Service's ``default_field_name``. - """ - def __init__(self, *, service=None, secondary=False, **initial_values): - if service is None: - # pylint: disable=no-member - self._service = _bleio.Service(self.uuid._uuid, secondary=secondary) - elif not service.remote: - raise ValueError("Can only create services with a remote service or None") - else: - self._service = service - - if self._service.remote: - self.__init_remote() - else: - self.__init_local(initial_values) - - def __init_local(self, initial_values): - self._bound_characteristics = {} - for class_attr in dir(self.__class__): - if class_attr.startswith("__"): - continue - value = getattr(self.__class__, class_attr) - if isinstance(value, ComplexCharacteristic): - setattr(self, class_attr, value.bind(self._service)) - elif isinstance(value, Characteristic): - initial_value = initial_values.get(class_attr, None) - self._bound_characteristics[class_attr] = value.bind(self._service, - initial_value=initial_value) - value.field_name = class_attr - - def __init_remote(self): - remote_service = self._service - uuid_to_bc = {} - for characteristic in remote_service.characteristics: - uuid_to_bc[characteristic.uuid] = characteristic - - self._bound_characteristics = {} - for class_attr in dir(self.__class__): - if class_attr.startswith("__"): - continue - value = getattr(self.__class__, class_attr) - if not isinstance(value, Characteristic): - continue - uuid = value.uuid._uuid # pylint: disable=protected-access - if isinstance(value, ComplexCharacteristic): - setattr(self, class_attr, value.bind(uuid_to_bc[uuid])) - elif isinstance(value, Characteristic): - if uuid in uuid_to_bc: - self._bound_characteristics[class_attr] = uuid_to_bc[uuid] - value.field_name = class_attr - return self diff --git a/adafruit_ble/services/midi.py b/adafruit_ble/services/midi.py index 66f91eb..48777af 100644 --- a/adafruit_ble/services/midi.py +++ b/adafruit_ble/services/midi.py @@ -29,9 +29,9 @@ import _bleio -from .core import Service +from . import Service from ..uuid import VendorUUID -from ..characteristics.core import Characteristic +from ..characteristics import Characteristic __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" diff --git a/adafruit_ble/services/nordic.py b/adafruit_ble/services/nordic.py index 4f90b7c..c482298 100755 --- a/adafruit_ble/services/nordic.py +++ b/adafruit_ble/services/nordic.py @@ -28,7 +28,7 @@ """ -from .core import Service +from . import Service from ..uuid import VendorUUID from ..characteristics.stream import StreamOut, StreamIn @@ -65,8 +65,6 @@ class UARTService(Service): _server_rx = StreamIn(uuid=VendorUUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), timeout=1.0, buffer_size=64) - default_field_name = "uart" - def __init__(self, service=None): super().__init__(service=service) self.connectable = True diff --git a/adafruit_ble/services/sphero.py b/adafruit_ble/services/sphero.py index 4906d97..8627ee3 100644 --- a/adafruit_ble/services/sphero.py +++ b/adafruit_ble/services/sphero.py @@ -27,7 +27,7 @@ """ -from .core import Service +from . import Service from ..uuid import VendorUUID __version__ = "0.0.0-auto.0" diff --git a/adafruit_ble/services/standard/hid.py b/adafruit_ble/services/standard/hid.py index 2ba63c0..0ef1a32 100755 --- a/adafruit_ble/services/standard/hid.py +++ b/adafruit_ble/services/standard/hid.py @@ -33,7 +33,7 @@ from micropython import const import _bleio -from adafruit_ble.characteristics.core import BytesCharacteristic +from adafruit_ble.characteristics import Characteristic from adafruit_ble.characteristics.int import Uint8Characteristic from adafruit_ble.uuid import StandardUUID @@ -75,8 +75,8 @@ class ReportIn: uuid = StandardUUID(0x24ad) def __init__(self, service, report_id, usage_page, usage, *, max_length): self._characteristic = _bleio.Characteristic.add_to_service( - service._service, - self.uuid._uuid, + service.bleio_service, + self.uuid.bleio_uuid, properties=_bleio.Characteristic.READ | _bleio.Characteristic.NOTIFY, read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, write_perm=_bleio.Attribute.NO_ACCESS, max_length=max_length, fixed_length=True) @@ -98,8 +98,8 @@ class ReportOut: uuid = StandardUUID(0x24ad) def __init__(self, service, report_id, usage_page, usage, *, max_length): self._characteristic = _bleio.Characteristic.add_to_service( - service._service, - self.uuid._uuid, + service.bleio_service, + self.uuid.bleio_uuid, max_length=max_length, fixed_length=True, properties=(_bleio.Characteristic.READ | _bleio.Characteristic.WRITE | @@ -141,20 +141,20 @@ class HIDService(Service): uuid = StandardUUID(0x1812) default_field_name = "hid" - boot_keyboard_in = BytesCharacteristic(uuid=StandardUUID(0x2A22), - properties=(_bleio.Characteristic.READ | - _bleio.Characteristic.NOTIFY), - read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, - write_perm=_bleio.Attribute.NO_ACCESS, - max_length=8, fixed_length=True) - - boot_keyboard_out = BytesCharacteristic(uuid=StandardUUID(0x2A32), - properties=(_bleio.Characteristic.READ | - _bleio.Characteristic.WRITE | - _bleio.Characteristic.WRITE_NO_RESPONSE), - read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, - write_perm=_bleio.Attribute.ENCRYPT_NO_MITM, - max_length=1, fixed_length=True) + boot_keyboard_in = Characteristic(uuid=StandardUUID(0x2A22), + properties=(_bleio.Characteristic.READ | + _bleio.Characteristic.NOTIFY), + read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, + write_perm=_bleio.Attribute.NO_ACCESS, + max_length=8, fixed_length=True) + + boot_keyboard_out = Characteristic(uuid=StandardUUID(0x2A32), + properties=(_bleio.Characteristic.READ | + _bleio.Characteristic.WRITE | + _bleio.Characteristic.WRITE_NO_RESPONSE), + read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, + write_perm=_bleio.Attribute.ENCRYPT_NO_MITM, + max_length=1, fixed_length=True) protocol_mode = Uint8Characteristic(uuid=StandardUUID(0x2A4E), properties=(_bleio.Characteristic.READ | @@ -168,18 +168,18 @@ class HIDService(Service): # bcdHID (version), bCountryCode (0 not localized), Flags: RemoteWake, NormallyConnectable # bcd1.1, country = 0, flag = normal connect # TODO: Make this a struct. - hid_information = BytesCharacteristic(uuid=StandardUUID(0x2A4A), - properties=_bleio.Characteristic.READ, - read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, - write_perm=_bleio.Attribute.NO_ACCESS, - initial_value=b'\x01\x01\x00\x02') - """Hid information including version, country code and flags.""" - - report_map = BytesCharacteristic(uuid=StandardUUID(0x2A4B), + hid_information = Characteristic(uuid=StandardUUID(0x2A4A), properties=_bleio.Characteristic.READ, read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, write_perm=_bleio.Attribute.NO_ACCESS, - fixed_length=True) + initial_value=b'\x01\x01\x00\x02') + """Hid information including version, country code and flags.""" + + report_map = Characteristic(uuid=StandardUUID(0x2A4B), + properties=_bleio.Characteristic.READ, + read_perm=_bleio.Attribute.ENCRYPT_NO_MITM, + write_perm=_bleio.Attribute.NO_ACCESS, + fixed_length=True) """This is the USB HID descriptor (not to be confused with a BLE Descriptor). It describes which report characteristic are what.""" @@ -198,7 +198,6 @@ def _init_devices(self): # pylint: disable=too-many-branches,too-many-statements,too-many-locals self.devices = [] hid_descriptor = self.report_map - print(hid_descriptor, len(hid_descriptor)) global_table = [None] * 10 local_table = [None] * 3 diff --git a/adafruit_ble/uuid/__init__.py b/adafruit_ble/uuid/__init__.py index 481557d..0cdcdae 100644 --- a/adafruit_ble/uuid/__init__.py +++ b/adafruit_ble/uuid/__init__.py @@ -39,32 +39,32 @@ class UUID: # TODO: Make subclassing _bleio.UUID work so we can just use it directly. # pylint: disable=no-member def __hash__(self): - return hash(self._uuid) + return hash(self.bleio_uuid) def __eq__(self, other): if isinstance(other, _bleio.UUID): - return self._uuid == other + return self.bleio_uuid == other if isinstance(other, UUID): - return self._uuid == other._uuid - return self == other + return self.bleio_uuid == other.bleio_uuid + return False def __str__(self): - return str(self._uuid) + return str(self.bleio_uuid) def pack_into(self, buffer, offset=0): """Packs the UUID into the buffer at the given offset.""" - self._uuid.pack_into(buffer, offset=offset) + self.bleio_uuid.pack_into(buffer, offset=offset) class StandardUUID(UUID): """Bluetooth defined, 16-bit UUID.""" def __init__(self, uuid16): if not isinstance(uuid16, int): uuid16 = struct.unpack(" closest_rssi or now - closest_last_time > 0.4: - closest = entry.address - else: - continue - closest_rssi = entry.rssi - closest_last_time = now - neopixels.fill(entry.color) - adapter.stop_scan() + print("Scanning for colors") + while not slide_switch.value: + for entry in ble.start_scan(AdafruitColor, minimum_rssi=-80, timeout=1): + if slide_switch.value: + break + now = time.monotonic() + new = False + if entry.address == closest: + pass + elif entry.rssi > closest_rssi or now - closest_last_time > 0.4: + closest = entry.address + else: + continue + closest_rssi = entry.rssi + closest_last_time = now + neopixels.fill(entry.color) + ble.stop_scan() diff --git a/examples/ble_demo_central.py b/examples/ble_demo_central.py index 89c23f6..a9c8b01 100644 --- a/examples/ble_demo_central.py +++ b/examples/ble_demo_central.py @@ -1,63 +1,68 @@ """ -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. +Demonstration of a Bluefruit BLE Central for Circuit Playground Bluefruit. Connects to the first BLE +UART peripheral it finds. Sends Bluefruit ColorPackets, read from three accelerometer axis, to the +peripheral. """ import time +import adafruit_lis3dh import board -from analogio import AnalogIn -import adafruit_ble -from adafruit_ble import SmartAdapter -from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +import busio +import digitalio +from adafruit_ble import BLERadio +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.nordic import UARTService +import neopixel from adafruit_bluefruit_connect.color_packet import ColorPacket def scale(value): - """Scale an value from 0-65535 (AnalogIn range) to 0-255 (RGB range)""" - return int(value / 65535 * 255) + """Scale an value from (acceleration range) to 0-255 (RGB range)""" + value = abs(value) + value = max(min(19.6, value), 0) + return int(value / 19.6 * 255) -adapter = SmartAdapter() -adafruit_ble.recognize_services(UARTService) +i2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA) +int1 = digitalio.DigitalInOut(board.ACCELEROMETER_INTERRUPT) +accelerometer = adafruit_lis3dh.LIS3DH_I2C(i2c, address=0x19, int1=int1) +accelerometer.range = adafruit_lis3dh.RANGE_8_G -a3 = AnalogIn(board.A3) -a4 = AnalogIn(board.A4) -a5 = AnalogIn(board.A5) +neopixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.1) + +ble = BLERadio() uart_connection = None # See if any existing connections are providing UARTService. -if adapter.connected: - for conn in adapter.connections: - if hasattr(conn, "uart"): - uart_connection = conn +if ble.connected: + for connection in ble.connections: + if UARTService in connection: + uart_connection = connection break while True: if not uart_connection: print("Scanning...") - for adv in adapter.start_scan((ProvideServiceAdvertisement,), timeout=5): + for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5): if UARTService in adv.services: print("found a UARTService advertisement") - uart_connection = adapter.connect(adv) + uart_connection = ble.connect(adv) break # Stop scanning whether or not we are connected. - adapter.stop_scan() + ble.stop_scan() while uart_connection and uart_connection.connected: - r = scale(a3.value) - g = scale(a4.value) - b = scale(a5.value) + r, g, b = map(scale, accelerometer.acceleration) color = (r, g, b) - print(color) + neopixels.fill(color) color_packet = ColorPacket(color) try: - uart_connection.uart.write(color_packet.to_bytes()) + uart_connection[UARTService].write(color_packet.to_bytes()) except OSError: try: uart_connection.disconnect() - except: + except: # pylint: disable=bare-except pass uart_connection = None time.sleep(0.3) diff --git a/examples/ble_demo_periph.py b/examples/ble_demo_periph.py index 20786dc..c2fe995 100644 --- a/examples/ble_demo_periph.py +++ b/examples/ble_demo_periph.py @@ -1,21 +1,21 @@ """ 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. +and updates a Circuit Playground to show the history of the received packets. """ import board import neopixel -from adafruit_ble import SmartAdapter -from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble import BLERadio +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.nordic import UARTService # Only the packet classes that are imported will be known to Packet. from adafruit_bluefruit_connect.packet import Packet from adafruit_bluefruit_connect.color_packet import ColorPacket -NUM_PIXELS = 32 -np = neopixel.NeoPixel(board.D10, NUM_PIXELS, brightness=0.1) +NUM_PIXELS = 10 +np = neopixel.NeoPixel(board.NEOPIXEL, NUM_PIXELS, brightness=0.1) next_pixel = 0 def mod(i): @@ -23,15 +23,15 @@ def mod(i): return i % NUM_PIXELS -adapter = SmartAdapter() +ble = BLERadio() uart = UARTService() -advertisement = ProvideServiceAdvertisement(uart) +advertisement = ProvideServicesAdvertisement(uart) while True: - adapter.start_advertising(advertisement) - while not adapter.connected: + ble.start_advertising(advertisement) + while not ble.connected: pass - while adapter.connected: + while ble.connected: packet = Packet.from_stream(uart) if isinstance(packet, ColorPacket): print(packet.color) diff --git a/examples/ble_detailed_scan.py b/examples/ble_detailed_scan.py new file mode 100644 index 0000000..6ac2f0d --- /dev/null +++ b/examples/ble_detailed_scan.py @@ -0,0 +1,27 @@ +# This example scans for any BLE advertisements and prints one advertisement and one scan response +# from every device found. This scan is more detailed than the simple test because it includes +# specialty advertising types. + +from adafruit_ble import BLERadio + +from adafruit_ble.advertising import Advertisement +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement + +ble = BLERadio() +print("scanning") +found = set() +scan_responses = set() +# By providing Advertisement as well we include everything, not just specific advertisements. +for advertisement in ble.start_scan(ProvideServicesAdvertisement, Advertisement): + addr = advertisement.address + if advertisement.scan_response and addr not in scan_responses: + scan_responses.add(addr) + elif not advertisement.scan_response and addr not in found: + found.add(addr) + else: + continue + print(addr, advertisement) + print("\t" + repr(advertisement)) + print() + +print("scan done") diff --git a/examples/ble_eddystone_test.py b/examples/ble_eddystone_test.py deleted file mode 100644 index 5e70c34..0000000 --- a/examples/ble_eddystone_test.py +++ /dev/null @@ -1,9 +0,0 @@ - -# This hasn't been updated. - -from adafruit_ble.beacon import EddystoneURLBeacon - -beacon = EddystoneURLBeacon('https://adafru.it/4062') - -while True: - pass diff --git a/examples/ble_hid_central.py b/examples/ble_hid_central.py index 5713811..21a75a7 100644 --- a/examples/ble_hid_central.py +++ b/examples/ble_hid_central.py @@ -2,30 +2,30 @@ Demonstration of a Bluefruit BLE Central. Connects to the first BLE HID peripheral it finds. """ -import time +# import time -import board +# import board -import adafruit_ble -from adafruit_ble.services.standard.hid import HIDService -from adafruit_ble.core.scanner import Scanner +# import adafruit_ble +# from adafruit_ble.services.standard.hid import HIDService +# from adafruit_ble.core.scanner import Scanner -# This hasn't been updated. +# # This hasn't been updated. -adafruit_ble.detect_service(HIDService) +# adafruit_ble.detect_service(HIDService) -scanner = Scanner() +# scanner = Scanner() -while True: - print("scanning") - results = [] - while not results: - results = scanner.scan(HIDService, timeout=4) +# while True: +# print("scanning") +# results = [] +# while not results: +# results = scanner.scan(HIDService, timeout=4) - peer = results[0].connect(timeout=10, pair=True) - print(peer) - print(peer.hid.protocol_mode) - print(peer.hid.report_map) - print(peer.hid.devices) +# peer = results[0].connect(timeout=10, pair=True) +# print(peer) +# print(peer.hid.protocol_mode) +# print(peer.hid.report_map) +# print(peer.hid.devices) - time.sleep(0.2) +# time.sleep(0.2) diff --git a/examples/ble_hid_periph.py b/examples/ble_hid_periph.py index 39093b6..9aedbc6 100644 --- a/examples/ble_hid_periph.py +++ b/examples/ble_hid_periph.py @@ -3,13 +3,13 @@ and updates a NeoPixel FeatherWing to show the history of the received packets. """ -import adafruit_ble -import board -import sys +# import board +# import sys import time -from adafruit_ble import SmartAdapter + +import adafruit_ble from adafruit_ble.advertising import to_hex -from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.standard.hid import HIDService from adafruit_ble.services.standard.device_info import DeviceInfoService from adafruit_hid.keyboard import Keyboard @@ -125,29 +125,29 @@ device_info = DeviceInfoService(software_revision=adafruit_ble.__version__, manufacturer="Adafruit Industries") print(device_info.manufacturer) -advertisement = ProvideServiceAdvertisement(hid) +advertisement = ProvideServicesAdvertisement(hid) advertisement.appearance = 961 -adapter = SmartAdapter() +ble = adafruit_ble.BLERadio() print(to_hex(bytes(advertisement))) -if not adapter.connected: +if not ble.connected: print("advertising") - adapter.start_advertising(advertisement) + ble.start_advertising(advertisement) else: print("already connected") - print(adapter.connections) + print(ble.connections) k = Keyboard(hid.devices) kl = KeyboardLayoutUS(k) while True: - while not adapter.connected: + while not ble.connected: pass #print("Start typing:") - while adapter.connected: + while ble.connected: # c = sys.stdin.read(1) # sys.stdout.write(c) #kl.write(c) kl.write("h") # print("sleeping") time.sleep(0.1) - adapter.start_advertising(advertisement) + ble.start_advertising(advertisement) diff --git a/examples/ble_scan_everything.py b/examples/ble_scan_everything.py deleted file mode 100644 index cd91416..0000000 --- a/examples/ble_scan_everything.py +++ /dev/null @@ -1,12 +0,0 @@ -from adafruit_ble import SmartAdapter - -adapter = SmartAdapter() -print("scanning") -found = set() -for entry in adapter.start_scan(timeout=60, minimum_rssi=-80): - addr = entry.address - if addr not in found: - print(entry) - found.add(addr) - -print("scan done") diff --git a/examples/ble_simpletest.py b/examples/ble_simpletest.py index 2094aca..c8a6f7d 100644 --- a/examples/ble_simpletest.py +++ b/examples/ble_simpletest.py @@ -1 +1,22 @@ -# Use one of the other examples to test specific functionality. +# This example scans for any BLE advertisements and prints one advertisement and one scan response +# from every device found. + +from adafruit_ble import BLERadio + +ble = BLERadio() +print("scanning") +found = set() +scan_responses = set() +for advertisement in ble.start_scan(): + addr = advertisement.address + if advertisement.scan_response and addr not in scan_responses: + scan_responses.add(addr) + elif not advertisement.scan_response and addr not in found: + found.add(addr) + else: + continue + print(addr, advertisement) + print("\t" + repr(advertisement)) + print() + +print("scan done") diff --git a/examples/ble_uart_echo_client.py b/examples/ble_uart_echo_client.py index e816af8..ce36ac8 100644 --- a/examples/ble_uart_echo_client.py +++ b/examples/ble_uart_echo_client.py @@ -1,37 +1,34 @@ -import time +""" +Used with ble_uart_echo_test.py. Transmits "echo" to the UARTService and receives it back. +""" -import adafruit_ble +import time -from adafruit_ble import SmartAdapter -from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble import BLERadio +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.nordic import UARTService -# This should work. - -adapter = SmartAdapter() - -adafruit_ble.recognize_services(UARTService) - -connection = None -if adapter.connected: - connection = adapter.connections[0] - +ble = BLERadio() while True: - print(connection, connection.connected if connection is not None else False) - while connection is not None and connection.connected: - print("echo") - connection.uart.write(b"echo") - # Returns b'' if nothing was read. - one_byte = connection.uart.read(4) - if one_byte: - print(one_byte) - print() + while ble.connected and any(UARTService in connection for connection in ble.connections): + for connection in ble.connections: + if UARTService not in connection: + continue + print("echo") + uart = connection[UARTService] + uart.write(b"echo") + # Returns b'' if nothing was read. + one_byte = uart.read(4) + if one_byte: + print(one_byte) + print() time.sleep(1) print("disconnected, scanning") - for entry in adapter.start_scan((ProvideServiceAdvertisement,), timeout=1): - if UARTService in entry.services: - connection = adapter.connect(entry) - break - - adapter.stop_scan() + for advertisement in ble.start_scan(ProvideServicesAdvertisement, timeout=1): + if UARTService not in advertisement.services: + continue + ble.connect(advertisement) + print("connected") + break + ble.stop_scan() diff --git a/examples/ble_uart_echo_test.py b/examples/ble_uart_echo_test.py index d9cf9df..bcd3b42 100644 --- a/examples/ble_uart_echo_test.py +++ b/examples/ble_uart_echo_test.py @@ -1,20 +1,20 @@ -import adafruit_ble +""" +Used with ble_uart_echo_client.py. Receives characters from the UARTService and transmits them back. +""" -from adafruit_ble import SmartAdapter -from adafruit_ble.advertising.standard import ProvideServiceAdvertisement +from adafruit_ble import BLERadio +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.nordic import UARTService -# This should work. - -adapter = SmartAdapter() +ble = BLERadio() uart = UARTService() -advertisement = ProvideServiceAdvertisement(uart) +advertisement = ProvideServicesAdvertisement(uart) while True: - adapter.start_advertising(advertisement) - while not adapter.connected: + ble.start_advertising(advertisement) + while not ble.connected: pass - while adapter.connected: + while ble.connected: # Returns b'' if nothing was read. one_byte = uart.read(1) if one_byte: From cd773157d187a80be33d92bf36645aa4f4d9be9c Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Fri, 1 Nov 2019 13:46:30 -0700 Subject: [PATCH 4/4] Lint and rename SolicitServicesAdvertisement --- adafruit_ble/__init__.py | 2 +- adafruit_ble/advertising/standard.py | 2 +- docs/micropython_mock.py | 2 +- examples/ble_color_proximity.py | 3 ++- examples/ble_demo_central.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 3fce1b0..beeb49f 100755 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -43,8 +43,8 @@ """ -import _bleio import board +import _bleio from .services import Service from .advertising import Advertisement diff --git a/adafruit_ble/advertising/standard.py b/adafruit_ble/advertising/standard.py index 5e18257..0d75c90 100644 --- a/adafruit_ble/advertising/standard.py +++ b/adafruit_ble/advertising/standard.py @@ -155,7 +155,7 @@ def __init__(self, *services): def matches(cls, entry): return entry.matches(cls.prefix, all=False) -class ServicesSolicitationAdvertisement(Advertisement): +class SolicitServicesAdvertisement(Advertisement): """Advertise what services the device would like to use over a connection.""" # This is two prefixes, one for each ADT that can carry solicited service UUIDs. prefix = b"\x01\x14\x01\x15" diff --git a/docs/micropython_mock.py b/docs/micropython_mock.py index 91adf9f..b5af9b3 100644 --- a/docs/micropython_mock.py +++ b/docs/micropython_mock.py @@ -1,2 +1,2 @@ def const(x): - return x \ No newline at end of file + return x diff --git a/examples/ble_color_proximity.py b/examples/ble_color_proximity.py index a951126..d1e3664 100644 --- a/examples/ble_color_proximity.py +++ b/examples/ble_color_proximity.py @@ -6,9 +6,10 @@ import time import board -import neopixel import digitalio +import neopixel + from adafruit_ble import BLERadio from adafruit_ble.advertising.adafruit import AdafruitColor diff --git a/examples/ble_demo_central.py b/examples/ble_demo_central.py index a9c8b01..3c2bbce 100644 --- a/examples/ble_demo_central.py +++ b/examples/ble_demo_central.py @@ -6,10 +6,10 @@ import time -import adafruit_lis3dh import board import busio import digitalio +import adafruit_lis3dh from adafruit_ble import BLERadio from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.nordic import UARTService