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..beeb49f --- 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,148 @@ """ -# imports +import board +import _bleio + +from .services import Service +from .advertising import Advertisement __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git" + +class BLEConnection: + """This represents a connection to a peer BLE device. + + It acts as a map from a Service type to a Service instance for the connection. + """ + def __init__(self, connection): + self._connection = connection + 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 = 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): + """True if the connection to the peer is still active.""" + return self._connection.connected + + def disconnect(self): + """Disconnect from peer.""" + self._connection.disconnect() + +class BLERadio: + """The BLERadio class enhances the normal `_bleio.Adapter`. + + It uses the library's `Advertisement` classes and the `BLEConnection` 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) + 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, **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 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: + 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 advertisement_types: + 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 `BLEConnection` to the peer that advertised the given advertisement.""" + connection = self._adapter.connect(advertisement.address, timeout=timeout) + self._connection_cache[connection] = BLEConnection(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 `BLEConnection` objects.""" + connections = self._adapter.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] = BLEConnection(connection) + wrapped_connections[i] = self._connection_cache[connection] + + return tuple(wrapped_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..8125a25 --- /dev/null +++ b/adafruit_ble/advertising/__init__.py @@ -0,0 +1,285 @@ +# 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 + +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 = {} + 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 its 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 __len__(self): + return 1 + + 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 + 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" # 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).""" + 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..44f65c0 --- /dev/null +++ b/adafruit_ble/advertising/adafruit.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. +""" +`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. + +""" + +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.""" + # This prefix matches all + prefix = 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) + filename = StringCharacteristic(uuid=CircuitPythonUUID(0x0200), + properties=(_bleio.Characteristic.READ | + _bleio.Characteristic.WRITE)) + contents = StreamOut(uuid=CircuitPythonUUID(0x0201)) 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..48777af --- /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 . import Service +from ..uuid import VendorUUID +from ..characteristics 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..c482298 --- /dev/null +++ b/adafruit_ble/services/nordic.py @@ -0,0 +1,120 @@ +# 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 . 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) + + 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..8627ee3 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 . 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..0ef1a32 --- /dev/null +++ b/adafruit_ble/services/standard/hid.py @@ -0,0 +1,287 @@ +# 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 import Characteristic +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.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) + 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) + ble.stop_scan() diff --git a/examples/ble_demo_central.py b/examples/ble_demo_central.py index eb56a9c..3c2bbce 100644 --- a/examples/ble_demo_central.py +++ b/examples/ble_demo_central.py @@ -1,48 +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 board -from analogio import AnalogIn +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 +import neopixel -#from adafruit_bluefruit_connect.packet import Packet -# Only the packet classes that are imported will be known to Packet. from adafruit_bluefruit_connect.color_packet import ColorPacket -from adafruit_ble.scanner import Scanner -from adafruit_ble.uart_client import UARTClient - def scale(value): - """Scale an value from 0-65535 (AnalogIn range) to 0-255 (RGB range)""" - return int(value / 65535 * 255) + """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) + +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 + +neopixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.1) -scanner = Scanner() -uart_client = UARTClient() +ble = BLERadio() -a3 = AnalogIn(board.A3) -a4 = AnalogIn(board.A4) -a5 = AnalogIn(board.A5) +uart_connection = None +# See if any existing connections are providing UARTService. +if ble.connected: + for connection in ble.connections: + if UARTService in connection: + uart_connection = connection + break 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) + if not uart_connection: + print("Scanning...") + for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5): + if UARTService in adv.services: + print("found a UARTService advertisement") + uart_connection = ble.connect(adv) + break + # Stop scanning whether or not we are connected. + ble.stop_scan() - while uart_client.connected: - r = scale(a3.value) - g = scale(a4.value) - b = scale(a5.value) + while uart_connection and uart_connection.connected: + r, g, b = map(scale, accelerometer.acceleration) color = (r, g, b) - print(color) + neopixels.fill(color) color_packet = ColorPacket(color) try: - uart_client.write(color_packet.to_bytes()) + uart_connection[UARTService].write(color_packet.to_bytes()) except OSError: - pass + try: + uart_connection.disconnect() + 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 83bb610..c2fe995 100644 --- a/examples/ble_demo_periph.py +++ b/examples/ble_demo_periph.py @@ -1,34 +1,38 @@ """ 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.uart_server import UARTServer -from adafruit_bluefruit_connect.packet import Packet +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 -uart_server = UARTServer() - -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): """Wrap i to modulus NUM_PIXELS.""" return i % NUM_PIXELS + +ble = BLERadio() +uart = UARTService() +advertisement = ProvideServicesAdvertisement(uart) + while True: - # Advertise when not connected. - uart_server.start_advertising() - while not uart_server.connected: + ble.start_advertising(advertisement) + while not ble.connected: pass - - while uart_server.connected: - packet = Packet.from_stream(uart_server) + while ble.connected: + packet = Packet.from_stream(uart) if isinstance(packet, ColorPacket): print(packet.color) np[next_pixel] = 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 ed0b8c5..0000000 --- a/examples/ble_eddystone_test.py +++ /dev/null @@ -1,4 +0,0 @@ -from adafruit_ble.beacon import EddystoneURLBeacon - -beacon = EddystoneURLBeacon('https://adafru.it/4062') -beacon.start() diff --git a/examples/ble_hid_central.py b/examples/ble_hid_central.py new file mode 100644 index 0000000..21a75a7 --- /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..9aedbc6 --- /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 board +# import sys +import time + +import adafruit_ble +from adafruit_ble.advertising import to_hex +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 +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 = ProvideServicesAdvertisement(hid) +advertisement.appearance = 961 + +ble = adafruit_ble.BLERadio() +print(to_hex(bytes(advertisement))) +if not ble.connected: + print("advertising") + ble.start_advertising(advertisement) +else: + print("already connected") + print(ble.connections) + +k = Keyboard(hid.devices) +kl = KeyboardLayoutUS(k) +while True: + while not ble.connected: + pass + #print("Start typing:") + while ble.connected: + # c = sys.stdin.read(1) + # sys.stdout.write(c) + #kl.write(c) + kl.write("h") + # print("sleeping") + time.sleep(0.1) + ble.start_advertising(advertisement) 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 new file mode 100644 index 0000000..ce36ac8 --- /dev/null +++ b/examples/ble_uart_echo_client.py @@ -0,0 +1,34 @@ +""" +Used with ble_uart_echo_test.py. Transmits "echo" to the UARTService and receives it back. +""" + +import time + +from adafruit_ble import BLERadio +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement +from adafruit_ble.services.nordic import UARTService + +ble = BLERadio() +while True: + 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 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 82bec77..bcd3b42 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 +""" +Used with ble_uart_echo_client.py. Receives characters from the UARTService and transmits them back. +""" -uart = UARTServer() +from adafruit_ble import BLERadio +from adafruit_ble.advertising.standard import ProvideServicesAdvertisement +from adafruit_ble.services.nordic import UARTService -while True: - uart.start_advertising() +ble = BLERadio() +uart = UARTService() +advertisement = ProvideServicesAdvertisement(uart) - # Wait for a connection - while not uart.connected: +while True: + ble.start_advertising(advertisement) + while not ble.connected: pass - - while uart.connected: + while ble.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.