From 49e04284db96055188f83bcc769e470f04eb9515 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 27 Jul 2019 14:47:58 -0400 Subject: [PATCH 1/8] discovery now done explicitly --- adafruit_ble/uart_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index dd505e6..51342b4 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -70,7 +70,9 @@ def connect(self, address, timeout): :param float/int timeout: Try to connect for ``timeout`` seconds. Not related to the timeout passed to ``UARTClient()``. """ - self._central.connect(address, timeout, service_uuids_whitelist=(NUS_SERVICE_UUID,)) + self._central.connect(address, timeout) + # Restrict discovery to NUS service only. + self._central.discover_remote_services((NUS_SERVICE_UUID,)) # Connect succeeded. Get the remote characteristics we need, which were # found during discovery. From 183cab6a532f6f8bbb6d0feb6415088ca300c847 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 27 Jul 2019 23:01:03 -0400 Subject: [PATCH 2/8] refactor advertising.py for peripheral service solicitation; skeletal CTS (no pairing yet) --- adafruit_ble/advertising.py | 143 ++++++++++++++++++---------- adafruit_ble/current_time_client.py | 79 +++++++++++++++ 2 files changed, 173 insertions(+), 49 deletions(-) create mode 100644 adafruit_ble/current_time_client.py diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index e507061..e4ecc54 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -51,6 +51,10 @@ class AdvertisingPacket: """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 @@ -131,84 +135,76 @@ def get(self, element_type, default=None): 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 - len(self._packet_bytes) + return self._max_length - self.length def _check_length(self): - if len(self._packet_bytes) > self._max_length: + 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): - packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME, name_bytes) + self._packet.add_field(AdvertisingPacket.COMPLETE_LOCAL_NAME, name_bytes) else: - packet.add_field(AdvertisingPacket.SHORT_LOCAL_NAME, name_bytes[:bytes_available]) + 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, @@ -216,12 +212,23 @@ def __init__(self, peripheral, *, tx_power=0): except IndexError: raise IndexError("Name too long") - self._advertising_data_packet = packet + 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._advertising_data_packet.packet_bytes + return self._packet.packet_bytes @property def scan_response_bytes(self): @@ -229,3 +236,41 @@ def scan_response_bytes(self): 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 + """ + + def __init__(self, peripheral, *, tx_power=0): + super().__init__() + 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/current_time_client.py b/adafruit_ble/current_time_client.py new file mode 100644 index 0000000..0430ead --- /dev/null +++ b/adafruit_ble/current_time_client.py @@ -0,0 +1,79 @@ + # 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.current_time_client` +==================================================== + +UART-style communication by a Central as a GATT Client + +* Author(s): Dan Halbert for Adafruit Industries + +""" +from bleio import Peripheral, UUID +from .advertising import SolicitationAdvertisement + +class CurrentTimeClient: + """ + Set up a peripheral that advertises for Current Time Service, + and connects if found. + + :param str name: Name to advertise for server. If None, use default Peripheral name. + + Example:: + + from adafruit_ble.current_time_client import SolicitationAdvertisement + + cts_client = CurrentTimeClient() + cts_client.start_advertising() + while not cts_client.connected: + pass + print(cts_client.time) + """ + + CTS_UUID = UUID(0x1805) + + def __init__(self, name="CTSClient", tx_power=0): + self._periph = Peripheral() + self._advertisement = SolicitationAdvertisement(name, (self.CTS_UUID,), tx_power=tx_power) + + 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 pair(self): + """Pair with the connected central.""" + pass + + @property + def time(self): + """Get the current time from the server.""" + return None From 9541ab2b59467abb3fdc283e3cb32e3f2b3575eb Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 30 Jul 2019 14:24:17 -0400 Subject: [PATCH 3/8] working pairing non-bonding Current Time Service client --- adafruit_ble/current_time_client.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/adafruit_ble/current_time_client.py b/adafruit_ble/current_time_client.py index 0430ead..da4741e 100644 --- a/adafruit_ble/current_time_client.py +++ b/adafruit_ble/current_time_client.py @@ -28,6 +28,9 @@ * Author(s): Dan Halbert for Adafruit Industries """ +import struct +import time + from bleio import Peripheral, UUID from .advertising import SolicitationAdvertisement @@ -40,20 +43,26 @@ class CurrentTimeClient: Example:: - from adafruit_ble.current_time_client import SolicitationAdvertisement + from adafruit_ble.current_time_client import CurrentTimeClient cts_client = CurrentTimeClient() cts_client.start_advertising() while not cts_client.connected: pass - print(cts_client.time) + cts_client.discover() + cts_client.pair() + print(cts_client.current_local_time) """ CTS_UUID = UUID(0x1805) + CURRENT_TIME_UUID = UUID(0x2A2B) + LOCAL_TIME_INFORMATION_UUID = UUID(0x2A0F) def __init__(self, name="CTSClient", tx_power=0): self._periph = Peripheral() self._advertisement = SolicitationAdvertisement(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.""" @@ -69,11 +78,66 @@ 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") + def pair(self): """Pair with the connected central.""" - pass + self._check_connected() + self._periph.pair() + + def discover(self): + """Discover service information.""" + self._check_connected() + self._periph.discover_remote_services((self.CTS_UUID,)) + services = self._periph.remote_services + 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(' Date: Wed, 31 Jul 2019 00:11:11 -0400 Subject: [PATCH 4/8] fix minor bugs and doc typos --- adafruit_ble/advertising.py | 3 ++- adafruit_ble/current_time_client.py | 11 +++++------ adafruit_ble/uart_client.py | 1 - 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index e4ecc54..483fd98 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -29,6 +29,7 @@ """ +import bleio import struct class AdvertisingPacket: @@ -171,7 +172,7 @@ def add_tx_power(self, tx_power): def add_appearance(self, appearance): """Add BLE Appearance value.""" - self.add_field(AdvertisingPacket.APPEARANCE, struct.pack(" Date: Wed, 7 Aug 2019 11:13:53 -0400 Subject: [PATCH 5/8] match updated API for Characteristic properties, etc. --- adafruit_ble/current_time_client.py | 11 ++++++++--- adafruit_ble/uart_server.py | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/adafruit_ble/current_time_client.py b/adafruit_ble/current_time_client.py index ccec52b..51269ce 100644 --- a/adafruit_ble/current_time_client.py +++ b/adafruit_ble/current_time_client.py @@ -36,7 +36,7 @@ class CurrentTimeClient: """ - Set up a peripheral that solicits centrals for Current Time Service, + Set up a peripheral that solicits centrals for Current Time Service. :param str name: Name to advertise for server. If None, use default Advertisement name. @@ -50,7 +50,12 @@ class CurrentTimeClient: pass cts_client.discover() cts_client.pair() - print(cts_client.current_local_time) + print(cts_client.current__time) + + To try the example above, open Settings->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) @@ -59,7 +64,7 @@ class CurrentTimeClient: def __init__(self, name=None, tx_power=0): self._periph = Peripheral(name=name) - self._advertisement = SolicitationAdvertisement(peripheral.name, (self.CTS_UUID,), tx_power=tx_power) + self._advertisement = SolicitationAdvertisement(self._periph.name, (self.CTS_UUID,), tx_power=tx_power) self._current_time_char = self._local_time_char = None diff --git a/adafruit_ble/uart_server.py b/adafruit_ble/uart_server.py index d7e1c9f..4b425cc 100644 --- a/adafruit_ble/uart_server.py +++ b/adafruit_ble/uart_server.py @@ -28,7 +28,7 @@ * Author(s): Dan Halbert for Adafruit Industries """ -from bleio import Characteristic, CharacteristicBuffer, Peripheral, Service +from bleio import Attribute, Characteristic, CharacteristicBuffer, Peripheral, Service from .advertising import ServerAdvertisement from .uart import NUS_SERVICE_UUID, NUS_RX_CHAR_UUID, NUS_TX_CHAR_UUID @@ -56,8 +56,14 @@ class UARTServer: """ def __init__(self, *, timeout=1.0, buffer_size=64, name=None): - self._read_char = Characteristic(NUS_RX_CHAR_UUID, write=True, write_no_response=True) - self._write_char = Characteristic(NUS_TX_CHAR_UUID, notify=True) + self._read_char = Characteristic( + NUS_RX_CHAR_UUID, + properties=Characteristic.WRITE | Characteristic.WRITE_NO_RESPONSE, + read_perm=Attribute.NO_ACCESS, write_perm=Attribute.OPEN) + self._write_char = Characteristic( + NUS_TX_CHAR_UUID, + properties=Characteristic.NOTIFY, + read_perm=Attribute.OPEN, write_perm=Attribute.NO_ACCESS) self._read_buffer = CharacteristicBuffer(self._read_char, timeout=timeout, buffer_size=buffer_size) @@ -82,6 +88,10 @@ def connected(self): """True if someone connected to the server.""" return self._periph.connected + def disconnect(self): + """Disconnect from peer.""" + self._periph.disconnect() + def read(self, nbytes=None): """ Read characters. If ``nbytes`` is specified then read at most that many bytes. From e4d6fe97c6b51e2f35f03cac54f50789ee13652c Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 7 Aug 2019 23:48:02 -0400 Subject: [PATCH 6/8] lint; first cut at HID; not working yet --- adafruit_ble/advertising.py | 1 - adafruit_ble/current_time_client.py | 3 +- adafruit_ble/hid.py | 269 ++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 adafruit_ble/hid.py diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index 483fd98..0e2d831 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -29,7 +29,6 @@ """ -import bleio import struct class AdvertisingPacket: diff --git a/adafruit_ble/current_time_client.py b/adafruit_ble/current_time_client.py index 51269ce..65c2d34 100644 --- a/adafruit_ble/current_time_client.py +++ b/adafruit_ble/current_time_client.py @@ -64,7 +64,8 @@ class CurrentTimeClient: def __init__(self, name=None, tx_power=0): self._periph = Peripheral(name=name) - self._advertisement = SolicitationAdvertisement(self._periph.name, (self.CTS_UUID,), tx_power=tx_power) + self._advertisement = SolicitationAdvertisement(self._periph.name, + (self.CTS_UUID,), tx_power=tx_power) self._current_time_char = self._local_time_char = None diff --git a/adafruit_ble/hid.py b/adafruit_ble/hid.py new file mode 100644 index 0000000..5195512 --- /dev/null +++ b/adafruit_ble/hid.py @@ -0,0 +1,269 @@ + # 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.hid_keyboard` +==================================================== + +BLE HID + +* Author(s): Dan Halbert for Adafruit Industries + +""" +import struct + +from bleio import Attribute, Characteristic, Descriptor, Peripheral, Service, UUID +from .advertising import ServerAdvertisement + +class HID: + """ + Provide devices for HID over BLE. + + :param str name: Name to advertise for server. If None, use default Peripheral name. + + Example:: + + from adafruit_ble.hid import HID + + hid = HID() + """ + + HUMAN_INTERFACE_DEVICE_UUID = UUID(0x1812) + REPORT_UUID = UUID(0x2A4D) + REPORT_MAP_UUID = UUID(0x2A4B) + HID_INFORMATION_UUID = UUID(0x2A4A) + HID_CONTROL_POINT_UUID = UUID(0x2A4C) + REPORT_REF_DESCR_UUID = UUID(0x2908) + _REPORT_TYPE_INPUT = 1 + # Boot keyboard and mouse not currently supported. + # PROTOCOL_MODE_UUID = UUID(0x2A4E) + # HID_BOOT_KEYBOARD_INPUT_REPORT_UUID = UUID(0x2A22) + # HID_BOOT_KEYBOARD_OUTPUT_REPORT_UUID = UUID(0x2A32) + # HID_BOOT_MOUSE_INPUT_REPORT_UUID = UUID(0x2A33) + + #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 + + REPORT_ID_KEYBOARD = 1 + """Keyboard device indicator, for use with `send_report()`.""" + REPORT_ID_MOUSE = 2 + """Mouse device indicator, for use with `send_report()`.""" + REPORT_ID_CONSUMER_CONTROL = 3 + """Consumer control device indicator, for use with `send_report()`.""" + REPORT_ID_GAMEPAD = 5 + """Gamepad device indicator, for use with `send_report()`.""" + + REPORT_SIZES = { + REPORT_ID_KEYBOARD : 8, + REPORT_ID_MOUSE : 4, + REPORT_ID_CONSUMER_CONTROL : 2, + REPORT_ID_GAMEPAD : 6, + } + + def __init__(self, name=None, tx_power=0): + self._input_chars = {} + for report_id in sorted(self.REPORT_SIZES.keys()): + desc = Descriptor(HID.REPORT_REF_DESCR_UUID, + read_perm=Attribute.OPEN, write_perm=Attribute.NO_ACCESS) + desc.value = struct.pack(' Date: Fri, 16 Aug 2019 15:20:14 -0400 Subject: [PATCH 7/8] discover_remote_services() now returns the services; no property needed --- adafruit_ble/current_time_client.py | 3 +-- adafruit_ble/uart_client.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/adafruit_ble/current_time_client.py b/adafruit_ble/current_time_client.py index 65c2d34..f3c5866 100644 --- a/adafruit_ble/current_time_client.py +++ b/adafruit_ble/current_time_client.py @@ -99,8 +99,7 @@ def pair(self): def discover(self): """Discover service information.""" self._check_connected() - self._periph.discover_remote_services((self.CTS_UUID,)) - services = self._periph.remote_services + services = self._periph.discover_remote_services((self.CTS_UUID,)) if not services: raise OSError("Unable to discover service") for characteristic in services[0].characteristics: diff --git a/adafruit_ble/uart_client.py b/adafruit_ble/uart_client.py index 29c1ed4..a4df20c 100644 --- a/adafruit_ble/uart_client.py +++ b/adafruit_ble/uart_client.py @@ -71,12 +71,12 @@ def connect(self, address, timeout): """ self._central.connect(address, timeout) # Restrict discovery to NUS service only. - self._central.discover_remote_services((NUS_SERVICE_UUID,)) + remote_services = self._central.discover_remote_services((NUS_SERVICE_UUID,)) # Connect succeeded. Get the remote characteristics we need, which were # found during discovery. - for characteristic in self._central.remote_services[0].characteristics: + for characteristic in remote_services[0].characteristics: # Since we're remote we receive on tx and send on rx. # The names are from the point of view of the server. if characteristic.uuid == NUS_RX_CHAR_UUID: From ab44563dae9161c268a8616bc3853792cf42b346 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Fri, 16 Aug 2019 18:18:01 -0400 Subject: [PATCH 8/8] Hide discovery and pairing in CurrentTimeClient --- adafruit_ble/current_time_client.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/adafruit_ble/current_time_client.py b/adafruit_ble/current_time_client.py index f3c5866..8ea5ffe 100644 --- a/adafruit_ble/current_time_client.py +++ b/adafruit_ble/current_time_client.py @@ -23,7 +23,7 @@ `adafruit_ble.current_time_client` ==================================================== -UART-style communication by a Central as a GATT Client +Connect to a Current Time Service, as a peripheral. * Author(s): Dan Halbert for Adafruit Industries @@ -43,14 +43,17 @@ class CurrentTimeClient: Example:: from adafruit_ble.current_time_client import CurrentTimeClient + import time cts_client = CurrentTimeClient() cts_client.start_advertising() while not cts_client.connected: pass - cts_client.discover() - cts_client.pair() - print(cts_client.current__time) + # The first time a property is read, the client + # will do discovery and pairing. + while True: + print(cts_client.current_time) + time.sleep(5) To try the example above, open Settings->Bluetooth on your iOS device. After the program starts advertising, ``CIRCUITPYxxxx` will show up as a Bluetooth @@ -90,15 +93,13 @@ def disconnect(self): 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 pair(self): - """Pair with the connected central.""" - self._check_connected() - self._periph.pair() - - def discover(self): + def _discover(self): """Discover service information.""" - self._check_connected() services = self._periph.discover_remote_services((self.CTS_UUID,)) if not services: raise OSError("Unable to discover service")