diff --git a/README.rst b/README.rst index b175d6b..d0c87ba 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,8 @@ Introduction :target: https://travis-ci.com/adafruit/Adafruit_CircuitPython_ble :alt: Build Status -This module provides higher-level BLE (Bluetooth Low Energy) functionality, building on the native `bleio` module. +This module provides higher-level BLE (Bluetooth Low Energy) functionality, +building on the native `_bleio` module. Dependencies ============= diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 47c2e21..20a3ca4 100644 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -24,7 +24,7 @@ ==================================================== This module provides higher-level BLE (Bluetooth Low Energy) functionality, -building on the native `bleio` module. +building on the native `_bleio` module. * Author(s): Dan Halbert for Adafruit Industries diff --git a/adafruit_ble/address.py b/adafruit_ble/address.py new file mode 100644 index 0000000..8c93b01 --- /dev/null +++ b/adafruit_ble/address.py @@ -0,0 +1,35 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Dan Halbert for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_ble.address` +==================================================== + +BLE Address + +* Author(s): Dan Halbert for Adafruit Industries + +""" + +from _bleio import UUID as _bleio_Address + +UUID = _bleio_Address +"""`adafruit_ble.Address` is the same as `_bleio.Address`""" diff --git a/adafruit_ble/advertising.py b/adafruit_ble/advertising.py index 0e2d831..3dd06eb 100644 --- a/adafruit_ble/advertising.py +++ b/adafruit_ble/advertising.py @@ -179,8 +179,11 @@ class Advertisement: consisting of an advertising data packet and an optional scan response packet. :param int flags: advertising flags. Default is general discovery, and BLE only (not classic) + :param int appearance: If not None, add BLE Appearance value to advertisement. + An Appearance describes what kind of device is advertising (keyboard, clock, + glucose meter, etc.) """ - def __init__(self, flags=None, tx_power=None): + def __init__(self, flags=None, tx_power=None, appearance=None): self._packet = AdvertisingPacket() self._scan_response_packet = None if flags: @@ -190,6 +193,8 @@ def __init__(self, flags=None, tx_power=None): if tx_power is not None: self._packet.add_tx_power(tx_power) + if appearance is not None: + self._packet.add_appearance(appearance) def add_name(self, name): """Add name to advertisement. If it doesn't fit, add truncated name to packet, @@ -246,10 +251,11 @@ class ServerAdvertisement(Advertisement): :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): - super().__init__() + 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, diff --git a/adafruit_ble/beacon.py b/adafruit_ble/beacon.py index c7891cd..7193b1a 100644 --- a/adafruit_ble/beacon.py +++ b/adafruit_ble/beacon.py @@ -30,7 +30,7 @@ """ import struct -import bleio +from _bleio import Peripheral from .advertising import AdvertisingPacket @@ -41,7 +41,7 @@ def __init__(self, advertising_packet): :param AdvertisingPacket advertising_packet """ - self._broadcaster = bleio.Peripheral(name=None) + self._broadcaster = Peripheral() self._advertising_packet = advertising_packet def start(self, interval=1.0): @@ -76,7 +76,7 @@ def __init__(self, company_id, uuid, major, minor, rssi): Example:: from adafruit_ble.beacon import LocationBeacon - from bleio import UUID + 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) diff --git a/adafruit_ble/current_time_client.py b/adafruit_ble/current_time_client.py index 8ea5ffe..5f036d0 100644 --- a/adafruit_ble/current_time_client.py +++ b/adafruit_ble/current_time_client.py @@ -31,7 +31,7 @@ import struct import time -from bleio import Peripheral, UUID +from _bleio import Peripheral, UUID from .advertising import SolicitationAdvertisement class CurrentTimeClient: @@ -66,7 +66,7 @@ class CurrentTimeClient: LOCAL_TIME_INFORMATION_UUID = UUID(0x2A0F) def __init__(self, name=None, tx_power=0): - self._periph = Peripheral(name=name) + 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 diff --git a/adafruit_ble/device_information_service.py b/adafruit_ble/device_information_service.py new file mode 100644 index 0000000..a5d00ef --- /dev/null +++ b/adafruit_ble/device_information_service.py @@ -0,0 +1,97 @@ +# 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.device_information` +==================================================== + +Device Information Service (DIS) + +* Author(s): Dan Halbert for Adafruit Industries + +""" +from _bleio import Attribute, Characteristic, Service, UUID + +class DeviceInformationService: + """This is a factory class only, and has no instances.""" + + @staticmethod + def add_to_peripheral(peripheral, *, model_number=None, serial_number=None, + firmware_revision=None, hardware_revision='', + software_revision='', manufacturer=''): + """ + Add a Service with fixed Device Information Service characteristics to the given Peripheral. + All values are optional. + + :param str model_number: Device model number. If None use `sys.platform`. + :param str serial_number: Device serial number. If None use a hex representation of + ``microcontroller.cpu.id``. + :param str firmware_revision: Device firmware revision. + If None use ``os.uname().version``. + :param str hardware_revision: Device hardware revision. + :param str software_revision: Device software revision. + :param str manufacturer: Device manufacturer name + :return: the created Service + + Example:: + + peripheral = Peripheral() + dis = DeviceInformationService.add_to_peripheral( + peripheral, software_revision="1.2.4", manufacturer="Acme Corp") + """ + + # Avoid creating constants with names if not necessary. Just takes up space. + # Device Information Service UUID = 0x180A + # Module Number UUID = 0x2A24 + # Serial Number UUID = 0x2A25 + # Firmware Revision UUID = 0x2A26 + # Hardware Revision UUID = 0x2A27 + # Software Revision UUID = 0x2A28 + # Manufacturer Name UUID = 0x2A29 + + service = Service.add_to_peripheral(peripheral, UUID(0x180A)) + + if model_number is None: + import sys + model_number = sys.platform + if serial_number is None: + import microcontroller + import binascii + serial_number = binascii.hexlify(microcontroller.cpu.uid).decode('utf-8') # pylint: disable=no-member + + if firmware_revision is None: + import os + firmware_revision = os.uname().version + + # Values must correspond to UUID numbers. + for uuid_num, value in zip( + range(0x2A24, 0x2A29+1), + (model_number, serial_number, + firmware_revision, hardware_revision, software_revision, + manufacturer)): + + Characteristic.add_to_service( + service, UUID(uuid_num), properties=Characteristic.READ, + read_perm=Attribute.OPEN, write_perm=Attribute.NO_ACCESS, + fixed_length=True, max_length=len(value), + initial_value=value) + + return service diff --git a/adafruit_ble/hid.py b/adafruit_ble/hid_server.py similarity index 55% rename from adafruit_ble/hid.py rename to adafruit_ble/hid_server.py index 5195512..3a9a758 100644 --- a/adafruit_ble/hid.py +++ b/adafruit_ble/hid_server.py @@ -1,4 +1,4 @@ - # The MIT License (MIT) +# The MIT License (MIT) # # Copyright (c) 2019 Dan Halbert for Adafruit Industries # @@ -30,10 +30,43 @@ """ import struct -from bleio import Attribute, Characteristic, Descriptor, Peripheral, Service, UUID +from micropython import const + +# for __version__ +import adafruit_ble + +from _bleio import Attribute, Characteristic, Descriptor, Peripheral, Service, UUID from .advertising import ServerAdvertisement +from .device_information_service import DeviceInformationService + +_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) +_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 HID: +class HIDServer: """ Provide devices for HID over BLE. @@ -41,23 +74,14 @@ class HID: Example:: - from adafruit_ble.hid import HID + from adafruit_ble.hid_server import HIDServer - hid = HID() + hid = HIDServer() """ - 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) + # These are used multiple times, so make them class constants. + _REPORT_UUID = UUID(_REPORT_UUID_NUM) + _REPORT_REF_DESCR_UUID = UUID(_REPORT_REF_DESCR_UUID_NUM) #pylint: disable=line-too-long HID_DESCRIPTOR = ( @@ -138,28 +162,28 @@ class HID: 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 + # 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 @@ -172,51 +196,100 @@ class HID: REPORT_ID_GAMEPAD = 5 """Gamepad device indicator, for use with `send_report()`.""" - REPORT_SIZES = { + _INPUT_REPORT_SIZES = { REPORT_ID_KEYBOARD : 8, REPORT_ID_MOUSE : 4, REPORT_ID_CONSUMER_CONTROL : 2, - REPORT_ID_GAMEPAD : 6, + # REPORT_ID_GAMEPAD : 6, + } + + _OUTPUT_REPORT_SIZES = { + REPORT_ID_KEYBOARD : 1, } def __init__(self, name=None, tx_power=0): + self._periph = Peripheral(name) + + # iOS requires Device Information Service. Android does not. + DeviceInformationService.add_to_peripheral( + self._periph, software_revision=adafruit_ble.__version__, + manufacturer="Adafruit Industries") + + hid_service = Service.add_to_peripheral(self._periph, UUID(_HID_SERVICE_UUID_NUM)) + 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('