├── .gitignore ├── .idea ├── blatann.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── .readthedocs.yaml ├── LICENSE ├── Makefile ├── README.md ├── blatann ├── __init__.py ├── bt_sig │ ├── __init__.py │ ├── assigned_numbers.py │ └── uuids.py ├── device.py ├── event_args.py ├── event_type.py ├── examples │ ├── __init__.py │ ├── __main__.py │ ├── broadcaster.py │ ├── central.py │ ├── central_async.py │ ├── central_battery_service.py │ ├── central_descriptors.py │ ├── central_device_info_service.py │ ├── central_event_driven.py │ ├── central_uart_service.py │ ├── constants.py │ ├── example_utils.py │ ├── peripheral.py │ ├── peripheral_async.py │ ├── peripheral_battery_service.py │ ├── peripheral_current_time_service.py │ ├── peripheral_descriptors.py │ ├── peripheral_device_info_service.py │ ├── peripheral_glucose_service.py │ ├── peripheral_rssi.py │ ├── peripheral_uart_service.py │ ├── scanner.py │ └── scanner_async.py ├── exceptions.py ├── gap │ ├── __init__.py │ ├── advertise_data.py │ ├── advertising.py │ ├── bond_db.py │ ├── default_bond_db.py │ ├── gap_types.py │ ├── generic_access_service.py │ ├── scanning.py │ ├── smp.py │ ├── smp_crypto.py │ └── smp_types.py ├── gatt │ ├── __init__.py │ ├── gattc.py │ ├── gattc_attribute.py │ ├── gatts.py │ ├── gatts_attribute.py │ ├── managers.py │ ├── reader.py │ ├── service_discovery.py │ └── writer.py ├── nrf │ ├── __init__.py │ ├── nrf_dll_load.py │ ├── nrf_driver.py │ ├── nrf_driver_types.py │ ├── nrf_events │ │ ├── __init__.py │ │ ├── gap_events.py │ │ ├── gatt_events.py │ │ ├── generic_events.py │ │ └── smp_events.py │ └── nrf_types │ │ ├── __init__.py │ │ ├── config.py │ │ ├── enums.py │ │ ├── gap.py │ │ ├── gatt.py │ │ ├── generic.py │ │ └── smp.py ├── peer.py ├── services │ ├── __init__.py │ ├── battery │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── data_types.py │ │ └── service.py │ ├── ble_data_types.py │ ├── current_time │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── data_types.py │ │ └── service.py │ ├── decoded_event_dispatcher.py │ ├── device_info │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── data_types.py │ │ └── service.py │ ├── glucose │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── data_types.py │ │ ├── database.py │ │ ├── racp.py │ │ └── service.py │ └── nordic_uart │ │ ├── __init__.py │ │ ├── constants.py │ │ └── service.py ├── utils │ ├── __init__.py │ ├── _threading.py │ └── queued_tasks_manager.py ├── uuid.py └── waitables │ ├── __init__.py │ ├── connection_waitable.py │ ├── event_queue.py │ ├── event_waitable.py │ ├── scan_waitable.py │ └── waitable.py ├── docs ├── Makefile ├── requirements.txt └── source │ ├── _static │ └── images │ │ └── blatann_architecture.png │ ├── api_reference.rst │ ├── architecture.rst │ ├── blatann.bt_sig.assigned_numbers.rst │ ├── blatann.bt_sig.rst │ ├── blatann.bt_sig.uuids.rst │ ├── blatann.device.rst │ ├── blatann.event_args.rst │ ├── blatann.event_type.rst │ ├── blatann.examples.broadcaster.rst │ ├── blatann.examples.central.rst │ ├── blatann.examples.central_async.rst │ ├── blatann.examples.central_battery_service.rst │ ├── blatann.examples.central_descriptors.rst │ ├── blatann.examples.central_device_info_service.rst │ ├── blatann.examples.central_event_driven.rst │ ├── blatann.examples.central_uart_service.rst │ ├── blatann.examples.constants.rst │ ├── blatann.examples.example_utils.rst │ ├── blatann.examples.peripheral.rst │ ├── blatann.examples.peripheral_async.rst │ ├── blatann.examples.peripheral_battery_service.rst │ ├── blatann.examples.peripheral_current_time_service.rst │ ├── blatann.examples.peripheral_descriptors.rst │ ├── blatann.examples.peripheral_device_info_service.rst │ ├── blatann.examples.peripheral_glucose_service.rst │ ├── blatann.examples.peripheral_rssi.rst │ ├── blatann.examples.peripheral_uart_service.rst │ ├── blatann.examples.rst │ ├── blatann.examples.scanner.rst │ ├── blatann.examples.scanner_async.rst │ ├── blatann.exceptions.rst │ ├── blatann.gap.advertise_data.rst │ ├── blatann.gap.advertising.rst │ ├── blatann.gap.bond_db.rst │ ├── blatann.gap.default_bond_db.rst │ ├── blatann.gap.gap_types.rst │ ├── blatann.gap.generic_access_service.rst │ ├── blatann.gap.rst │ ├── blatann.gap.scanning.rst │ ├── blatann.gap.smp.rst │ ├── blatann.gap.smp_crypto.rst │ ├── blatann.gap.smp_types.rst │ ├── blatann.gatt.gattc.rst │ ├── blatann.gatt.gattc_attribute.rst │ ├── blatann.gatt.gatts.rst │ ├── blatann.gatt.gatts_attribute.rst │ ├── blatann.gatt.managers.rst │ ├── blatann.gatt.reader.rst │ ├── blatann.gatt.rst │ ├── blatann.gatt.service_discovery.rst │ ├── blatann.gatt.writer.rst │ ├── blatann.nrf.nrf_dll_load.rst │ ├── blatann.nrf.nrf_driver.rst │ ├── blatann.nrf.nrf_driver_types.rst │ ├── blatann.nrf.nrf_events.gap_events.rst │ ├── blatann.nrf.nrf_events.gatt_events.rst │ ├── blatann.nrf.nrf_events.generic_events.rst │ ├── blatann.nrf.nrf_events.rst │ ├── blatann.nrf.nrf_events.smp_events.rst │ ├── blatann.nrf.nrf_types.config.rst │ ├── blatann.nrf.nrf_types.enums.rst │ ├── blatann.nrf.nrf_types.gap.rst │ ├── blatann.nrf.nrf_types.gatt.rst │ ├── blatann.nrf.nrf_types.generic.rst │ ├── blatann.nrf.nrf_types.rst │ ├── blatann.nrf.nrf_types.smp.rst │ ├── blatann.nrf.rst │ ├── blatann.peer.rst │ ├── blatann.rst │ ├── blatann.services.battery.constants.rst │ ├── blatann.services.battery.data_types.rst │ ├── blatann.services.battery.rst │ ├── blatann.services.battery.service.rst │ ├── blatann.services.ble_data_types.rst │ ├── blatann.services.current_time.constants.rst │ ├── blatann.services.current_time.data_types.rst │ ├── blatann.services.current_time.rst │ ├── blatann.services.current_time.service.rst │ ├── blatann.services.decoded_event_dispatcher.rst │ ├── blatann.services.device_info.constants.rst │ ├── blatann.services.device_info.data_types.rst │ ├── blatann.services.device_info.rst │ ├── blatann.services.device_info.service.rst │ ├── blatann.services.glucose.constants.rst │ ├── blatann.services.glucose.data_types.rst │ ├── blatann.services.glucose.database.rst │ ├── blatann.services.glucose.racp.rst │ ├── blatann.services.glucose.rst │ ├── blatann.services.glucose.service.rst │ ├── blatann.services.nordic_uart.constants.rst │ ├── blatann.services.nordic_uart.rst │ ├── blatann.services.nordic_uart.service.rst │ ├── blatann.services.rst │ ├── blatann.utils.queued_tasks_manager.rst │ ├── blatann.utils.rst │ ├── blatann.uuid.rst │ ├── blatann.waitables.connection_waitable.rst │ ├── blatann.waitables.event_queue.rst │ ├── blatann.waitables.event_waitable.rst │ ├── blatann.waitables.rst │ ├── blatann.waitables.scan_waitable.rst │ ├── blatann.waitables.waitable.rst │ ├── blatann_architecture.html │ ├── changelog.rst │ ├── compatibility_matrix.rst │ ├── conf.py │ ├── core_classes.rst │ ├── examples.rst │ ├── getting_started.rst │ ├── index.rst │ ├── modules.rst │ └── troubleshooting.rst ├── pyproject.toml ├── requirements.txt ├── tests ├── __init__.py └── integrated │ ├── .gitignore │ ├── __init__.py │ ├── base.py │ ├── helpers.py │ ├── test_advertising_data.py │ ├── test_advertising_duration.py │ ├── test_gap.py │ ├── test_gatt.py │ ├── test_gatt_writes.py │ ├── test_scanner.py │ └── test_security.py └── tools └── macos_retarget_pc_ble_driver_py.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OS generated files 3 | .DS_Store 4 | 5 | # Generated Protobuf modules 6 | *_pb2.py 7 | 8 | # Log files 9 | *.log 10 | 11 | # Compiled python objects 12 | *.py[co] 13 | 14 | # Auto-generated version file 15 | version.py 16 | 17 | # User-specific project settings 18 | .idea/workspace.xml 19 | .idea/tasks.xml 20 | .idea/dictionaries 21 | .idea/encodings.xml 22 | .idea/misc.xml 23 | 24 | # Sensitive or high-churn files: 25 | .idea/dataSources.ids 26 | .idea/dataSources.xml 27 | .idea/sqlDataSources.xml 28 | .idea/dynamic.xml 29 | .idea/uiDesigner.xml 30 | .idea/preferred-vcs.xml 31 | 32 | # build outputs 33 | build/ 34 | dist/ 35 | *egg-info/ 36 | 37 | # Code coverage files 38 | .coverage 39 | _coverage/ 40 | 41 | # Log Files 42 | log/ 43 | 44 | # User-specific config files 45 | blatann/.user 46 | 47 | # Auto-generated documentation 48 | docs/_build 49 | -------------------------------------------------------------------------------- /.idea/blatann.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.10" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/source/conf.py 16 | 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Thomas Gerstenberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Suspend command echo for non-verbose builds 2 | ifeq ("$(VERBOSE)","1") 3 | NO_ECHO := 4 | TEST_VERBOSE := -v 5 | else 6 | NO_ECHO := @ 7 | TEST_VERBOSE := 8 | endif 9 | 10 | # Set up project-relative source paths 11 | BUILD_OUTPUTS := $(abspath ./dist) $(abspath ./build) $(abspath ./blatann.egg-info) 12 | 13 | TEST_ROOT := $(abspath ./tests) 14 | TEST_VERBOSE := -v 15 | 16 | # Utility commands 17 | RM := rm -rf 18 | CD := cd 19 | CP := cp 20 | TOUCH := touch 21 | MKDIR := mkdir 22 | CAT := cat 23 | 24 | ifeq ($(OS),Windows_NT) 25 | PYTHON ?= python 26 | else 27 | PYTHON ?= python3 28 | endif 29 | 30 | # Python-based commands 31 | PIP := $(PYTHON) -m pip 32 | VENV := $(PYTHON) -m venv 33 | COVERAGE := $(PYTHON) -m coverage 34 | RUFF := $(PYTHON) -m ruff 35 | ISORT := $(PYTHON) -m isort 36 | 37 | # Target Definitions 38 | 39 | .PHONY: default binaries clean run-tests setup-dev lint-check format 40 | 41 | # First target, default to building the wheel 42 | default: binaries 43 | 44 | binaries: 45 | $(NO_ECHO)$(PYTHON) -m build 46 | 47 | clean: 48 | $(NO_ECHO)$(RM) $(BUILD_OUTPUTS) 49 | 50 | setup-dev: 51 | $(NO_ECHO)$(PIP) install -e .[dev] 52 | 53 | run-tests: 54 | $(NO_ECHO)$(PYTHON) -m unittest discover $(TEST_VERBOSE) -s $(TEST_ROOT) -t $(TEST_ROOT) 55 | 56 | lint-check: 57 | $(NO_ECHO)$(RUFF) check . 58 | $(NO_ECHO)$(ISORT) --check . 59 | 60 | format: 61 | $(NO_ECHO)$(RUFF) check --fix . 62 | $(NO_ECHO)$(ISORT) . 63 | -------------------------------------------------------------------------------- /blatann/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pc_ble_driver_py import config 4 | 5 | config.__conn_ic_id__ = "NRF52" 6 | 7 | __version__ = "0.6.0" 8 | 9 | from blatann.device import BleDevice # noqa: E402 10 | -------------------------------------------------------------------------------- /blatann/bt_sig/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/blatann/bt_sig/__init__.py -------------------------------------------------------------------------------- /blatann/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/blatann/examples/__init__.py -------------------------------------------------------------------------------- /blatann/examples/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, annotations 2 | 3 | import glob 4 | import sys 5 | from os import path 6 | 7 | import blatann.examples 8 | 9 | # Non-example python files to exclude 10 | EXCLUDES = ["__init__", "__main__", "constants", "example_utils"] 11 | 12 | _py_files = [path.splitext(path.basename(f))[0] for f in glob.glob(path.dirname(__file__) + "/*.py")] 13 | examples = [f for f in _py_files if f not in EXCLUDES] 14 | 15 | 16 | def print_help(): 17 | print("\nUsage: python -m blatann.examples [example_filename] [comport]") 18 | print("Examples:") 19 | for e in examples: 20 | print(" {}".format(e)) 21 | sys.exit(1) 22 | 23 | 24 | def main(): 25 | if len(sys.argv) < 3: 26 | print_help() 27 | 28 | example_name = sys.argv[1] 29 | comport = sys.argv[2] 30 | 31 | if example_name not in examples: 32 | print("Unknown example {}".format(example_name)) 33 | print_help() 34 | 35 | module = __import__(blatann.examples.__name__, fromlist=[example_name]) 36 | example = getattr(module, example_name) 37 | example.main(comport) 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /blatann/examples/broadcaster.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example of a broadcaster role BLE device. It advertises as a non-connectable device 3 | and emits the device's current time as a part of the advertising data. 4 | """ 5 | from __future__ import annotations 6 | 7 | import threading 8 | import time 9 | 10 | from blatann import BleDevice 11 | from blatann.examples import example_utils 12 | from blatann.gap.advertising import AdvertisingData, AdvertisingFlags, AdvertisingMode 13 | 14 | logger = example_utils.setup_logger(level="DEBUG") 15 | 16 | 17 | def _get_time_service_data(): 18 | # Get the current time 19 | t = time.strftime("%H:%M:%S", time.localtime()) 20 | # Service data is 2 bytes UUID + data, use UUID 0x2143 21 | return "\x43\x21" + t 22 | 23 | 24 | def wait_for_user_stop(stop_event): 25 | # Thread that just waits for the user to press enter, then signals the event 26 | input("Press enter to exit\n") 27 | stop_event.set() 28 | 29 | 30 | def main(serial_port): 31 | stop_event = threading.Event() 32 | threading.Thread(target=wait_for_user_stop, args=(stop_event, )).start() 33 | time.sleep(2) 34 | 35 | ble_device = BleDevice(serial_port) 36 | ble_device.open() 37 | 38 | interval_ms = 100 # Send out an advertising packet every 100ms 39 | timeout_sec = 0 # Advertise forever 40 | mode = AdvertisingMode.non_connectable_undirected # Set mode to not allow connections 41 | 42 | adv_flags = AdvertisingFlags.BR_EDR_NOT_SUPPORTED | AdvertisingFlags.GENERAL_DISCOVERY_MODE 43 | adv_data = AdvertisingData(flags=adv_flags, local_name="Time", service_data=_get_time_service_data()) 44 | 45 | ble_device.advertiser.set_advertise_data(adv_data) 46 | ble_device.advertiser.start(interval_ms, timeout_sec, auto_restart=True, advertise_mode=mode) 47 | 48 | while True: 49 | # Update the advertising data every 1 second 50 | stop_event.wait(1) 51 | if stop_event.is_set(): # User stopped execution, break out and stop advertising 52 | break 53 | # Update the service data and set it in the BLE device 54 | adv_data.service_data = _get_time_service_data() 55 | ble_device.advertiser.set_advertise_data(adv_data) 56 | 57 | ble_device.advertiser.stop() 58 | ble_device.close() 59 | 60 | 61 | if __name__ == '__main__': 62 | main("COM8") 63 | -------------------------------------------------------------------------------- /blatann/examples/central_battery_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates reading and subscribing to a peripheral's Battery service to get 3 | updates on the peripheral's current battery levels. 4 | The operations here are programmed in a procedural manner. 5 | 6 | This can be used alongside any peripheral which implements the Battery Service and 7 | advertises the 16-bit Battery Service UUID. 8 | The peripheral_battery_service example can be used with this. 9 | """ 10 | from __future__ import annotations 11 | 12 | import time 13 | 14 | from blatann import BleDevice 15 | from blatann.nrf import nrf_events 16 | from blatann.services import battery 17 | from blatann.utils import setup_logger 18 | 19 | logger = setup_logger(level="INFO") 20 | 21 | 22 | def on_battery_level_update(battery_service, event_args): 23 | """ 24 | :param battery_service: 25 | :type event_args: blatann.event_args.DecodedReadCompleteEventArgs 26 | """ 27 | battery_percent = event_args.value 28 | logger.info("Battery: {}%".format(battery_percent)) 29 | 30 | 31 | def main(serial_port): 32 | # Open the BLE Device and suppress spammy log messages 33 | ble_device = BleDevice(serial_port) 34 | ble_device.event_logger.suppress(nrf_events.GapEvtAdvReport) 35 | ble_device.open() 36 | 37 | # Set scan duration for 4 seconds 38 | ble_device.scanner.set_default_scan_params(timeout_seconds=4) 39 | logger.info("Scanning for peripherals advertising UUID {}".format(battery.BATTERY_SERVICE_UUID)) 40 | 41 | target_address = None 42 | # Start scan and wait for it to complete 43 | scan_report = ble_device.scanner.start_scan().wait() 44 | # Search each peer's advertising data for the Battery Service UUID to be advertised 45 | for report in scan_report.advertising_peers_found: 46 | if battery.BATTERY_SERVICE_UUID in report.advertise_data.service_uuid16s: 47 | target_address = report.peer_address 48 | break 49 | 50 | if not target_address: 51 | logger.info("Did not find peripheral advertising battery service") 52 | return 53 | 54 | # Initiate connection and wait for it to finish 55 | logger.info("Found match: connecting to address {}".format(target_address)) 56 | peer = ble_device.connect(target_address).wait() 57 | if not peer: 58 | logger.warning("Timed out connecting to device") 59 | return 60 | 61 | logger.info("Connected, conn_handle: {}".format(peer.conn_handle)) 62 | # Initiate service discovery and wait for it to complete 63 | _, event_args = peer.discover_services().wait(10, exception_on_timeout=False) 64 | logger.info("Service discovery complete! status: {}".format(event_args.status)) 65 | 66 | # Find the battery service within the peer's database 67 | battery_service = battery.find_battery_service(peer.database) 68 | if not battery_service: 69 | logger.info("Failed to find Battery Service in peripheral database") 70 | peer.disconnect().wait() 71 | return 72 | 73 | # Read out the battery level 74 | logger.info("Reading battery level...") 75 | _, event_args = battery_service.read().wait() 76 | battery_percent = event_args.value 77 | logger.info("Battery: {}%".format(battery_percent)) 78 | 79 | if battery_service.can_enable_notifications: 80 | battery_service.on_battery_level_updated.register(on_battery_level_update) 81 | battery_service.enable_notifications().wait() 82 | 83 | wait_duration = 30 84 | logger.info("Waiting {} seconds for any battery notifications".format(wait_duration)) 85 | time.sleep(wait_duration) 86 | 87 | # Clean up 88 | logger.info("Disconnecting from peripheral") 89 | peer.disconnect().wait() 90 | ble_device.close() 91 | 92 | 93 | if __name__ == '__main__': 94 | main("COM9") 95 | -------------------------------------------------------------------------------- /blatann/examples/central_descriptors.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to read descriptors of a peripheral's characteristic. 3 | 4 | This can be used with the peripheral_descriptor example running on a separate nordic device. 5 | """ 6 | from __future__ import annotations 7 | 8 | import binascii 9 | 10 | from blatann import BleDevice 11 | from blatann.bt_sig.uuids import DescriptorUuid 12 | from blatann.examples import constants, example_utils 13 | from blatann.gatt import GattStatusCode, PresentationFormat 14 | 15 | logger = example_utils.setup_logger(level="INFO") 16 | 17 | 18 | def main(serial_port): 19 | # Create and open the device 20 | ble_device = BleDevice(serial_port) 21 | ble_device.configure() 22 | ble_device.open() 23 | 24 | ble_device.scanner.set_default_scan_params(timeout_seconds=4) 25 | 26 | # Set the target to the peripheral's advertised name 27 | target_device_name = constants.PERIPHERAL_NAME 28 | 29 | logger.info("Scanning for '{}'".format(target_device_name)) 30 | target_address = example_utils.find_target_device(ble_device, target_device_name) 31 | 32 | if not target_address: 33 | logger.info("Did not find target peripheral") 34 | return 35 | 36 | # Initiate the connection and wait for it to finish 37 | logger.info("Found match: connecting to address {}".format(target_address)) 38 | peer = ble_device.connect(target_address).wait() 39 | if not peer: 40 | logger.warning("Timed out connecting to device") 41 | return 42 | logger.info("Connected, conn_handle: {}".format(peer.conn_handle)) 43 | 44 | # Wait up to 10 seconds for service discovery to complete 45 | _, event_args = peer.discover_services().wait(10, exception_on_timeout=False) 46 | logger.info("Service discovery complete! status: {}".format(event_args.status)) 47 | 48 | # Find the Time characteristic 49 | time_char = peer.database.find_characteristic(constants.DESC_EXAMPLE_CHAR_UUID) 50 | if not time_char: 51 | logger.info("Did not find the time characteristic") 52 | else: 53 | logger.info("Reading all characteristic attributes") 54 | for attr in time_char.attributes: 55 | logger.info(f"Reading UUID {attr.uuid} - {attr.uuid.description or '[unknown]'}") 56 | _, event_args = attr.read().wait(5) 57 | if event_args.status == GattStatusCode.success: 58 | # Show as hex unless it's the user descriptor UUID which should be a string 59 | if attr.uuid == DescriptorUuid.user_description: 60 | value = event_args.value.decode("utf8") 61 | else: 62 | value = binascii.hexlify(event_args.value) 63 | logger.info(f" Value: {value}") 64 | else: 65 | logger.warning(f" Failed to read attribute, status: {event_args.status}") 66 | 67 | # Read the characteristic's Presentation Format descriptor directly and decode the value 68 | presentation_fmt_desc = time_char.find_descriptor(DescriptorUuid.presentation_format) 69 | if presentation_fmt_desc: 70 | # Read, then decode the value using the PresentationFormat type 71 | logger.info("Reading the presentation format descriptor directly") 72 | _, event_args = presentation_fmt_desc.read().wait(5) 73 | if event_args.status == GattStatusCode.success: 74 | try: 75 | fmt = PresentationFormat.decode(event_args.value) 76 | logger.info(f"Presentation Format: {fmt.format}, Exponent: {fmt.exponent}, Unit: {fmt.unit}") 77 | except Exception as e: 78 | logger.error("Failed to decode the presentation format descriptor") 79 | logger.exception(e) 80 | else: 81 | logger.info("Failed to find the presentation format descriptor") 82 | 83 | # Clean up 84 | logger.info("Disconnecting from peripheral") 85 | peer.disconnect().wait() 86 | ble_device.close() 87 | 88 | 89 | if __name__ == '__main__': 90 | main("COM11") 91 | -------------------------------------------------------------------------------- /blatann/examples/central_device_info_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates reading a peripheral's Device Info Service using blatann's device_info service module. 3 | The operations here are programmed in a procedural manner. 4 | 5 | This can be used alongside any peripheral which implements the DIS and advertises the 16-bit DIS service UUID. 6 | The peripheral_device_info_service example can be used with this. 7 | """ 8 | from __future__ import annotations 9 | 10 | from blatann import BleDevice 11 | from blatann.examples import example_utils 12 | from blatann.nrf import nrf_events 13 | from blatann.services import device_info 14 | 15 | logger = example_utils.setup_logger(level="INFO") 16 | 17 | 18 | def main(serial_port): 19 | # Open the BLE Device and suppress spammy log messages 20 | ble_device = BleDevice(serial_port) 21 | ble_device.event_logger.suppress(nrf_events.GapEvtAdvReport) 22 | ble_device.open() 23 | 24 | # Set scan duration to 4 seconds 25 | ble_device.scanner.set_default_scan_params(timeout_seconds=4) 26 | logger.info("Scanning for peripherals advertising UUID {}".format(device_info.DIS_SERVICE_UUID)) 27 | 28 | target_address = None 29 | # Start scan and wait for it to complete 30 | scan_report = ble_device.scanner.start_scan().wait() 31 | # Search each peer's advertising data for the DIS Service UUID to be advertised 32 | for report in scan_report.advertising_peers_found: 33 | if device_info.DIS_SERVICE_UUID in report.advertise_data.service_uuid16s: 34 | target_address = report.peer_address 35 | break 36 | 37 | if not target_address: 38 | logger.info("Did not find peripheral advertising DIS service") 39 | return 40 | 41 | # Initiate connection and wait for it to finish 42 | logger.info("Found match: connecting to address {}".format(target_address)) 43 | peer = ble_device.connect(target_address).wait() 44 | if not peer: 45 | logger.warning("Timed out connecting to device") 46 | return 47 | 48 | logger.info("Connected, conn_handle: {}".format(peer.conn_handle)) 49 | # Initiate service discovery and wait for it to complete 50 | _, event_args = peer.discover_services().wait(10, exception_on_timeout=False) 51 | logger.info("Service discovery complete! status: {}".format(event_args.status)) 52 | 53 | # Find the device info service in the peer's database 54 | dis = device_info.find_device_info_service(peer.database) 55 | if not dis: 56 | logger.info("Failed to find Device Info Service in peripheral database") 57 | peer.disconnect().wait() 58 | return 59 | 60 | # Example 1: 61 | # Iterate through all possible device info characteristics, read the value if present in service 62 | for char in device_info.CHARACTERISTICS: 63 | if dis.has(char): 64 | logger.info("Reading characteristic: {}...".format(char)) 65 | _, event_args = dis.get(char).wait() 66 | if isinstance(event_args.value, bytes): 67 | value = event_args.value.decode("utf8") 68 | else: 69 | value = event_args.value 70 | logger.info("Value: {}".format(value)) 71 | 72 | # Example 2: 73 | # Read specific characteristics, if present in the service 74 | if dis.has_software_revision: 75 | char, event_args = dis.get_software_revision().wait() 76 | sw_version = event_args.value 77 | logger.info("Software Version: {}".format(sw_version.decode("utf8"))) 78 | if dis.has_pnp_id: 79 | char, event_args = dis.get_pnp_id().wait() 80 | pnp_id = event_args.value # type: device_info.PnpId 81 | logger.info("Vendor ID: {}".format(pnp_id.vendor_id)) 82 | if dis.has_system_id: 83 | char, event_args = dis.get_system_id().wait() 84 | system_id = event_args.value # type: device_info.SystemId 85 | logger.info("System ID: {}".format(system_id)) 86 | 87 | # Disconnect and close device 88 | peer.disconnect().wait() 89 | ble_device.close() 90 | 91 | 92 | if __name__ == '__main__': 93 | main("COM9") 94 | -------------------------------------------------------------------------------- /blatann/examples/central_uart_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example implements Nordic's custom UART service and demonstrates how to configure the MTU size. 3 | It is configured to use an MTU size based on the Data Length Extensions feature of BLE for maximum throughput. 4 | This is compatible with the peripheral_uart_service example. 5 | 6 | This is a simple example which just echos back any data that the client sends to it. 7 | """ 8 | from __future__ import annotations 9 | 10 | from builtins import input 11 | 12 | from blatann import BleDevice 13 | from blatann.gatt import MTU_SIZE_FOR_MAX_DLE 14 | from blatann.nrf import nrf_events 15 | from blatann.services import nordic_uart 16 | from blatann.utils import setup_logger 17 | 18 | logger = setup_logger(level="DEBUG") 19 | 20 | 21 | def on_connect(peer, event_args): 22 | """ 23 | Event callback for when a central device connects to us 24 | 25 | :param peer: The peer that connected to us 26 | :type peer: blatann.peer.Client 27 | :param event_args: None 28 | """ 29 | if peer: 30 | logger.info("Connected to peer") 31 | else: 32 | logger.warning("Connection timed out") 33 | 34 | 35 | def on_disconnect(peer, event_args): 36 | """ 37 | Event callback for when the client disconnects from us (or when we disconnect from the client) 38 | 39 | :param peer: The peer that disconnected 40 | :type peer: blatann.peer.Client 41 | :param event_args: The event args 42 | :type event_args: blatann.event_args.DisconnectionEventArgs 43 | """ 44 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 45 | 46 | 47 | def on_mtu_size_update(peer, event_args): 48 | """ 49 | Callback for when the peer's MTU size has been updated/negotiated 50 | 51 | :param peer: The peer the MTU was updated on 52 | :type peer: blatann.peer.Client 53 | :type event_args: blatann.event_args.MtuSizeUpdatedEventArgs 54 | """ 55 | logger.info("MTU size updated from {} to {}".format(event_args.previous_mtu_size, event_args.current_mtu_size)) 56 | 57 | 58 | def on_data_rx(service, data): 59 | """ 60 | Called whenever data is received on the RX line of the Nordic UART Service 61 | 62 | :param service: the service the data was received from 63 | :type service: nordic_uart.service.NordicUartClient 64 | :param data: The data that was received 65 | :type data: bytes 66 | """ 67 | logger.info("Received data (len {}): '{}'".format(len(data), data)) 68 | 69 | 70 | def main(serial_port): 71 | # Open the BLE Device and suppress spammy log messages 72 | ble_device = BleDevice(serial_port) 73 | ble_device.event_logger.suppress(nrf_events.GapEvtAdvReport) 74 | # Configure the BLE device to support MTU sizes which allow the max data length extension PDU size 75 | # Note this isn't 100% necessary as the default configuration sets the max to this value also 76 | ble_device.configure(att_mtu_max_size=MTU_SIZE_FOR_MAX_DLE) 77 | ble_device.open() 78 | 79 | # Set scan duration for 4 seconds 80 | ble_device.scanner.set_default_scan_params(timeout_seconds=4) 81 | ble_device.set_default_peripheral_connection_params(7.5, 15, 4000) 82 | logger.info("Scanning for peripherals advertising UUID {}".format(nordic_uart.NORDIC_UART_SERVICE_UUID)) 83 | 84 | target_address = None 85 | # Start scan and wait for it to complete 86 | scan_report = ble_device.scanner.start_scan().wait() 87 | # Search each peer's advertising data for the Nordic UART Service UUID to be advertised 88 | for report in scan_report.advertising_peers_found: 89 | if nordic_uart.NORDIC_UART_SERVICE_UUID in report.advertise_data.service_uuid128s and report.device_name == "Nordic UART Server": 90 | target_address = report.peer_address 91 | break 92 | 93 | if not target_address: 94 | logger.info("Did not find peripheral advertising Nordic UART service") 95 | return 96 | 97 | # Initiate connection and wait for it to finish 98 | logger.info("Found match: connecting to address {}".format(target_address)) 99 | peer = ble_device.connect(target_address).wait() 100 | if not peer: 101 | logger.warning("Timed out connecting to device") 102 | return 103 | 104 | logger.info("Connected, conn_handle: {}".format(peer.conn_handle)) 105 | 106 | logger.info("Exchanging MTU") 107 | peer.exchange_mtu(peer.max_mtu_size).wait(10) 108 | logger.info("MTU Exchange complete, discovering services") 109 | 110 | # Initiate service discovery and wait for it to complete 111 | _, event_args = peer.discover_services().wait(exception_on_timeout=False) 112 | logger.info("Service discovery complete! status: {}".format(event_args.status)) 113 | 114 | uart_service = nordic_uart.find_nordic_uart_service(peer.database) 115 | if not uart_service: 116 | logger.info("Failed to find Nordic UART service in peripheral database") 117 | peer.disconnect().wait() 118 | ble_device.close() 119 | return 120 | 121 | # Initialize the service 122 | uart_service.initialize().wait(5) 123 | uart_service.on_data_received.register(on_data_rx) 124 | 125 | while True: 126 | data = input("Enter data to send to peripheral (q to exit): ") 127 | if data == "q": 128 | break 129 | uart_service.write(data).wait(10) 130 | 131 | peer.disconnect().wait() 132 | ble_device.close() 133 | 134 | 135 | if __name__ == '__main__': 136 | main("COM9") 137 | -------------------------------------------------------------------------------- /blatann/examples/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.gatt import gatts 4 | from blatann.uuid import Uuid128 5 | 6 | PERIPHERAL_NAME = "Periph Test" 7 | 8 | 9 | # Miscellaneous service, used for testing 10 | MATH_SERVICE_UUID = Uuid128("deadbeef-0011-2345-6679-ab12ccd4f550") 11 | 12 | # Hex conversion characteristic. A central writes a byte sequence to this characteristic 13 | # and the peripheral will convert it to its hex representation. e.g. "0123" -> "30313233" 14 | HEX_CONVERT_CHAR_UUID = MATH_SERVICE_UUID.new_uuid_from_base(0xbeaa) 15 | 16 | # Counting characteristic. The peripheral will periodically send out a notification on this characteristic 17 | # With a monotonically-increasing, 4-byte little-endian number 18 | COUNTING_CHAR_UUID = MATH_SERVICE_UUID.new_uuid_from_base("1234") 19 | 20 | # Properties for the hex conversion and counting characteristics 21 | _HEX_CONVERT_USER_DESC = gatts.GattsUserDescriptionProperties("Hex Converter") 22 | HEX_CONVERT_CHAR_PROPERTIES = gatts.GattsCharacteristicProperties(read=True, notify=True, indicate=True, write=True, 23 | max_length=128, variable_length=True, 24 | user_description=_HEX_CONVERT_USER_DESC) 25 | 26 | _COUNTING_CHAR_USER_DESC = gatts.GattsUserDescriptionProperties("Counter") 27 | COUNTING_CHAR_PROPERTIES = gatts.GattsCharacteristicProperties(read=False, notify=True, max_length=4, variable_length=False, 28 | user_description=_COUNTING_CHAR_USER_DESC) 29 | 30 | # Time service, report's the current time. Also demonstrating another wait to create a UUID 31 | _TIME_SERVICE_BASE_UUID = "beef0000-0123-4567-89ab-cdef01234567" 32 | TIME_SERVICE_UUID = Uuid128.combine_with_base("dead", _TIME_SERVICE_BASE_UUID) 33 | 34 | # Time characteristic. When read, reports the peripheral's current time in a human-readable string 35 | TIME_CHAR_UUID = TIME_SERVICE_UUID.new_uuid_from_base("dddd") 36 | 37 | # Properties for the time characteristic 38 | _TIME_CHAR_USER_DESC = gatts.GattsUserDescriptionProperties("Current Time") 39 | TIME_CHAR_PROPERTIES = gatts.GattsCharacteristicProperties(read=True, max_length=30, variable_length=True, user_description=_TIME_CHAR_USER_DESC) 40 | 41 | 42 | DESC_EXAMPLE_SERVICE_UUID = Uuid128("12340000-5678-90ab-cdef-001122334455") 43 | DESC_EXAMPLE_CHAR_UUID = DESC_EXAMPLE_SERVICE_UUID.new_uuid_from_base("0001") 44 | -------------------------------------------------------------------------------- /blatann/examples/example_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann import BleDevice 4 | from blatann.utils import setup_logger 5 | 6 | 7 | def find_target_device(ble_device: BleDevice, name: str): 8 | """ 9 | Starts the scanner and searches the advertising report for the desired name. 10 | If found, returns the peer's address that can be connected to 11 | 12 | :param ble_device: The ble device to operate on 13 | :param name: The device's local name that is advertised 14 | :return: The peer's address if found, or None if not found 15 | """ 16 | # Start scanning for the peripheral. 17 | # Using the `scan_reports` iterable on the waitable will return the scan reports as they're 18 | # discovered in real-time instead of waiting for the full scan to complete 19 | for report in ble_device.scanner.start_scan().scan_reports: 20 | if report.advertise_data.local_name == name: 21 | return report.peer_address 22 | 23 | 24 | async def find_target_device_async(ble_device: BleDevice, name: str): 25 | async for report in ble_device.scanner.start_scan().scan_reports_async: 26 | if report.advertise_data.local_name == name: 27 | return report.peer_address 28 | -------------------------------------------------------------------------------- /blatann/examples/peripheral_battery_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates using Bluetooth SIG's Battery service as a peripheral. 3 | The peripheral adds the service, then updates the battery level percentage periodically. 4 | 5 | This can be used in conjunction with the nRF Connect apps to explore the functionality demonstrated 6 | """ 7 | from __future__ import annotations 8 | 9 | import time 10 | 11 | from blatann import BleDevice 12 | from blatann.gap import advertising 13 | from blatann.services import battery 14 | from blatann.utils import setup_logger 15 | 16 | logger = setup_logger(level="INFO") 17 | 18 | 19 | def on_connect(peer, event_args): 20 | """ 21 | Event callback for when a central device connects to us 22 | 23 | :param peer: The peer that connected to us 24 | :type peer: blatann.peer.Client 25 | :param event_args: None 26 | """ 27 | if peer: 28 | logger.info("Connected to peer") 29 | else: 30 | logger.warning("Connection timed out") 31 | 32 | 33 | def on_disconnect(peer, event_args): 34 | """ 35 | Event callback for when the client disconnects from us (or when we disconnect from the client) 36 | 37 | :param peer: The peer that disconnected 38 | :type peer: blatann.peer.Client 39 | :param event_args: The event args 40 | :type event_args: blatann.event_args.DisconnectionEventArgs 41 | """ 42 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 43 | 44 | 45 | def main(serial_port): 46 | # Create and open the device 47 | ble_device = BleDevice(serial_port) 48 | ble_device.open() 49 | 50 | # Create and add the battery service to the database 51 | battery_service = battery.add_battery_service(ble_device.database, enable_notifications=True) 52 | battery_service.set_battery_level(100, False) 53 | 54 | # Register listeners for when the client connects and disconnects 55 | ble_device.client.on_connect.register(on_connect) 56 | ble_device.client.on_disconnect.register(on_disconnect) 57 | 58 | # Leaving security and connection parameters as defaults (don't care) 59 | 60 | # Advertise the Battery Service 61 | adv_data = advertising.AdvertisingData(local_name="Battery Test", flags=0x06, 62 | service_uuid16s=battery.BATTERY_SERVICE_UUID) 63 | ble_device.advertiser.set_advertise_data(adv_data) 64 | 65 | logger.info("Advertising") 66 | ble_device.advertiser.start(timeout_sec=0, auto_restart=True) 67 | 68 | battery_level = 100 69 | battery_level_decrement_steps = 1 70 | time_between_steps = 10 71 | 72 | # Decrement the battery level until it runs out 73 | while battery_level >= 0: 74 | time.sleep(time_between_steps) 75 | battery_level -= battery_level_decrement_steps 76 | logger.info("Updating battery level to {}".format(battery_level)) 77 | battery_service.set_battery_level(battery_level) 78 | 79 | logger.info("Done") 80 | ble_device.close() 81 | 82 | 83 | if __name__ == '__main__': 84 | main("COM13") 85 | -------------------------------------------------------------------------------- /blatann/examples/peripheral_current_time_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates using Bluetooth SIG's defined Current Time service as a peripheral. 3 | """ 4 | from __future__ import annotations 5 | 6 | import binascii 7 | import datetime 8 | 9 | from blatann import BleDevice 10 | from blatann.gap import advertising 11 | from blatann.services import current_time 12 | from blatann.utils import setup_logger 13 | from blatann.waitables import GenericWaitable 14 | 15 | logger = setup_logger(level="INFO") 16 | 17 | 18 | def on_connect(peer, event_args): 19 | """ 20 | Event callback for when a central device connects to us 21 | 22 | :param peer: The peer that connected to us 23 | :type peer: blatann.peer.Client 24 | :param event_args: None 25 | """ 26 | if peer: 27 | logger.info("Connected to peer") 28 | else: 29 | logger.warning("Connection timed out") 30 | 31 | 32 | def on_disconnect(peer, event_args): 33 | """ 34 | Event callback for when the client disconnects from us (or when we disconnect from the client) 35 | 36 | :param peer: The peer that disconnected 37 | :type peer: blatann.peer.Client 38 | :param event_args: The event args 39 | :type event_args: blatann.event_args.DisconnectionEventArgs 40 | """ 41 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 42 | 43 | 44 | def on_current_time_write(characteristic, event_args): 45 | """ 46 | Callback registered to be triggered whenever the Current Time characteristic is written to 47 | 48 | :param characteristic: 49 | :param event_args: The write event args 50 | :type event_args: blatann.event_args.DecodedWriteEventArgs 51 | """ 52 | # event_args.value is of type current_time.CurrentTime 53 | logger.info("Current time written to, new value: {}. " 54 | "Raw: {}".format(event_args.value, binascii.hexlify(event_args.raw_value))) 55 | 56 | 57 | def on_local_time_info_write(characteristic, event_args): 58 | """ 59 | Callback registered to be triggered whenever the Local Time Info characteristic is written to 60 | 61 | :param characteristic: 62 | :param event_args: The write event args 63 | :type event_args: blatann.event_args.DecodedWriteEventArgs 64 | """ 65 | # event_args.value is of type current_time.LocalTimeInfo 66 | logger.info("Local Time info written to, new value: {}. " 67 | "Raw: {}".format(event_args.value, binascii.hexlify(event_args.raw_value))) 68 | 69 | 70 | def main(serial_port): 71 | # Create and open the device 72 | ble_device = BleDevice(serial_port) 73 | ble_device.open() 74 | 75 | # Create and add the current time service to the database. 76 | # Tweak the flags below to change how the service is set up 77 | current_time_service = current_time.add_current_time_service(ble_device.database, 78 | enable_writes=True, 79 | enable_local_time_info=True, 80 | enable_reference_info=False) 81 | 82 | # Register handlers for when the characteristics are written to. 83 | # These will only be triggered if enable_writes above is true 84 | current_time_service.on_current_time_write.register(on_current_time_write) 85 | current_time_service.on_local_time_info_write.register(on_local_time_info_write) 86 | 87 | # Demo of the different ways to manually or automatically control the reported time 88 | 89 | # Example 1: Automatically reference system time. 90 | # All logic is handled within the service and reports the time whenever the characteristic is read 91 | # Example 2: Manually report the time 1 day behind using callback method. 92 | # Register a user-defined callback to retrieve the current time to report back to the client 93 | # Example 3: Manually report the time 1 hour ahead by setting the base time 94 | # Set the characteristic's base time to 1 day ahead and allow the service to auto-increment from there 95 | example_mode = 1 96 | 97 | if example_mode == 1: 98 | # configure_automatic() also sets up the Local Time characteristic (if enabled) 99 | # to just set the automatic time and leave Local Time unconfigured, use set_time() with no parameters 100 | current_time_service.configure_automatic() 101 | elif example_mode == 2: 102 | def on_time_read(): 103 | d = datetime.datetime.now() - datetime.timedelta(days=1) 104 | logger.info("Getting time: {}".format(d)) 105 | return d 106 | current_time_service.set_time(characteristic_read_callback=on_time_read) 107 | elif example_mode == 3: 108 | base_time = datetime.datetime.now() + datetime.timedelta(hours=1) 109 | current_time_service.set_time(base_time) 110 | 111 | # Register listeners for when the client connects and disconnects 112 | ble_device.client.on_connect.register(on_connect) 113 | ble_device.client.on_disconnect.register(on_disconnect) 114 | 115 | # Advertise the Current Time service 116 | adv_data = advertising.AdvertisingData(local_name="Current Time", flags=0x06, 117 | service_uuid16s=current_time.CURRENT_TIME_SERVICE_UUID) 118 | ble_device.advertiser.set_advertise_data(adv_data) 119 | 120 | logger.info("Advertising") 121 | ble_device.advertiser.start(timeout_sec=0, auto_restart=True) 122 | 123 | # Create a waitable that waits 5 minutes, then exits 124 | w = GenericWaitable() 125 | w.wait(5 * 60, exception_on_timeout=False) 126 | 127 | logger.info("Done") 128 | ble_device.close() 129 | 130 | 131 | if __name__ == '__main__': 132 | main("COM13") 133 | -------------------------------------------------------------------------------- /blatann/examples/peripheral_descriptors.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to add descriptors to a characteristic in a GATT Database. 3 | 4 | This can be used with the central_descriptor example running on a separate nordic device or 5 | can be run with the nRF Connect app 6 | """ 7 | from __future__ import annotations 8 | 9 | import time 10 | 11 | from blatann import BleDevice 12 | from blatann.bt_sig.assigned_numbers import Format, Namespace, NamespaceDescriptor, Units 13 | from blatann.bt_sig.uuids import DescriptorUuid 14 | from blatann.examples import constants, example_utils 15 | from blatann.gap import advertising, smp 16 | from blatann.gatt import PresentationFormat, gatts 17 | from blatann.gatt.gatts_attribute import GattsAttributeProperties 18 | from blatann.services.ble_data_types import Uint32 19 | from blatann.waitables import GenericWaitable 20 | 21 | logger = example_utils.setup_logger(level="INFO") 22 | 23 | 24 | def on_connect(peer, event_args): 25 | """ 26 | Event callback for when a central device connects to us 27 | 28 | :param peer: The peer that connected to us 29 | :type peer: blatann.peer.Client 30 | :param event_args: None 31 | """ 32 | if peer: 33 | logger.info("Connected to peer") 34 | else: 35 | logger.warning("Connection timed out") 36 | 37 | 38 | def on_disconnect(peer, event_args): 39 | """ 40 | Event callback for when the client disconnects from us (or when we disconnect from the client) 41 | 42 | :param peer: The peer that disconnected 43 | :type peer: blatann.peer.Client 44 | :param event_args: The event args 45 | :type event_args: blatann.event_args.DisconnectionEventArgs 46 | """ 47 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 48 | 49 | 50 | def on_read(characteristic, event_args): 51 | """ 52 | On read for the Time characteristic. Updates the characteristic with the current 53 | UTC time as a 32-bit number 54 | """ 55 | t = int(time.time()) 56 | characteristic.set_value(Uint32.encode(t)) 57 | 58 | 59 | def main(serial_port): 60 | # Create and open the device 61 | ble_device = BleDevice(serial_port) 62 | ble_device.configure() 63 | ble_device.open() 64 | 65 | # Create the service 66 | service = ble_device.database.add_service(constants.DESC_EXAMPLE_SERVICE_UUID) 67 | 68 | # Create a characteristic and add some descriptors 69 | # NOTE: Some descriptors MUST be added during creation: SCCD, User Description, and Presentation Format 70 | # CCCD is added automatically based on the characteristic's Notify and Indicate properties 71 | 72 | # Define the User Description properties and make it writable 73 | # The simplest use-case in which the user description is read-only can provide just the first parameter 74 | user_desc_props = gatts.GattsUserDescriptionProperties("UTC Time", write=True, security_level=smp.SecurityLevel.OPEN, 75 | max_length=20, variable_length=True) 76 | # Define the presentation format. Returning the time in seconds so set exponent to 0 77 | presentation_format = PresentationFormat(fmt=Format.uint32, exponent=0, unit=Units.time_second) 78 | # Create the characteristic properties, including the SCCD, User Description, and Presentation Format 79 | char_props = gatts.GattsCharacteristicProperties(read=True, write=False, notify=True, max_length=Uint32.byte_count, variable_length=False, 80 | sccd=True, user_description=user_desc_props, presentation_format=presentation_format) 81 | char = service.add_characteristic(constants.DESC_EXAMPLE_CHAR_UUID, char_props, Uint32.encode(0)) 82 | char.on_read.register(on_read) 83 | 84 | # Add another descriptor to the list 85 | char_range_value = Uint32.encode(5555) + Uint32.encode(2**32-1000) 86 | desc_props = GattsAttributeProperties(read=True, write=False, variable_length=False, max_length=len(char_range_value)) 87 | char.add_descriptor(DescriptorUuid.valid_range, desc_props, char_range_value) 88 | 89 | # Initialize the advertising and scan response data 90 | adv_data = advertising.AdvertisingData(local_name=constants.PERIPHERAL_NAME, flags=0x06) 91 | scan_data = advertising.AdvertisingData(service_uuid128s=constants.DESC_EXAMPLE_SERVICE_UUID, has_more_uuid128_services=False) 92 | ble_device.advertiser.set_advertise_data(adv_data, scan_data) 93 | 94 | # Start advertising 95 | logger.info("Advertising") 96 | ble_device.client.on_connect.register(on_connect) 97 | ble_device.client.on_disconnect.register(on_disconnect) 98 | ble_device.advertiser.start(timeout_sec=0, auto_restart=True) 99 | 100 | # Create a waitable that will never fire, and wait for some time 101 | w = GenericWaitable() 102 | w.wait(60*30, exception_on_timeout=False) # Keep device active for 30 mins 103 | 104 | logger.info("Done") 105 | ble_device.close() 106 | 107 | 108 | if __name__ == '__main__': 109 | main("COM13") 110 | -------------------------------------------------------------------------------- /blatann/examples/peripheral_device_info_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to implement a Device Info Service on a peripheral. 3 | 4 | This example can be used alongside the central_device_info_service example running on another nordic device, 5 | or using the Nordic nRF Connect app to connect and browse the peripheral's service data 6 | """ 7 | from __future__ import annotations 8 | 9 | from blatann import BleDevice 10 | from blatann.examples import example_utils 11 | from blatann.gap import advertising 12 | from blatann.services import device_info 13 | from blatann.waitables import GenericWaitable 14 | 15 | logger = example_utils.setup_logger(level="DEBUG") 16 | 17 | 18 | def on_connect(peer, event_args): 19 | """ 20 | Event callback for when a central device connects to us 21 | 22 | :param peer: The peer that connected to us 23 | :type peer: blatann.peer.Client 24 | :param event_args: None 25 | """ 26 | if peer: 27 | logger.info("Connected to peer") 28 | else: 29 | logger.warning("Connection timed out") 30 | 31 | 32 | def on_disconnect(peer, event_args): 33 | """ 34 | Event callback for when the client disconnects from us (or when we disconnect from the client) 35 | 36 | :param peer: The peer that disconnected 37 | :type peer: blatann.peer.Client 38 | :param event_args: The event args 39 | :type event_args: blatann.event_args.DisconnectionEventArgs 40 | """ 41 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 42 | 43 | 44 | def main(serial_port): 45 | # Create and open te device 46 | ble_device = BleDevice(serial_port) 47 | ble_device.open() 48 | 49 | # Create and add the Device Info Service to the database 50 | dis = device_info.add_device_info_service(ble_device.database) 51 | 52 | # Set some characteristics in the DIS. The service only contains characteristics which 53 | # have values set. The other ones are not present 54 | dis.set_software_revision("14.2.1") 55 | dis.set_hardware_revision("A") 56 | dis.set_firmware_revision("1.0.4") 57 | dis.set_serial_number("AB1234") 58 | pnp_id = device_info.PnpId(device_info.PnpVendorSource.bluetooth_sig, 0x0058, 0x0002, 0x0013) 59 | dis.set_pnp_id(pnp_id) 60 | 61 | # Initiate the advertising data. Advertise the name and DIS service UUID 62 | name = "Peripheral DIS" 63 | adv_data = advertising.AdvertisingData(local_name=name, service_uuid16s=device_info.DIS_SERVICE_UUID) 64 | ble_device.advertiser.set_advertise_data(adv_data) 65 | 66 | # Start advertising 67 | logger.info("Advertising") 68 | ble_device.client.on_connect.register(on_connect) 69 | ble_device.client.on_disconnect.register(on_disconnect) 70 | ble_device.advertiser.start(timeout_sec=0, auto_restart=True) 71 | 72 | # Create a waitable that will never fire, and wait for some time 73 | w = GenericWaitable() 74 | w.wait(60*30, exception_on_timeout=False) # Keep device active for 30 mins 75 | 76 | # Cleanup 77 | logger.info("Done") 78 | ble_device.close() 79 | 80 | 81 | if __name__ == '__main__': 82 | main("COM13") 83 | -------------------------------------------------------------------------------- /blatann/examples/peripheral_rssi.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a simple example which demonstrates enabling RSSI updates for active connections. 3 | """ 4 | from __future__ import annotations 5 | 6 | import blatann.peer 7 | from blatann import BleDevice 8 | from blatann.examples import constants, example_utils 9 | from blatann.gap import AdvertisingData 10 | from blatann.waitables import GenericWaitable 11 | 12 | logger = example_utils.setup_logger(level="INFO") 13 | 14 | 15 | def on_rssi_changed(peer, rssi: int): 16 | """ 17 | Event callback for when the RSSI with the central device changes by 18 | the configured dBm threshold 19 | 20 | :param peer: The peer object 21 | :param rssi: The new RSSI for the connection 22 | """ 23 | logger.info(f"RSSI changed to {rssi}dBm") 24 | 25 | 26 | def on_connect(peer, event_args): 27 | """ 28 | Event callback for when a central device connects to us 29 | 30 | :param peer: The peer that connected to us 31 | :type peer: blatann.peer.Client 32 | :param event_args: None 33 | """ 34 | if peer: 35 | logger.info("Starting RSSI reporting") 36 | # Start reporting RSSI. The RSSI Changed event will be triggered if the dBm delta is >= 5 37 | # and is sustained for at least 3 RSSI samples. Modify these values to your liking. 38 | # Setting threshold_dbm=None will disable the on_rssi_changed event and RSSI can be polled using peer.rssi 39 | peer.start_rssi_reporting(threshold_dbm=5, skip_count=3) 40 | 41 | 42 | def on_disconnect(peer, event_args): 43 | """ 44 | Event callback for when the client disconnects from us (or when we disconnect from the client) 45 | 46 | :param peer: The peer that disconnected 47 | :type peer: blatann.peer.Client 48 | :param event_args: The event args 49 | :type event_args: blatann.event_args.DisconnectionEventArgs 50 | """ 51 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 52 | 53 | 54 | def main(serial_port): 55 | # Create and open the device 56 | ble_device = BleDevice(serial_port) 57 | ble_device.configure() 58 | ble_device.open() 59 | 60 | ble_device.generic_access_service.device_name = "RSSI Example" 61 | 62 | # Setup event callbacks 63 | ble_device.client.on_connect.register(on_connect) 64 | ble_device.client.on_disconnect.register(on_disconnect) 65 | ble_device.client.on_rssi_changed.register(on_rssi_changed) 66 | 67 | adv_data = AdvertisingData(local_name="RSSI Test", flags=0x06) 68 | ble_device.advertiser.set_advertise_data(adv_data) 69 | 70 | logger.info("Advertising") 71 | ble_device.advertiser.start(timeout_sec=0, auto_restart=True) 72 | 73 | # Create a waitable that will never fire, and wait for some time 74 | w = GenericWaitable() 75 | w.wait(60*10, exception_on_timeout=False) # Keep device active for 10 mins 76 | 77 | logger.info("Done") 78 | ble_device.close() 79 | 80 | 81 | if __name__ == '__main__': 82 | main("COM3") 83 | -------------------------------------------------------------------------------- /blatann/examples/peripheral_uart_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example implements Nordic's custom UART service and demonstrates how to configure the MTU size. 3 | It is configured to use an MTU size based on the Data Length Extensions feature of BLE for maximum throughput. 4 | This is compatible with the nRF Connect app (Android version tested) and the central_uart_service example. 5 | 6 | This is a simple example which just echos back any data that the client sends to it. 7 | """ 8 | from __future__ import annotations 9 | 10 | from blatann import BleDevice 11 | from blatann.gap import advertising 12 | from blatann.gatt import MTU_SIZE_FOR_MAX_DLE 13 | from blatann.services import nordic_uart 14 | from blatann.utils import setup_logger 15 | from blatann.waitables import GenericWaitable 16 | 17 | logger = setup_logger(level="DEBUG") 18 | 19 | 20 | def on_connect(peer, event_args): 21 | """ 22 | Event callback for when a central device connects to us 23 | 24 | :param peer: The peer that connected to us 25 | :type peer: blatann.peer.Client 26 | :param event_args: None 27 | """ 28 | if peer: 29 | logger.info("Connected to peer, initiating MTU exchange") 30 | peer.exchange_mtu() 31 | else: 32 | logger.warning("Connection timed out") 33 | 34 | 35 | def on_disconnect(peer, event_args): 36 | """ 37 | Event callback for when the client disconnects from us (or when we disconnect from the client) 38 | 39 | :param peer: The peer that disconnected 40 | :type peer: blatann.peer.Client 41 | :param event_args: The event args 42 | :type event_args: blatann.event_args.DisconnectionEventArgs 43 | """ 44 | logger.info("Disconnected from peer, reason: {}".format(event_args.reason)) 45 | 46 | 47 | def on_mtu_size_update(peer, event_args): 48 | """ 49 | Callback for when the peer's MTU size has been updated/negotiated 50 | 51 | :param peer: The peer the MTU was updated on 52 | :type peer: blatann.peer.Client 53 | :type event_args: blatann.event_args.MtuSizeUpdatedEventArgs 54 | """ 55 | logger.info("MTU size updated from {} to {}".format(event_args.previous_mtu_size, event_args.current_mtu_size)) 56 | # Request that the connection parameters be re-negotiated using our preferred parameters 57 | peer.update_connection_parameters() 58 | 59 | 60 | def on_data_rx(service, data): 61 | """ 62 | Called whenever data is received on the RX line of the Nordic UART Service 63 | 64 | :param service: the service the data was received from 65 | :type service: nordic_uart.service.NordicUartServer 66 | :param data: The data that was received 67 | :type data: bytes 68 | """ 69 | logger.info("Received data (len {}): '{}'".format(len(data), data)) 70 | logger.info("Echoing data back to client") 71 | # Echo it back to the client 72 | service.write(data) 73 | 74 | 75 | def on_tx_complete(service, event_args): 76 | logger.info("Write Complete") 77 | 78 | 79 | def main(serial_port): 80 | ble_device = BleDevice(serial_port) 81 | # Configure the BLE device to support MTU sizes which allow the max data length extension PDU size 82 | # Note this isn't 100% necessary as the default configuration sets the max to this value also 83 | ble_device.configure(att_mtu_max_size=MTU_SIZE_FOR_MAX_DLE) 84 | ble_device.open() 85 | 86 | # Create and add the Nordic UART service 87 | nus = nordic_uart.add_nordic_uart_service(ble_device.database) 88 | nus.on_data_received.register(on_data_rx) 89 | nus.on_write_complete.register(on_tx_complete) 90 | 91 | # Register listeners for when the client connects and disconnects 92 | ble_device.client.on_connect.register(on_connect) 93 | ble_device.client.on_disconnect.register(on_disconnect) 94 | ble_device.client.on_mtu_size_updated.register(on_mtu_size_update) 95 | 96 | # Configure the client to prefer the max MTU size 97 | ble_device.client.preferred_mtu_size = ble_device.max_mtu_size 98 | ble_device.client.set_connection_parameters(7.5, 15, 4000) 99 | 100 | # Advertise the service UUID 101 | adv_data = advertising.AdvertisingData(flags=0x06, local_name="Nordic UART Server") 102 | scan_data = advertising.AdvertisingData(service_uuid128s=nordic_uart.NORDIC_UART_SERVICE_UUID) 103 | 104 | ble_device.advertiser.set_advertise_data(adv_data, scan_data) 105 | 106 | logger.info("Advertising") 107 | 108 | ble_device.advertiser.start(timeout_sec=0, auto_restart=True) 109 | 110 | # Create a waitable that waits 5 minutes then exits 111 | w = GenericWaitable() 112 | try: 113 | w.wait(5 * 60, exception_on_timeout=False) 114 | except KeyboardInterrupt: 115 | pass 116 | finally: 117 | ble_device.close() 118 | 119 | 120 | if __name__ == '__main__': 121 | main("COM7") 122 | -------------------------------------------------------------------------------- /blatann/examples/scanner.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example simply demonstrates scanning for peripheral devices 3 | """ 4 | from __future__ import annotations 5 | 6 | from blatann import BleDevice 7 | from blatann.examples import example_utils 8 | 9 | logger = example_utils.setup_logger(level="INFO") 10 | 11 | 12 | def main(serial_port): 13 | # Create and open the device 14 | ble_device = BleDevice(serial_port) 15 | ble_device.open() 16 | 17 | logger.info("Scanning...") 18 | # Set scanning for 4 seconds 19 | ble_device.scanner.set_default_scan_params(timeout_seconds=4) 20 | 21 | # Start scanning and iterate through the reports as they're received 22 | for report in ble_device.scanner.start_scan().scan_reports: 23 | if not report.duplicate: 24 | logger.info(report) 25 | 26 | scan_report = ble_device.scanner.scan_report 27 | print("\n") 28 | logger.info("Finished scanning. Scan reports by peer address:") 29 | # Iterate through all the peers found and print out the reports 30 | for report in scan_report.advertising_peers_found: 31 | logger.info(report) 32 | 33 | # Clean up and close the device 34 | ble_device.close() 35 | 36 | 37 | if __name__ == '__main__': 38 | main("COM4") 39 | -------------------------------------------------------------------------------- /blatann/examples/scanner_async.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example simply demonstrates scanning for peripheral devices using async methods 3 | """ 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | 8 | from blatann import BleDevice 9 | from blatann.examples import example_utils 10 | 11 | logger = example_utils.setup_logger(level="INFO") 12 | 13 | 14 | async def _main(ble_device: BleDevice): 15 | # Open the device 16 | ble_device.open() 17 | 18 | logger.info("Scanning...") 19 | # Set scanning for 4 seconds 20 | ble_device.scanner.set_default_scan_params(timeout_seconds=4) 21 | 22 | # Start scanning and iterate through the reports as they're received, async to not block the main thread 23 | async for report in ble_device.scanner.start_scan().scan_reports_async: 24 | if not report.duplicate: 25 | logger.info(report) 26 | 27 | scan_report = ble_device.scanner.scan_report 28 | print("\n") 29 | logger.info("Finished scanning. Scan reports by peer address:") 30 | # Iterate through all the peers found and print out the reports 31 | for report in scan_report.advertising_peers_found: 32 | logger.info(report) 33 | 34 | # Clean up and close the device 35 | ble_device.close() 36 | 37 | 38 | def main(serial_port): 39 | ble_device = BleDevice(serial_port) 40 | try: 41 | asyncio.run(_main(ble_device)) 42 | except KeyboardInterrupt: 43 | pass 44 | finally: 45 | # Ensure the ble device is closed on exit 46 | ble_device.close() 47 | 48 | 49 | if __name__ == '__main__': 50 | main("COM4") 51 | -------------------------------------------------------------------------------- /blatann/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class BlatannException(Exception): 5 | pass 6 | 7 | 8 | class InvalidStateException(BlatannException): 9 | pass 10 | 11 | 12 | class InvalidOperationException(BlatannException): 13 | pass 14 | 15 | 16 | class TimeoutError(BlatannException): 17 | pass 18 | 19 | 20 | class DecodeError(BlatannException): 21 | pass 22 | -------------------------------------------------------------------------------- /blatann/gap/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.gap.advertising import AdvertisingData, AdvertisingFlags 4 | from blatann.gap.scanning import ScanParameters 5 | from blatann.gap.smp_types import ( 6 | AuthenticationKeyType, IoCapabilities, PairingPolicy, SecurityLevel, SecurityParameters, SecurityStatus 7 | ) 8 | from blatann.nrf import nrf_events, nrf_types 9 | 10 | HciStatus = nrf_types.BLEHci 11 | 12 | """ 13 | The default link-layer packet size used when a connection is established 14 | """ 15 | DLE_SIZE_DEFAULT = 27 16 | 17 | """ 18 | The minimum allowed link-layer packet size 19 | """ 20 | DLE_SIZE_MINIMUM = 27 21 | 22 | """ 23 | The maximum allowed link-layer packet size 24 | """ 25 | DLE_SIZE_MAXIMUM = 251 26 | -------------------------------------------------------------------------------- /blatann/gap/gap_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | 5 | from blatann.nrf import nrf_types 6 | 7 | 8 | class Phy(enum.IntFlag): 9 | """ 10 | The supported PHYs 11 | 12 | .. note:: Coded PHY is currently not supported (hardware limitation) 13 | """ 14 | auto = int(nrf_types.BLEGapPhy.auto) #: Automatically select the PHY based on what's supported 15 | one_mbps = int(nrf_types.BLEGapPhy.one_mbps) #: 1 Mbps PHY 16 | two_mbps = int(nrf_types.BLEGapPhy.two_mbps) #: 2 Mbps PHY 17 | # NOT SUPPORTED coded = int(nrf_events.BLEGapPhy.coded) 18 | 19 | 20 | class PeerAddress(nrf_types.BLEGapAddr): 21 | pass 22 | 23 | 24 | class ConnectionParameters(nrf_types.BLEGapConnParams): 25 | """ 26 | Represents the connection parameters that are sent during negotiation. This includes 27 | the preferred min/max interval range, timeout, and slave latency 28 | """ 29 | def __init__(self, min_conn_interval_ms, max_conn_interval_ms, timeout_ms, slave_latency=0): 30 | super(ConnectionParameters, self).__init__(min_conn_interval_ms, max_conn_interval_ms, timeout_ms, slave_latency) 31 | self.validate() 32 | 33 | def __str__(self): 34 | return (f"ConnectionParams([{self.min_conn_interval_ms}-{self.max_conn_interval_ms}] ms, " 35 | f"timeout: {self.conn_sup_timeout_ms} ms, latency: {self.slave_latency}") 36 | 37 | def __repr__(self): 38 | return str(self) 39 | 40 | 41 | class ActiveConnectionParameters: 42 | """ 43 | Represents the connection parameters that are currently in use with a peer device. 44 | This is similar to ConnectionParameters with the sole difference being 45 | the connection interval is not a min/max range but a single number 46 | """ 47 | def __init__(self, conn_params: ConnectionParameters): 48 | self._interval_ms = conn_params.min_conn_interval_ms 49 | self._timeout_ms = conn_params.conn_sup_timeout_ms 50 | self._slave_latency = conn_params.slave_latency 51 | 52 | def __repr__(self): 53 | return str(self) 54 | 55 | def __str__(self): 56 | return f"ConnectionParams({self._interval_ms}ms/{self._slave_latency}/{self._timeout_ms}ms)" 57 | 58 | def __eq__(self, other): 59 | if not isinstance(other, ActiveConnectionParameters): 60 | return False 61 | return (self._interval_ms == other._interval_ms and 62 | self._slave_latency == other._slave_latency and 63 | self._timeout_ms == other._timeout_ms) 64 | 65 | @property 66 | def interval_ms(self) -> float: 67 | """ 68 | **Read Only** 69 | 70 | The connection interval, in milliseconds 71 | """ 72 | return self._interval_ms 73 | 74 | @property 75 | def timeout_ms(self) -> float: 76 | """ 77 | **Read Only** 78 | 79 | The connection timeout, in milliseconds 80 | """ 81 | return self._timeout_ms 82 | 83 | @property 84 | def slave_latency(self) -> int: 85 | """ 86 | **Read Only** 87 | 88 | The slave latency (the number of connection intervals the slave is allowed to skip before being 89 | required to respond) 90 | """ 91 | return self._slave_latency 92 | -------------------------------------------------------------------------------- /blatann/gap/generic_access_service.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import Optional, Union 5 | 6 | from blatann.bt_sig.assigned_numbers import Appearance 7 | from blatann.nrf import nrf_types 8 | from blatann.peer import ConnectionParameters 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class GenericAccessService: 14 | """ 15 | Class which represents the Generic Access service within the local database 16 | """ 17 | DEVICE_NAME_MAX_LENGTH = 31 18 | 19 | def __init__(self, ble_driver, 20 | device_name=nrf_types.BLE_GAP_DEVNAME_DEFAULT, 21 | appearance=Appearance.unknown): 22 | """ 23 | :type ble_driver: blatann.nrf.nrf_driver.NrfDriver 24 | :type device_name: str 25 | """ 26 | self.ble_driver = ble_driver 27 | self._name = device_name 28 | self._appearance = appearance 29 | self._empty_conn_params = None 30 | self._preferred_conn_params: Optional[ConnectionParameters] = None 31 | 32 | @property 33 | def device_name(self) -> str: 34 | """ 35 | The device name that is configured in the Generic Access service of the local GATT database 36 | 37 | :getter: Gets the current device name 38 | :setter: Sets the current device name. Length (after utf8 encoding) must be <= 31 bytes 39 | """ 40 | return self._name 41 | 42 | @device_name.setter 43 | def device_name(self, name: str): 44 | name_bytes = name.encode("utf8") 45 | if len(name_bytes) > self.DEVICE_NAME_MAX_LENGTH: 46 | raise ValueError(f"Encoded device name must be <= {self.DEVICE_NAME_MAX_LENGTH} bytes") 47 | if self.ble_driver.is_open: 48 | self.ble_driver.ble_gap_device_name_set(name_bytes) 49 | self._name = name 50 | 51 | @property 52 | def appearance(self) -> Appearance: 53 | """ 54 | The Appearance that is configured in the Generic Access service of the local GATT database 55 | 56 | :getter: Gets the device appearance 57 | :setter: Sets the device appearance 58 | """ 59 | return self._appearance 60 | 61 | @appearance.setter 62 | def appearance(self, value: Union[Appearance, int]): 63 | if self.ble_driver.is_open: 64 | self.ble_driver.ble_gap_appearance_set(value) 65 | self._appearance = value 66 | 67 | @property 68 | def preferred_peripheral_connection_params(self) -> Optional[ConnectionParameters]: 69 | """ 70 | The preferred peripheral connection parameters that are configured in the Generic Access service 71 | of the local GATT Database. If not configured, returns None. 72 | 73 | :getter: Gets the configured connection parameters or None if not configured 74 | :setter: Sets the configured connection parameters 75 | """ 76 | return self._preferred_conn_params 77 | 78 | @preferred_peripheral_connection_params.setter 79 | def preferred_peripheral_connection_params(self, value: ConnectionParameters): 80 | if self.ble_driver.is_open: 81 | self.ble_driver.ble_gap_ppcp_set(value) 82 | self._preferred_conn_params = value 83 | 84 | def update(self): 85 | """ 86 | **Not to be called by users** 87 | 88 | Used internally to configure the generic access in the case that values were set before 89 | the driver was opened and configured. 90 | """ 91 | if not self.ble_driver.is_open: 92 | return 93 | name_bytes = self._name.encode("utf8") 94 | self.ble_driver.ble_gap_device_name_set(name_bytes) 95 | self.ble_driver.ble_gap_appearance_set(self._appearance) 96 | if self._preferred_conn_params: 97 | self.ble_driver.ble_gap_ppcp_set(self._preferred_conn_params) 98 | -------------------------------------------------------------------------------- /blatann/gap/smp_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | from typing import Union 5 | 6 | from blatann.nrf import nrf_types 7 | from blatann.utils import repr_format 8 | 9 | # Enum used to report IO Capabilities for the pairing process 10 | IoCapabilities = nrf_types.BLEGapIoCaps 11 | 12 | # Enum of the status codes emitted during security procedures 13 | SecurityStatus = nrf_types.BLEGapSecStatus 14 | 15 | # Enum of the different Pairing passkeys to be entered by the user (passcode, out-of-band, etc.) 16 | AuthenticationKeyType = nrf_types.BLEGapAuthKeyType 17 | 18 | 19 | class SecurityLevel(enum.Enum): 20 | """ 21 | Security levels used for defining GATT server characteristics 22 | """ 23 | NO_ACCESS = 0 24 | OPEN = 1 25 | JUST_WORKS = 2 26 | MITM = 3 27 | LESC_MITM = 4 28 | 29 | 30 | # TODO: Figure out the best way to document enum values 31 | class PairingPolicy(enum.IntFlag): 32 | allow_all = 0 33 | # allow_all.__doc__ = "Allows all pairing requests to be initiated" 34 | 35 | reject_new_pairing_requests = enum.auto() 36 | # reject_new_pairing_requests.__doc__ = "Rejects all pairing requests from non-bonded devices" 37 | 38 | reject_nonbonded_peripheral_requests = enum.auto() 39 | # reject_nonbonded_peripheral_requests.__doc__ = "Rejects peripheral-initiated security requests from non-bonded devices" 40 | 41 | reject_bonded_peripheral_requests = enum.auto() 42 | # reject_bonded_peripheral_requests.__doc__ = "Rejects peripheral-initiated security requests from bonded devices. " \ 43 | # "Used for cases where the central wants to control when security is enabled." 44 | 45 | reject_bonded_device_repairing_requests = enum.auto() 46 | # reject_bonded_device_repairing_requests.__doc__ = "Rejects re-pairing attempts from a central that is already bonded. " \ 47 | # "Requires explicit bond data deletion in order to pair again." 48 | 49 | # Composites 50 | reject_peripheral_requests = reject_bonded_peripheral_requests | reject_nonbonded_peripheral_requests 51 | # reject_peripheral_requests.__doc__ = "Rejects all peripheral-initiated security requests" 52 | 53 | reject_all_requests = reject_new_pairing_requests | reject_peripheral_requests | reject_bonded_device_repairing_requests 54 | # reject_all_requests.__doc__ = "Rejects all security requests, except from already-bonded central devices" 55 | 56 | @staticmethod 57 | def combine(*policies: PairingPolicy): 58 | policy = 0 59 | for p in policies: 60 | policy |= p 61 | return policy 62 | 63 | 64 | class SecurityParameters: 65 | """ 66 | Class representing the desired security parameters for a given connection 67 | """ 68 | def __init__(self, 69 | passcode_pairing=False, 70 | io_capabilities=IoCapabilities.KEYBOARD_DISPLAY, 71 | bond=False, 72 | out_of_band=False, 73 | reject_pairing_requests: Union[bool, PairingPolicy] = False, 74 | lesc_pairing=False): 75 | self.passcode_pairing = passcode_pairing 76 | self.io_capabilities = io_capabilities 77 | self.bond = bond 78 | self.out_of_band = out_of_band 79 | self.lesc_pairing = lesc_pairing 80 | self.reject_pairing_requests = reject_pairing_requests 81 | if not isinstance(reject_pairing_requests, PairingPolicy): 82 | self.reject_pairing_requests = (PairingPolicy.reject_all_requests if reject_pairing_requests else 83 | PairingPolicy.allow_all) 84 | 85 | def __repr__(self): 86 | return repr_format(self, passcode_pairing=self.passcode_pairing, io=self.io_capabilities, 87 | bond=self.bond, oob=self.out_of_band, lesc=self.lesc_pairing) 88 | -------------------------------------------------------------------------------- /blatann/gatt/gattc_attribute.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from blatann.event_args import ReadCompleteEventArgs, WriteCompleteEventArgs 6 | from blatann.event_type import Event, EventSource 7 | from blatann.gatt import Attribute 8 | from blatann.gatt.managers import GattcOperationManager 9 | from blatann.nrf import nrf_types 10 | from blatann.uuid import Uuid 11 | from blatann.waitables.event_waitable import IdBasedEventWaitable 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class GattcAttribute(Attribute): 17 | """ 18 | Represents a client-side interface to a single attribute which lives inside a Characteristic 19 | """ 20 | def __init__(self, uuid: Uuid, handle: int, read_write_manager: GattcOperationManager, 21 | initial_value=b"", string_encoding="utf8"): 22 | super(GattcAttribute, self).__init__(uuid, handle, initial_value, string_encoding) 23 | self._manager = read_write_manager 24 | 25 | self._on_read_complete_event = EventSource(f"[{handle}/{uuid}] On Read Complete", logger) 26 | self._on_write_complete_event = EventSource(f"[{handle}/{uuid}] On Write Complete", logger) 27 | 28 | """ 29 | Events 30 | """ 31 | 32 | @property 33 | def on_read_complete(self) -> Event[GattcAttribute, ReadCompleteEventArgs]: 34 | """ 35 | Event that is triggered when a read from the attribute is completed 36 | """ 37 | return self._on_read_complete_event 38 | 39 | @property 40 | def on_write_complete(self) -> Event[GattcAttribute, WriteCompleteEventArgs]: 41 | """ 42 | Event that is triggered when a write to the attribute is completed 43 | """ 44 | return self._on_write_complete_event 45 | 46 | """ 47 | Public Methods 48 | """ 49 | 50 | def read(self) -> IdBasedEventWaitable[GattcAttribute, ReadCompleteEventArgs]: 51 | """ 52 | Performs a read of the attribute and returns a Waitable that executes when the read finishes 53 | with the data read. 54 | 55 | :return: A waitable that will trigger when the read finishes 56 | """ 57 | read_id = self._manager.read(self._handle, self._read_complete) 58 | return IdBasedEventWaitable(self._on_read_complete_event, read_id) 59 | 60 | def write(self, data, with_response=True) -> IdBasedEventWaitable[GattcAttribute, WriteCompleteEventArgs]: 61 | """ 62 | Initiates a write of the data provided to the attribute and returns a Waitable that executes 63 | when the write completes and the confirmation response is received from the other device. 64 | 65 | :param data: The data to write. Can be a string, bytes, or anything that can be converted to bytes 66 | :type data: str or bytes or bytearray 67 | :param with_response: Used internally for characteristics that support write without responses. 68 | Should always be true for any other case (descriptors, etc.). 69 | :return: A waitable that returns when the write finishes 70 | """ 71 | if isinstance(data, str): 72 | data = data.encode(self._string_encoding) 73 | write_id = self._manager.write(self._handle, bytes(data), self._write_complete, with_response) 74 | return IdBasedEventWaitable(self._on_write_complete_event, write_id) 75 | 76 | def update(self, value): 77 | """ 78 | Used internally to update the value after data is received from another means, i.e. Indication/notification. 79 | Should not be called by the user. 80 | """ 81 | self._value = bytes(value) 82 | 83 | def _read_complete(self, sender, event_args): 84 | if event_args.handle == self._handle: 85 | if event_args.status == nrf_types.BLEGattStatusCode.success: 86 | self._value = event_args.data 87 | args = ReadCompleteEventArgs(event_args.id, self._value, event_args.status, event_args.reason) 88 | self._on_read_complete_event.notify(self, args) 89 | 90 | def _write_complete(self, sender, event_args): 91 | # Success, update the local value 92 | if event_args.handle == self._handle: 93 | if event_args.status == nrf_types.BLEGattStatusCode.success: 94 | self._value = event_args.data 95 | args = WriteCompleteEventArgs(event_args.id, self._value, event_args.status, event_args.reason) 96 | self._on_write_complete_event.notify(self, args) 97 | -------------------------------------------------------------------------------- /blatann/gatt/reader.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from blatann.event_args import EventArgs 6 | from blatann.event_type import Event, EventSource 7 | from blatann.exceptions import InvalidStateException 8 | from blatann.nrf import nrf_events, nrf_types 9 | from blatann.waitables.event_waitable import EventWaitable 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class GattcReadCompleteEventArgs(EventArgs): 15 | def __init__(self, handle, status, data): 16 | self.handle = handle 17 | self.status = status 18 | self.data = data 19 | 20 | 21 | class GattcReader: 22 | """ 23 | Class which implements the state machine for completely reading a peripheral's attribute 24 | """ 25 | _READ_OVERHEAD = 1 # Number of bytes per MTU that are overhead for the read operation 26 | 27 | def __init__(self, ble_device, peer): 28 | """ 29 | :type ble_device: blatann.device.BleDevice 30 | :type peer: blatann.peer.Peer 31 | """ 32 | self.ble_device = ble_device 33 | self.peer = peer 34 | self._on_read_complete_event = EventSource("On Read Complete", logger) 35 | self._busy = False 36 | self._data = bytearray() 37 | self._handle = 0x0000 38 | self._offset = 0 39 | self.peer.driver_event_subscribe(self._on_read_response, nrf_events.GattcEvtReadResponse) 40 | 41 | @property 42 | def on_read_complete(self): 43 | """ 44 | Event that is emitted when a read completes on an attribute handle. 45 | 46 | Handler args: (int attribute_handle, gatt.GattStatusCode, bytes data_read) 47 | 48 | :return: an Event which can have handlers registered to and deregistered from 49 | :rtype: Event 50 | """ 51 | return self._on_read_complete_event 52 | 53 | def read(self, handle): 54 | """ 55 | Reads the attribute value from the handle provided. Can only read from a single attribute at a time. If a 56 | read is in progress, raises an InvalidStateException 57 | 58 | :param handle: the attribute handle to read 59 | :return: A waitable that will fire when the read finishes. 60 | See on_read_complete for the values returned from the waitable 61 | :rtype: EventWaitable 62 | """ 63 | if self._busy: 64 | raise InvalidStateException("Gattc Reader is busy") 65 | self._handle = handle 66 | self._offset = 0 67 | self._data = bytearray() 68 | logger.debug("Starting read from handle {}".format(handle)) 69 | self._read_next_chunk() 70 | self._busy = True 71 | return EventWaitable(self.on_read_complete) 72 | 73 | def _read_next_chunk(self): 74 | self.ble_device.ble_driver.ble_gattc_read(self.peer.conn_handle, self._handle, self._offset) 75 | 76 | def _on_read_response(self, driver, event): 77 | """ 78 | Handler for GattcEvtReadResponse 79 | 80 | :type event: nrf_events.GattcEvtReadResponse 81 | """ 82 | if event.conn_handle != self.peer.conn_handle or event.attr_handle != self._handle: 83 | return 84 | if event.status != nrf_types.BLEGattStatusCode.success: 85 | self._complete(event.status) 86 | return 87 | 88 | bytes_read = len(event.data) 89 | self._data += bytearray(event.data) 90 | self._offset += bytes_read 91 | 92 | if bytes_read == (self.peer.mtu_size - self._READ_OVERHEAD): 93 | self._read_next_chunk() 94 | else: 95 | self._complete() 96 | 97 | def _complete(self, status=nrf_types.BLEGattStatusCode.success): 98 | self._busy = False 99 | event_args = GattcReadCompleteEventArgs(self._handle, status, bytes(self._data)) 100 | self._on_read_complete_event.notify(self, event_args) 101 | -------------------------------------------------------------------------------- /blatann/nrf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/blatann/nrf/__init__.py -------------------------------------------------------------------------------- /blatann/nrf/nrf_dll_load.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pc_ble_driver_py.lib.nrf_ble_driver_sd_api_v5 as driver 4 | -------------------------------------------------------------------------------- /blatann/nrf/nrf_events/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.nrf.nrf_events.gap_events import * 4 | from blatann.nrf.nrf_events.gatt_events import * 5 | from blatann.nrf.nrf_events.generic_events import * 6 | from blatann.nrf.nrf_events.smp_events import * 7 | 8 | from blatann.nrf.nrf_events import ( # isort: skip 9 | gap_events as _gap_events, 10 | gatt_events as _gatt_events, 11 | generic_events as _generic_events, 12 | smp_events as _smp_events 13 | ) 14 | 15 | __all__ = [ # noqa PLE0604 - *_mod.__all__ is rexporting the submodules 16 | *_gap_events.__all__, 17 | *_gatt_events.__all__, 18 | *_generic_events.__all__, 19 | *_smp_events.__all__, 20 | "event_decode", 21 | ] 22 | 23 | _event_classes = [ 24 | EvtUserMemoryRequest, 25 | 26 | # Gap 27 | GapEvtConnected, 28 | GapEvtDisconnected, 29 | GapEvtConnParamUpdate, 30 | GapEvtRssiChanged, 31 | GapEvtAdvReport, 32 | GapEvtTimeout, 33 | GapEvtConnParamUpdateRequest, 34 | GapEvtDataLengthUpdate, 35 | GapEvtDataLengthUpdateRequest, 36 | GapEvtPhyUpdate, 37 | GapEvtPhyUpdateRequest, 38 | 39 | # SMP 40 | GapEvtSecParamsRequest, 41 | GapEvtAuthKeyRequest, 42 | GapEvtConnSecUpdate, 43 | GapEvtAuthStatus, 44 | GapEvtPasskeyDisplay, 45 | GapEvtSecInfoRequest, 46 | GapEvtLescDhKeyRequest, 47 | GapEvtSecRequest, 48 | 49 | # Gattc 50 | GattcEvtPrimaryServiceDiscoveryResponse, 51 | GattcEvtCharacteristicDiscoveryResponse, 52 | GattcEvtDescriptorDiscoveryResponse, 53 | GattcEvtReadResponse, 54 | GattcEvtWriteResponse, 55 | GattcEvtHvx, 56 | GattcEvtAttrInfoDiscoveryResponse, 57 | GattcEvtMtuExchangeResponse, 58 | GattcEvtTimeout, 59 | GattcEvtWriteCmdTxComplete, 60 | # TODO: 61 | # driver.BLE_GATTC_EVT_REL_DISC_RSP 62 | # driver.BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP 63 | # driver.BLE_GATTC_EVT_CHAR_VALS_READ_RSP 64 | 65 | # Gatts 66 | GattsEvtWrite, 67 | GattsEvtReadWriteAuthorizeRequest, 68 | GattsEvtHandleValueConfirm, 69 | GattsEvtExchangeMtuRequest, 70 | GattsEvtNotificationTxComplete, 71 | GattsEvtTimeout, 72 | GattsEvtSysAttrMissing 73 | # TODO: 74 | # driver.BLE_GATTS_EVT_SC_CONFIRM 75 | ] 76 | 77 | _events_by_id = {e.evt_id: e for e in _event_classes} 78 | 79 | 80 | def event_decode(ble_event): 81 | event_cls = _events_by_id.get(ble_event.header.evt_id, None) 82 | if event_cls: 83 | return event_cls.from_c(ble_event) 84 | return None 85 | -------------------------------------------------------------------------------- /blatann/nrf/nrf_events/generic_events.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.nrf.nrf_dll_load import driver 4 | from blatann.utils import repr_format 5 | 6 | __all__ = [ 7 | "BLEEvent", 8 | "EvtUserMemoryRequest", 9 | ] 10 | 11 | 12 | class BLEEvent: 13 | evt_id = None 14 | 15 | def __init__(self, conn_handle): 16 | self.conn_handle = conn_handle 17 | 18 | def __str__(self): 19 | return self.__repr__() 20 | 21 | def _repr_format(self, **kwargs): 22 | """ 23 | Helper method to format __repr__ for BLE events 24 | """ 25 | return repr_format(self, conn_handle=self.conn_handle, **kwargs) 26 | 27 | 28 | class EvtUserMemoryRequest(BLEEvent): 29 | evt_id = driver.BLE_EVT_USER_MEM_REQUEST 30 | 31 | def __init__(self, conn_handle, request_type): 32 | super(EvtUserMemoryRequest, self).__init__(conn_handle) 33 | self.type = request_type 34 | 35 | @classmethod 36 | def from_c(cls, event): 37 | return cls(event.evt.common_evt.conn_handle, event.evt.common_evt.params.user_mem_request.type) 38 | 39 | def __repr__(self): 40 | return self._repr_format(type=self.type) 41 | -------------------------------------------------------------------------------- /blatann/nrf/nrf_types/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pc_ble_driver_py.exceptions import NordicSemiException 4 | 5 | from blatann.nrf.nrf_types.config import * 6 | from blatann.nrf.nrf_types.enums import * 7 | from blatann.nrf.nrf_types.gap import * 8 | from blatann.nrf.nrf_types.gatt import * 9 | from blatann.nrf.nrf_types.generic import * 10 | from blatann.nrf.nrf_types.smp import * 11 | 12 | from blatann.nrf.nrf_types import ( # isort: skip 13 | config as _config, 14 | enums as _enums, 15 | gap as _gap, 16 | gatt as _gatt, 17 | generic as _generic, 18 | smp as _smp 19 | ) 20 | 21 | __all__ = [ # noqa PLE0604 - *_mod.__all__ is rexporting the submodules 22 | "NordicSemiException", 23 | *_config.__all__, 24 | *_enums.__all__, 25 | *_gap.__all__, 26 | *_gatt.__all__, 27 | *_generic.__all__, 28 | *_smp.__all__, 29 | ] 30 | -------------------------------------------------------------------------------- /blatann/nrf/nrf_types/generic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from enum import Enum 5 | 6 | import blatann.nrf.nrf_driver_types as util 7 | from blatann.nrf.nrf_dll_load import driver 8 | 9 | __all__ = [ 10 | "BLE_CONN_HANDLE_INVALID", 11 | "BLEUUID", 12 | "BLEUUIDBase", 13 | ] 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | BLE_CONN_HANDLE_INVALID = driver.BLE_CONN_HANDLE_INVALID 18 | 19 | NoneType = type(None) 20 | 21 | 22 | class BLEUUIDBase: 23 | BLE_UUID_TYPE_BLE = driver.BLE_UUID_TYPE_BLE 24 | 25 | def __init__(self, vs_uuid_base=None, uuid_type=None): 26 | assert isinstance(vs_uuid_base, (list, NoneType)), 'Invalid argument type' 27 | assert isinstance(uuid_type, (int, NoneType)), 'Invalid argument type' 28 | if vs_uuid_base is None: 29 | self.base = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 30 | 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB] 31 | self.def_base = True 32 | else: 33 | self.base = vs_uuid_base 34 | self.def_base = False 35 | 36 | if uuid_type is None and self.def_base: 37 | self.type = driver.BLE_UUID_TYPE_BLE 38 | else: 39 | self.type = uuid_type if uuid_type is not None else 0 40 | 41 | def __eq__(self, other): 42 | if not isinstance(other, BLEUUIDBase): 43 | return False 44 | if self.base != other.base: 45 | return False 46 | if self.type != other.type: 47 | return False 48 | return True 49 | 50 | def __ne__(self, other): 51 | return not self.__eq__(other) 52 | 53 | @classmethod 54 | def from_c(cls, uuid): 55 | if uuid.type == driver.BLE_UUID_TYPE_BLE: 56 | return cls(uuid_type=uuid.type) 57 | else: 58 | return cls([0] * 16, uuid_type=uuid.type) # TODO: Hmmmm? [] or [None]*16? what? 59 | 60 | @classmethod 61 | def from_uuid128_array(cls, uuid128_array): 62 | msb_list = uuid128_array[::-1] 63 | return cls(msb_list) 64 | 65 | def to_c(self): 66 | lsb_list = self.base[::-1] 67 | self.__array = util.list_to_uint8_array(lsb_list) 68 | uuid = driver.ble_uuid128_t() 69 | uuid.uuid128 = self.__array.cast() 70 | return uuid 71 | 72 | 73 | class BLEUUID: 74 | class Standard(Enum): 75 | unknown = 0x0000 76 | service_primary = 0x2800 77 | service_secondary = 0x2801 78 | characteristic = 0x2803 79 | cccd = 0x2902 80 | battery_level = 0x2A19 81 | heart_rate = 0x2A37 82 | 83 | def __init__(self, value, base=BLEUUIDBase()): 84 | assert isinstance(base, BLEUUIDBase), 'Invalid argument type' 85 | self.base = base 86 | if self.base.def_base: 87 | try: 88 | self.value = value if isinstance(value, BLEUUID.Standard) else BLEUUID.Standard(value) 89 | except ValueError: 90 | self.value = value 91 | else: 92 | self.value = value 93 | 94 | def get_value(self): 95 | if isinstance(self.value, BLEUUID.Standard): 96 | return self.value.value 97 | return self.value 98 | 99 | def as_array(self): 100 | base_and_value = self.base.base[:] 101 | base_and_value[2] = (self.get_value() >> 8) & 0xff 102 | base_and_value[3] = (self.get_value() >> 0) & 0xff 103 | return base_and_value 104 | 105 | def __str__(self): 106 | if isinstance(self.value, BLEUUID.Standard): 107 | return '0x{:04X} ({})'.format(self.value.value, self.value) 108 | elif self.base.type == driver.BLE_UUID_TYPE_BLE and self.base.def_base: 109 | return '0x{:04X}'.format(self.value) 110 | else: 111 | base_and_value = self.base.base[:] 112 | base_and_value[2] = (self.value >> 8) & 0xff 113 | base_and_value[3] = (self.value >> 0) & 0xff 114 | return '0x{}'.format(''.join(['{:02X}'.format(i) for i in base_and_value])) 115 | 116 | def __repr__(self): 117 | return self.__str__() 118 | 119 | def __eq__(self, other): 120 | if isinstance(other, BLEUUID.Standard): 121 | return self.get_value() == other.value 122 | if not isinstance(other, BLEUUID): 123 | return False 124 | if not self.base == other.base: 125 | return False 126 | if not self.value == other.value: 127 | return False 128 | return True 129 | 130 | def __ne__(self, other): 131 | return not self.__eq__(other) 132 | 133 | def __hash__(self): 134 | return hash(str(self)) 135 | 136 | @classmethod 137 | def from_c(cls, uuid): 138 | return cls(value=uuid.uuid, base=BLEUUIDBase.from_c(uuid)) # TODO: Is this correct? 139 | 140 | @classmethod 141 | def from_uuid128(cls, uuid128): 142 | uuid = util.uint8_array_to_list(uuid128.uuid, 16) 143 | return cls.from_array(uuid) 144 | 145 | def to_c(self): 146 | assert self.base.type is not None, 'Vendor specific UUID not registered' 147 | uuid = driver.ble_uuid_t() 148 | if isinstance(self.value, BLEUUID.Standard): 149 | uuid.uuid = self.value.value 150 | else: 151 | uuid.uuid = self.value 152 | uuid.type = self.base.type 153 | return uuid 154 | 155 | @classmethod 156 | def from_array(cls, uuid_array_lt): 157 | base = list(reversed(uuid_array_lt)) 158 | uuid = (base[2] << 8) + base[3] 159 | base[2] = 0 160 | base[3] = 0 161 | return cls(value=uuid, base=BLEUUIDBase(base, 0)) 162 | -------------------------------------------------------------------------------- /blatann/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/blatann/services/__init__.py -------------------------------------------------------------------------------- /blatann/services/battery/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.services.battery.constants import BATTERY_LEVEL_CHARACTERISTIC_UUID, BATTERY_SERVICE_UUID 4 | from blatann.services.battery.service import BatteryClient as _BatteryClient 5 | from blatann.services.battery.service import BatteryServer as _BatteryServer 6 | from blatann.services.battery.service import SecurityLevel 7 | 8 | 9 | def add_battery_service(gatts_database, enable_notifications=False, security_level=SecurityLevel.OPEN): 10 | """ 11 | Adds a battery service to the given GATT Server database 12 | 13 | :param gatts_database: The database to add the service to 14 | :type gatts_database: blatann.gatt.gatts.GattsDatabase 15 | :param enable_notifications: Whether or not the Battery Level characteristic allows notifications 16 | :param security_level: The security level to use for the service 17 | :return: The Battery service 18 | :rtype: _BatteryServer 19 | """ 20 | return _BatteryServer.add_to_database(gatts_database, enable_notifications, security_level) 21 | 22 | 23 | def find_battery_service(gattc_database): 24 | """ 25 | Finds a battery service in the given GATT client database 26 | 27 | :param gattc_database: the GATT client database to search 28 | :type gattc_database: blatann.gatt.gattc.GattcDatabase 29 | :return: The Battery service if found, None if not found 30 | :rtype: _BatteryClient 31 | """ 32 | return _BatteryClient.find_in_database(gattc_database) 33 | -------------------------------------------------------------------------------- /blatann/services/battery/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.bt_sig.uuids import CharacteristicUuid, ServiceUuid 4 | 5 | BATTERY_SERVICE_UUID = ServiceUuid.battery_service 6 | 7 | BATTERY_LEVEL_CHARACTERISTIC_UUID = CharacteristicUuid.battery_level 8 | -------------------------------------------------------------------------------- /blatann/services/battery/data_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.services import ble_data_types 4 | 5 | # See https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.battery_service.xml 6 | # For more info about the data types and values defined here 7 | 8 | 9 | class BatteryLevel(ble_data_types.Uint8): 10 | pass 11 | -------------------------------------------------------------------------------- /blatann/services/current_time/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.services.current_time.constants import ( 4 | CURRENT_TIME_CHARACTERISTIC_UUID, CURRENT_TIME_SERVICE_UUID, LOCAL_TIME_INFO_CHARACTERISTIC_UUID, 5 | REFERENCE_INFO_CHARACTERISTIC_UUID 6 | ) 7 | from blatann.services.current_time.data_types import ( 8 | AdjustmentReason, AdjustmentReasonType, CurrentTime, DaylightSavingsTimeOffset, ExactTime256, LocalTimeInfo, 9 | ReferenceTimeInfo, TimeAccuracy, TimeSource 10 | ) 11 | from blatann.services.current_time.service import CurrentTimeServer as _CurrentTimeServer 12 | 13 | 14 | def add_current_time_service(gatts_database, enable_writes=False, 15 | enable_local_time_info=False, enable_reference_info=False): 16 | """ 17 | Adds a Current Time service to the given GATT server database 18 | 19 | :param gatts_database: The database to add the service to 20 | :type gatts_database: blatann.gatt.gatts.GattsDatabase 21 | :param enable_writes: Makes the Current time and Local Time info characteristics writable so 22 | clients/centrals can update the server's time 23 | :param enable_local_time_info: Enables the Local Time characteristic in the service 24 | :param enable_reference_info: Enables the Reference Info characteristic in the service 25 | :return: The Current Time service 26 | :rtype: _CurrentTimeServer 27 | """ 28 | return _CurrentTimeServer.add_to_database(gatts_database, enable_writes, 29 | enable_local_time_info, enable_reference_info) 30 | -------------------------------------------------------------------------------- /blatann/services/current_time/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.bt_sig.uuids import CharacteristicUuid, ServiceUuid 4 | 5 | CURRENT_TIME_SERVICE_UUID = ServiceUuid.current_time 6 | 7 | CURRENT_TIME_CHARACTERISTIC_UUID = CharacteristicUuid.current_time 8 | LOCAL_TIME_INFO_CHARACTERISTIC_UUID = CharacteristicUuid.local_time_information 9 | REFERENCE_INFO_CHARACTERISTIC_UUID = CharacteristicUuid.reference_time_information 10 | -------------------------------------------------------------------------------- /blatann/services/decoded_event_dispatcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import binascii 4 | 5 | from blatann.event_args import ( 6 | DecodedReadCompleteEventArgs, DecodedWriteEventArgs, NotificationReceivedEventArgs, ReadCompleteEventArgs, 7 | WriteEventArgs 8 | ) 9 | from blatann.gatt import GattStatusCode 10 | from blatann.services import ble_data_types 11 | 12 | 13 | class DecodedReadWriteEventDispatcher: 14 | def __init__(self, owner, ble_type, event_to_raise, logger=None): 15 | self.owner = owner 16 | self.ble_type = ble_type 17 | self.event_to_raise = event_to_raise 18 | self.logger = logger 19 | 20 | def decode(self, data): 21 | try: 22 | stream = ble_data_types.BleDataStream(data) 23 | return self.ble_type.decode(stream) 24 | except Exception as e: 25 | if self.logger: 26 | self.logger.error("Failed to decode into {} type, stream: [{}]".format(self.ble_type.__name__, 27 | binascii.hexlify(data))) 28 | self.logger.exception(e) 29 | return None 30 | 31 | def __call__(self, characteristic, event_args): 32 | 33 | if isinstance(event_args, ReadCompleteEventArgs): 34 | if event_args.status == GattStatusCode.success: 35 | decoded_value = self.decode(event_args.value) 36 | else: 37 | decoded_value = None 38 | decoded_event_args = DecodedReadCompleteEventArgs.from_read_complete_event_args(event_args, decoded_value) 39 | 40 | elif isinstance(event_args, NotificationReceivedEventArgs): 41 | decoded_value = self.decode(event_args.value) 42 | decoded_event_args = DecodedReadCompleteEventArgs.from_notification_complete_event_args(event_args, decoded_value) 43 | elif isinstance(event_args, WriteEventArgs): 44 | decoded_value = self.decode(event_args.value) 45 | decoded_event_args = DecodedWriteEventArgs(decoded_value, event_args.value) 46 | else: 47 | if self.logger: 48 | self.logger.error("Unable to handle unknown event args {}".format(event_args)) 49 | return 50 | 51 | self.event_to_raise.notify(self.owner, decoded_event_args) 52 | -------------------------------------------------------------------------------- /blatann/services/device_info/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.services.device_info.constants import ( 4 | CHARACTERISTICS, DIS_SERVICE_UUID, FirmwareRevisionCharacteristic, HardwareRevisionCharacteristic, 5 | ManufacturerNameCharacteristic, ModelNumberCharacteristic, PnpIdCharacteristic, RegulatoryCertificateCharacteristic, 6 | SerialNumberCharacteristic, SoftwareRevisionCharacteristic, SystemIdCharacteristic 7 | ) 8 | from blatann.services.device_info.data_types import PnpId, PnpVendorSource, SystemId 9 | from blatann.services.device_info.service import DisClient as _DisClient 10 | from blatann.services.device_info.service import DisServer as _DisServer 11 | 12 | 13 | def add_device_info_service(gatts_database): 14 | """ 15 | :rtype: _DisServer 16 | """ 17 | return _DisServer.add_to_database(gatts_database) 18 | 19 | 20 | def find_device_info_service(gattc_database): 21 | """ 22 | :type gattc_database: blatann.gatt.gattc.GattcDatabase 23 | :rtype: _DisClient 24 | """ 25 | return _DisClient.find_in_database(gattc_database) 26 | -------------------------------------------------------------------------------- /blatann/services/device_info/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.bt_sig.uuids import CharacteristicUuid, ServiceUuid 4 | 5 | # Uuids 6 | DIS_SERVICE_UUID = ServiceUuid.device_information 7 | 8 | SYSTEM_ID_UUID = CharacteristicUuid.system_id 9 | MODEL_NUMBER_UUID = CharacteristicUuid.model_number_string 10 | SERIAL_NUMBER_UUID = CharacteristicUuid.serial_number_string 11 | FIRMWARE_REV_UUID = CharacteristicUuid.firmware_revision_string 12 | HARDWARE_REV_UUID = CharacteristicUuid.hardware_revision_string 13 | SOFTWARE_REV_UUID = CharacteristicUuid.software_revision_string 14 | MANUFACTURER_NAME_UUID = CharacteristicUuid.manufacturer_name_string 15 | REGULATORY_CERT_UUID = CharacteristicUuid.ieee11073_20601_regulatory_certification_data_list 16 | PNP_ID_UUID = CharacteristicUuid.pnp_id 17 | 18 | 19 | class _Characteristic: 20 | def __init__(self, name, uuid): 21 | self.uuid = uuid 22 | self.name = name 23 | 24 | def __str__(self): 25 | return "{} ({})".format(self.name, self.uuid) 26 | 27 | 28 | SystemIdCharacteristic = _Characteristic("System Id", SYSTEM_ID_UUID) 29 | ModelNumberCharacteristic = _Characteristic("Model Number", MODEL_NUMBER_UUID) 30 | SerialNumberCharacteristic = _Characteristic("Serial Number", SERIAL_NUMBER_UUID) 31 | FirmwareRevisionCharacteristic = _Characteristic("Firmware Revision", FIRMWARE_REV_UUID) 32 | HardwareRevisionCharacteristic = _Characteristic("Hardware Revision", HARDWARE_REV_UUID) 33 | SoftwareRevisionCharacteristic = _Characteristic("Software Revision", SOFTWARE_REV_UUID) 34 | ManufacturerNameCharacteristic = _Characteristic("Manufacturer Name", MANUFACTURER_NAME_UUID) 35 | RegulatoryCertificateCharacteristic = _Characteristic("Regulatory Certificate", REGULATORY_CERT_UUID) 36 | PnpIdCharacteristic = _Characteristic("PNP Id", PNP_ID_UUID) 37 | 38 | 39 | CHARACTERISTICS = [ 40 | SystemIdCharacteristic, 41 | ModelNumberCharacteristic, 42 | SerialNumberCharacteristic, 43 | FirmwareRevisionCharacteristic, 44 | HardwareRevisionCharacteristic, 45 | SoftwareRevisionCharacteristic, 46 | ManufacturerNameCharacteristic, 47 | RegulatoryCertificateCharacteristic, 48 | PnpIdCharacteristic 49 | ] 50 | -------------------------------------------------------------------------------- /blatann/services/device_info/data_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import IntEnum 4 | 5 | from blatann.services import ble_data_types 6 | 7 | 8 | class PnpVendorSource(IntEnum): 9 | bluetooth_sig = 1 10 | usb_vendor = 2 11 | 12 | 13 | class PnpId(ble_data_types.BleCompoundDataType): 14 | data_stream_types = [ble_data_types.Uint8, ble_data_types.Uint16, ble_data_types.Uint16, ble_data_types.Uint16] 15 | 16 | def __init__(self, vendor_id_source, vendor_id, product_id, product_revision): 17 | super(PnpId, self).__init__() 18 | self.vendor_id_source = vendor_id_source 19 | self.vendor_id = vendor_id 20 | self.product_id = product_id 21 | self.product_revision = product_revision 22 | 23 | def encode(self): 24 | return self.encode_values(self.vendor_id_source, self.vendor_id, self.product_id, self.product_revision) 25 | 26 | @classmethod 27 | def decode(cls, stream): 28 | vendor_id_source, vendor_id, product_id, product_version = super(PnpId, cls).decode(stream) 29 | return PnpId(vendor_id_source, vendor_id, product_id, product_version) 30 | 31 | def __repr__(self): 32 | return "{}(Vendor ID Source: {}, Vendor ID: {}, Product ID: {}, Product Version: {})".format( 33 | self.__class__.__name__, self.vendor_id_source, self.vendor_id, self.product_id, self.product_revision) 34 | 35 | 36 | class SystemId(ble_data_types.BleCompoundDataType): 37 | data_stream_types = [ble_data_types.Uint40, ble_data_types.Uint24] 38 | 39 | def __init__(self, manufacturer_id, organizationally_unique_id): 40 | super(SystemId, self).__init__() 41 | self.manufacturer_id = manufacturer_id 42 | self.organizationally_unique_id = organizationally_unique_id 43 | 44 | def encode(self): 45 | """ 46 | :rtype: ble_data_types.BleDataStream 47 | """ 48 | return self.encode_values(self.manufacturer_id, self.organizationally_unique_id) 49 | 50 | @classmethod 51 | def decode(cls, stream): 52 | manufacturer_id, organizationally_unique_id = super(SystemId, cls).decode(stream) 53 | return SystemId(manufacturer_id, organizationally_unique_id) 54 | 55 | def __repr__(self): 56 | return "{}(Manufacturer ID: {}, OUI: {})".format(self.__class__.__name__, self.manufacturer_id, self.organizationally_unique_id) 57 | -------------------------------------------------------------------------------- /blatann/services/glucose/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.gatt import SecurityLevel 4 | from blatann.services.glucose.constants import GLUCOSE_SERVICE_UUID 5 | from blatann.services.glucose.data_types import ( 6 | CarbohydrateType, CarbsInfo, ExerciseInfo, GlucoseConcentrationUnits, GlucoseContext, GlucoseFeatures, 7 | GlucoseFeatureType, GlucoseMeasurement, GlucoseSample, GlucoseType, HealthStatus, MealType, MedicationInfo, 8 | MedicationType, MedicationUnits, SampleLocation, SensorStatus, SensorStatusType, TesterType 9 | ) 10 | from blatann.services.glucose.database import BasicGlucoseDatabase, IGlucoseDatabase 11 | from blatann.services.glucose.service import GlucoseServer as _GlucoseServer 12 | 13 | 14 | def add_glucose_service(gatts_database, glucose_database, security_level=SecurityLevel.OPEN, 15 | include_context_characteristic=True): 16 | """ 17 | Adds the Glucose bluetooth service to the Gatt Server database 18 | 19 | :param gatts_database: The GATT database to add the service to 20 | :type gatts_database: blatann.gatt.gatts.GattsDatabase 21 | :param glucose_database: The database which holds the glucose measurements 22 | :type glucose_database: IGlucoseDatabase 23 | :param security_level: The security level for the record-access control point of the service 24 | :type security_level: SecurityLevel 25 | :param include_context_characteristic: flag whether or not to include the 26 | optional context characteristic in the service. If this is False, 27 | any context stored with glucose measurements will not be reported. 28 | """ 29 | return _GlucoseServer.add_to_database(gatts_database, glucose_database, security_level, 30 | include_context_characteristic) 31 | -------------------------------------------------------------------------------- /blatann/services/glucose/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.bt_sig.uuids import CharacteristicUuid, ServiceUuid 4 | 5 | GLUCOSE_SERVICE_UUID = ServiceUuid.glucose 6 | 7 | MEASUREMENT_CHARACTERISTIC_UUID = CharacteristicUuid.glucose_measurement 8 | MEASUREMENT_CONTEXT_CHARACTERISTIC_UUID = CharacteristicUuid.glucose_measurement_context 9 | FEATURE_CHARACTERISTIC_UUID = CharacteristicUuid.glucose_feature 10 | RACP_CHARACTERISTIC_UUID = CharacteristicUuid.record_access_control_point 11 | -------------------------------------------------------------------------------- /blatann/services/glucose/racp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import IntEnum 4 | 5 | from blatann.exceptions import DecodeError 6 | from blatann.services import ble_data_types 7 | 8 | 9 | class RacpOpcode(IntEnum): 10 | report_stored_records = 1 11 | delete_stored_records = 2 12 | abort_operation = 3 13 | report_number_of_records = 4 14 | number_of_records_response = 5 15 | response_code = 6 16 | 17 | 18 | class RacpOperator(IntEnum): 19 | null = 0 20 | all_records = 1 21 | less_than_or_equal_to = 2 22 | greater_than_or_equal_to = 3 23 | within_range_inclusive = 4 24 | first_record = 5 25 | last_record = 6 26 | 27 | 28 | class FilterType(IntEnum): 29 | sequence_number = 1 30 | user_facing_time = 2 31 | 32 | 33 | class RacpResponseCode(IntEnum): 34 | success = 1 35 | not_supported = 2 36 | invalid_operator = 3 37 | operator_not_supported = 4 38 | invalid_operand = 5 39 | no_records_found = 6 40 | abort_not_successful = 7 41 | procedure_not_completed = 8 42 | operand_not_supported = 9 43 | 44 | 45 | class RacpCommand(ble_data_types.BleCompoundDataType): 46 | def __init__(self, opcode, operator, filter_type=None, filter_params=None): 47 | """ 48 | :type opcode: RacpOpcode 49 | :type operator: RacpOperator 50 | :type filter_type: FilterType 51 | :param filter_params: 52 | """ 53 | self.opcode = opcode 54 | self.operator = operator 55 | self.filter_type = filter_type 56 | if filter_params is None: 57 | filter_params = [] 58 | self.filter_params = filter_params 59 | 60 | def get_filter_min_max(self): 61 | if self.operator == RacpOperator.all_records: 62 | return None, None 63 | if self.operator == RacpOperator.less_than_or_equal_to: 64 | return None, self.filter_params[0] 65 | if self.operator == RacpOperator.greater_than_or_equal_to: 66 | return self.filter_params[0], None 67 | if self.operator == RacpOperator.within_range_inclusive: 68 | return self.filter_params[0], self.filter_params[1] 69 | # First/Last record, return Nones 70 | return None, None 71 | 72 | def encode(self): 73 | stream = ble_data_types.BleDataStream() 74 | stream.encode(ble_data_types.Uint8, self.opcode) 75 | stream.encode(ble_data_types.Uint8, self.operator) 76 | if self.filter_type is not None: 77 | stream.encode(ble_data_types.Uint8, self.filter_type) 78 | for f in self.filter_params: 79 | stream.encode(ble_data_types.Uint16, f) 80 | return stream 81 | 82 | @classmethod 83 | def decode(cls, stream): 84 | opcode = stream.decode(ble_data_types.Uint8) 85 | operator = stream.decode(ble_data_types.Uint8) 86 | if len(stream) > 0: 87 | filter_type = stream.decode(ble_data_types.Uint8) 88 | filter_params = [] 89 | while len(stream) >= 2: 90 | filter_params.append(stream.decode(ble_data_types.Uint16)) 91 | else: 92 | filter_type = None 93 | filter_params = None 94 | 95 | return RacpCommand(opcode, operator, filter_type, filter_params) 96 | 97 | 98 | class RacpResponse(ble_data_types.BleCompoundDataType): 99 | def __init__(self, request_opcode=None, response_code=None, record_count=None): 100 | """ 101 | :type request_opcode: RacpOpcode 102 | :type response_code: RacpResponseCode 103 | :type record_count: int 104 | """ 105 | self.request_code = request_opcode 106 | self.response_code = response_code 107 | self.record_count = record_count 108 | 109 | def encode(self): 110 | stream = ble_data_types.BleDataStream() 111 | if self.record_count is None: 112 | stream.encode_multiple([ble_data_types.Uint8, RacpOpcode.response_code], 113 | [ble_data_types.Uint8, RacpOperator.null], 114 | [ble_data_types.Uint8, self.request_code], 115 | [ble_data_types.Uint8, self.response_code]) 116 | else: 117 | stream.encode_multiple([ble_data_types.Uint8, RacpOpcode.number_of_records_response], 118 | [ble_data_types.Uint8, RacpOperator.null], 119 | [ble_data_types.Uint16, self.record_count]) 120 | return stream 121 | 122 | @classmethod 123 | def decode(cls, stream): 124 | opcode = RacpOpcode(stream.decode(ble_data_types.Uint8)) 125 | _ = RacpOperator(stream.decode(ble_data_types.Uint8)) 126 | 127 | if opcode == RacpOpcode.response_code: 128 | request_opcode, response_code = stream.decode_multiple(ble_data_types.Uint8, ble_data_types.Uint8) 129 | record_count = None 130 | elif opcode == RacpOpcode.number_of_records_response: 131 | request_opcode = None 132 | response_code = None 133 | record_count = stream.decode(ble_data_types.Uint16) 134 | else: 135 | raise DecodeError("Unable to decode RACP Response, got opcode: {}".format(opcode)) 136 | 137 | return RacpResponse(request_opcode, response_code, record_count) 138 | -------------------------------------------------------------------------------- /blatann/services/nordic_uart/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.services.nordic_uart.constants import ( 4 | NORDIC_UART_FEATURE_CHARACTERISTIC_UUID, NORDIC_UART_RX_CHARACTERISTIC_UUID, NORDIC_UART_SERVICE_UUID, 5 | NORDIC_UART_TX_CHARACTERISTIC_UUID 6 | ) 7 | from blatann.services.nordic_uart.service import NordicUartClient as _Client 8 | from blatann.services.nordic_uart.service import NordicUartServer as _Server 9 | 10 | 11 | def add_nordic_uart_service(gatts_database, max_characteristic_size=None): 12 | """ 13 | Adds a Nordic UART service to the database 14 | 15 | :param gatts_database: The database to add the service to 16 | :type gatts_database: blatann.gatt.gatts.GattsDatabase 17 | :param max_characteristic_size: The size of the characteristic which determines the read/write chunk size. 18 | This should be tuned to the MTU size of the connection 19 | :return: The Nordic Uart Service 20 | :rtype: _Server 21 | """ 22 | return _Server.add_to_database(gatts_database, max_characteristic_size) 23 | 24 | 25 | def find_nordic_uart_service(gattc_database): 26 | """ 27 | Finds a Nordic UART service in the given GATT client database 28 | 29 | :param gattc_database: the GATT client database to search 30 | :type gattc_database: blatann.gatt.gattc.GattcDatabase 31 | :return: The UART service if found, None if not found 32 | :rtype: _Client 33 | """ 34 | return _Client.find_in_database(gattc_database) 35 | -------------------------------------------------------------------------------- /blatann/services/nordic_uart/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.uuid import Uuid128 4 | 5 | NORDIC_UART_SERVICE_UUID = Uuid128("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") 6 | 7 | # TX and RX characteristics are from the perspective of the client. The server will receive on TX and transmit on RX 8 | NORDIC_UART_TX_CHARACTERISTIC_UUID = NORDIC_UART_SERVICE_UUID.new_uuid_from_base(0x0002) 9 | NORDIC_UART_RX_CHARACTERISTIC_UUID = NORDIC_UART_SERVICE_UUID.new_uuid_from_base(0x0003) 10 | # Slight deviation from Nordic's implementation so the client can read the server's characteristic size 11 | NORDIC_UART_FEATURE_CHARACTERISTIC_UUID = NORDIC_UART_SERVICE_UUID.new_uuid_from_base(0x0004) 12 | -------------------------------------------------------------------------------- /blatann/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | import logging 5 | import sys 6 | import threading 7 | import time 8 | 9 | from blatann.utils import _threading 10 | 11 | LOG_FORMAT = "[%(asctime)s] [%(threadName)s] [%(name)s.%(funcName)s:%(lineno)s] [%(levelname)s]: %(message)s" 12 | 13 | 14 | def setup_logger(name=None, level="DEBUG"): 15 | logger = logging.getLogger(name) 16 | logger.setLevel(level) 17 | formatter = logging.Formatter(LOG_FORMAT) 18 | stream_handler = logging.StreamHandler(sys.stderr) 19 | stream_handler.setFormatter(formatter) 20 | logger.addHandler(stream_handler) 21 | return logger 22 | 23 | 24 | def repr_format(obj, *args, **kwargs): 25 | """ 26 | Helper function to format objects into strings in the format of: 27 | ClassName(param1=value1, param2=value2, ...) 28 | 29 | :param obj: Object to get the class name from 30 | :param args: Optional tuples of (param_name, value) which will ensure ordering during format 31 | :param kwargs: Other keyword args to populate with 32 | :return: String which represents the object 33 | """ 34 | items = args + tuple(kwargs.items()) 35 | inner = ", ".join("{}={!r}".format(k, v) for k, v in items) 36 | return "{}({})".format(obj.__class__.__name__, inner) 37 | 38 | 39 | class Stopwatch: 40 | def __init__(self): 41 | self._t_start = 0 42 | self._t_stop = 0 43 | self._t_mark = 0 44 | self._is_running = False 45 | self._started = False 46 | 47 | def start(self): 48 | self._t_start = time.perf_counter() 49 | self._t_stop = 0 50 | self._started = True 51 | self._is_running = True 52 | 53 | def stop(self): 54 | if self._is_running: 55 | self._t_stop = time.perf_counter() 56 | self._is_running = False 57 | 58 | def mark(self): 59 | if self._is_running: 60 | self._t_mark = time.perf_counter() 61 | 62 | @property 63 | def is_running(self): 64 | return self._is_running 65 | 66 | @property 67 | def start_time(self): 68 | return self._t_start 69 | 70 | @property 71 | def stop_time(self): 72 | return self._t_stop 73 | 74 | @property 75 | def elapsed(self): 76 | if not self._started: 77 | raise RuntimeError("Timer was never started") 78 | if self._is_running: 79 | if self._t_mark == 0: 80 | return time.perf_counter() - self._t_start 81 | return self._t_mark - self._t_start 82 | return self._t_stop - self._t_start 83 | 84 | def __enter__(self): 85 | self.start() 86 | return self 87 | 88 | def __exit__(self, exc_type, exc_val, exc_tb): 89 | self.stop() 90 | return self 91 | 92 | 93 | class SynchronousMonotonicCounter: 94 | """ 95 | Utility class which implements a thread-safe monotonic counter 96 | """ 97 | def __init__(self, start_value=0): 98 | self._lock = threading.Lock() 99 | self._counter = start_value 100 | 101 | def __iter__(self): 102 | return self 103 | 104 | def __next__(self): 105 | return self.next() 106 | 107 | def next(self): 108 | with self._lock: 109 | value = self._counter 110 | self._counter += 1 111 | return value 112 | 113 | 114 | def snake_case_to_capitalized_words(string: str): 115 | parts = [p for p in string.split("_") if p] 116 | words = [] 117 | for p in parts: 118 | if len(p) == 1: 119 | words.append(p.upper()) 120 | else: 121 | words.append(p[0].upper() + p[1:]) 122 | return " ".join(words) 123 | 124 | 125 | class IntEnumWithDescription(int, enum.Enum): 126 | def __new__(cls, *args, **kwargs): 127 | val = args[0] 128 | obj = int.__new__(cls, val) 129 | obj._value_ = val 130 | return obj 131 | 132 | def __init__(self, _, description: str = ""): 133 | self._description_ = description 134 | if not self._description_: 135 | self._description_ = snake_case_to_capitalized_words(self.name) 136 | 137 | @property 138 | def description(self): 139 | return self._description_ 140 | -------------------------------------------------------------------------------- /blatann/utils/_threading.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import threading 4 | 5 | 6 | def _or(self, other): 7 | if isinstance(self, _OrEvent): 8 | events = self.events[:] 9 | elif isinstance(self, threading.Event): 10 | events = [self] 11 | else: 12 | raise ValueError("Incompatible Event type to OR with") 13 | 14 | if isinstance(other, _OrEvent): 15 | events.extend(other.events) 16 | elif isinstance(other, threading.Event): 17 | events.append(other) 18 | else: 19 | raise ValueError("Incompatible Event type to OR with") 20 | 21 | return _OrEvent(*events) 22 | 23 | 24 | def _or_set(self): 25 | self._set() 26 | for h in self._changed: 27 | h() 28 | 29 | 30 | def _or_clear(self): 31 | self._clear() 32 | for h in self._changed: 33 | h() 34 | 35 | 36 | class _OrEvent(threading.Event): 37 | def __init__(self, *events): 38 | super(_OrEvent, self).__init__() 39 | self.events = [] 40 | for e in events: 41 | self.add(e) 42 | 43 | def _changed(self): 44 | bools = [e.is_set() for e in self.events] 45 | if any(bools): 46 | self.set() 47 | else: 48 | self.clear() 49 | 50 | def _orify(self, e): 51 | if not hasattr(e, "_set"): 52 | e._set = e.set 53 | e._clear = e.clear 54 | e._changed = [] 55 | e.set = lambda: _or_set(e) 56 | e.clear = lambda: _or_clear(e) 57 | e._changed.append(self._changed) 58 | 59 | def add(self, event): 60 | if isinstance(event, _OrEvent): 61 | for e in event.events: 62 | self._orify(e) 63 | self.events.extend(event.events) 64 | else: 65 | self._orify(event) 66 | self.events.append(event) 67 | 68 | def __or__(self, other): 69 | return _or(self, other) 70 | 71 | 72 | threading.Event.__or__ = _or 73 | -------------------------------------------------------------------------------- /blatann/utils/queued_tasks_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import queue 5 | import threading 6 | from typing import Generic, Optional, TypeVar 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | T = TypeVar("T") 11 | 12 | 13 | class QueuedTasksManagerBase(Generic[T]): 14 | """ 15 | Handles queuing of tasks that can only be done one at a time 16 | """ 17 | class TaskFailure: 18 | def __init__(self, reason=None, ignore_stack_trace=False, clear_all=False): 19 | self.ignore_stack_trace = ignore_stack_trace 20 | self.clear_all = clear_all 21 | self.reason = reason 22 | 23 | def __init__(self, max_processing_items_at_once=1): 24 | self._input_queue = queue.Queue() 25 | self._lock = threading.RLock() 26 | self._in_process_queue = queue.Queue(max_processing_items_at_once) 27 | 28 | def _add_task(self, task: T): 29 | with self._lock: 30 | # Cannot process any more tasks currently, add to input queue 31 | if self._in_process_queue.full(): 32 | self._input_queue.put(task) 33 | else: 34 | try: 35 | # Handle the task. If it's not complete put it in the in process queue 36 | task_complete = self._handle_task(task) 37 | if not task_complete: 38 | self._in_process_queue.put_nowait(task) 39 | except Exception as e: 40 | self._process_exception(task, e) 41 | 42 | def _pop_task_in_process(self) -> Optional[T]: 43 | # Pops the earliest task from the process queue, will be completed shortly 44 | with self._lock: 45 | return self._get_next(self._in_process_queue) 46 | 47 | def _task_completed(self, task: T): 48 | with self._lock: 49 | # Ensure the queue wasn't filled by another thread 50 | if self._in_process_queue.full(): 51 | return 52 | # Start processing tasks from the input queue 53 | task = self._get_next(self._input_queue) 54 | while task: 55 | try: 56 | task_complete = self._handle_task(task) 57 | if not task_complete: 58 | # Task is still in process, add to the queue and if full break out of the loop 59 | self._in_process_queue.put_nowait(task) 60 | if self._in_process_queue.full(): 61 | break 62 | except Exception as e: 63 | self._process_exception(task, e) 64 | task = self._get_next(self._input_queue) 65 | 66 | def _process_exception(self, task: T, e: Exception): 67 | action = self._handle_task_failure(task, e) 68 | if not action.ignore_stack_trace: 69 | logger.exception(e) 70 | if action.clear_all: 71 | self._clear_all(action.reason) 72 | 73 | def _get_next(self, q: queue.Queue) -> Optional[T]: 74 | if not q: 75 | q = self._input_queue 76 | try: 77 | return q.get_nowait() 78 | except queue.Empty: 79 | return None 80 | 81 | def _clear_all(self, reason): 82 | with self._lock: 83 | # Clear the process queue, then clear the input queue 84 | task = self._get_next(self._in_process_queue) 85 | while task: 86 | self._handle_task_cleared(task, reason) 87 | task = self._get_next(self._in_process_queue) 88 | 89 | # Clear the input queue 90 | task = self._get_next(self._input_queue) 91 | while task: 92 | self._handle_task_cleared(task, reason) 93 | task = self._get_next(self._input_queue) 94 | 95 | def clear_all(self): 96 | raise NotImplementedError() 97 | 98 | def _handle_task(self, task: T): 99 | raise NotImplementedError() 100 | 101 | def _handle_task_failure(self, task: T, e: Exception) -> TaskFailure: 102 | raise NotImplementedError() 103 | 104 | def _handle_task_cleared(self, task: T, reason): 105 | raise NotImplementedError() 106 | -------------------------------------------------------------------------------- /blatann/waitables/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from blatann.waitables.event_waitable import EventWaitable 4 | from blatann.waitables.waitable import GenericWaitable, Waitable 5 | -------------------------------------------------------------------------------- /blatann/waitables/connection_waitable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import typing 5 | 6 | from blatann.exceptions import InvalidStateException 7 | from blatann.nrf import nrf_events, nrf_types 8 | from blatann.waitables.waitable import Waitable 9 | 10 | if typing.TYPE_CHECKING: 11 | from blatann.device import BleDevice 12 | from blatann.event_args import DisconnectionEventArgs 13 | from blatann.peer import Client, Peer, Peripheral 14 | 15 | 16 | class ConnectionWaitable(Waitable): 17 | def __init__(self, ble_device: BleDevice, current_peer: Peer, role=nrf_types.BLEGapRoles.periph): 18 | super().__init__() 19 | self._peer = current_peer 20 | self._role = role 21 | self.ble_driver = ble_device.ble_driver 22 | ble_device.ble_driver.event_subscribe(self._on_connected_event, nrf_events.GapEvtConnected) 23 | ble_device.ble_driver.event_subscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) 24 | 25 | def wait(self, timeout: float = None, exception_on_timeout=True) -> Peer: 26 | return super().wait(timeout, exception_on_timeout) 27 | 28 | async def as_async(self, timeout: float = None, exception_on_timeout=True, loop: asyncio.AbstractEventLoop = None) -> Peer: 29 | return await super().as_async(timeout, exception_on_timeout, loop) 30 | 31 | def _event_occured(self, ble_driver, result): 32 | ble_driver.event_unsubscribe(self._on_connected_event, nrf_events.GapEvtConnected) 33 | ble_driver.event_unsubscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) 34 | self._notify(result) 35 | 36 | def _on_timeout(self): 37 | self.ble_driver.event_unsubscribe(self._on_connected_event, nrf_events.GapEvtConnected) 38 | self.ble_driver.event_unsubscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) 39 | 40 | def _on_timeout_event(self, ble_driver, event: nrf_events.GapEvtTimeout): 41 | if self._role == nrf_types.BLEGapRoles.periph: 42 | expected_source = nrf_types.BLEGapTimeoutSrc.advertising 43 | else: 44 | expected_source = nrf_types.BLEGapTimeoutSrc.conn 45 | if event.src == expected_source: 46 | self._event_occured(ble_driver, None) 47 | 48 | def _on_connected_event(self, ble_driver, event: nrf_events.GapEvtConnected): 49 | if event.role != self._role: 50 | return 51 | self._event_occured(ble_driver, self._peer) 52 | 53 | 54 | class ClientConnectionWaitable(ConnectionWaitable): 55 | def __init__(self, ble_device: BleDevice, peer: Peer): 56 | super().__init__(ble_device, peer, nrf_types.BLEGapRoles.periph) 57 | 58 | def wait(self, timeout=None, exception_on_timeout=True) -> Client: 59 | return super().wait(timeout, exception_on_timeout) 60 | 61 | 62 | class PeripheralConnectionWaitable(ConnectionWaitable): 63 | def __init__(self, ble_device, peer): 64 | super().__init__(ble_device, peer, nrf_types.BLEGapRoles.central) 65 | 66 | def wait(self, timeout=None, exception_on_timeout=True) -> Peripheral: 67 | return super().wait(timeout, exception_on_timeout) 68 | 69 | 70 | class DisconnectionWaitable(Waitable): 71 | def __init__(self, connected_peer: Peer): 72 | super().__init__(n_args=2) 73 | if not connected_peer: 74 | raise InvalidStateException("Peer already disconnected") 75 | connected_peer.on_disconnect.register(self._on_disconnect) 76 | 77 | def _on_disconnect(self, disconnected_peer: Peer, event_args: DisconnectionEventArgs): 78 | disconnected_peer.on_disconnect.deregister(self._on_disconnect) 79 | self._notify(disconnected_peer, event_args) 80 | -------------------------------------------------------------------------------- /blatann/waitables/event_queue.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import queue 5 | from typing import Generic, Optional, Tuple, Union 6 | 7 | from blatann.event_type import Event, TEvent, TSender 8 | 9 | _disconnect_sentinel = object() 10 | 11 | 12 | class EventQueue(Generic[TSender, TEvent]): 13 | """ 14 | Iterable object which provides a stream of events dispatched on a provided ``Event`` object. 15 | The iterator does not exit unless a "disconnect event" is provided, typically when a peer disconnects. 16 | """ 17 | def __init__( 18 | self, 19 | event: Event[TSender, TEvent], 20 | disconnect_event: Event = None 21 | ): 22 | self._event = event 23 | self._queue = queue.Queue[Union[Tuple[TSender, TEvent], None]]() 24 | self._event.register(self._on_event, weak=True) 25 | self._disconnect_event = disconnect_event 26 | if disconnect_event: 27 | disconnect_event.register(self._on_disconnect, weak=True) 28 | 29 | def __iter__(self): 30 | """Create the iterator object""" 31 | return self 32 | 33 | def __next__(self) -> Tuple[TSender, TEvent]: 34 | """Block until the next event is received""" 35 | item = self.get() 36 | if item is None: 37 | raise StopIteration 38 | return item 39 | 40 | def get(self, block=True, timeout=None) -> Optional[Tuple[TSender, TEvent]]: 41 | """ 42 | Gets the next item in the queue. 43 | If a disconnect event occurs, the queue will return ``None`` and not return any other events afterward. 44 | 45 | :param block: True to block the current thread until the next event is received 46 | :param timeout: Optional timeout to wait for the next object 47 | :return: The next item in the queue 48 | :raises: queue.Empty if a timeout is provided and no event was received 49 | """ 50 | item = self._queue.get(block, timeout) 51 | if item is _disconnect_sentinel: 52 | self._event.deregister(self._on_event) 53 | if self._disconnect_event: 54 | self._disconnect_event.deregister(self._on_disconnect) 55 | item = None 56 | return item 57 | 58 | def _get_next(self, block=True, timeout=None): 59 | item = self._queue.get(block, timeout) 60 | if item is _disconnect_sentinel: 61 | # Disconnection occurred, clear the handlers from the events 62 | self._event.deregister(self._on_event) 63 | if self._disconnect_event: 64 | self._disconnect_event.deregister(self._on_disconnect) 65 | item = None 66 | return item 67 | 68 | def _on_event(self, sender: TSender, event: TEvent): 69 | self._queue.put((sender, event)) 70 | 71 | def _on_disconnect(self, sender, event): 72 | self._queue.put(_disconnect_sentinel) 73 | 74 | 75 | class AsyncEventQueue(Generic[TSender, TEvent]): 76 | """ 77 | Asynchronous iterable object which provides a stream of events dispatched on a provided ``Event`` object. 78 | The iterator does not exit unless a "disconnect event" is provided, typically when a peer disconnects. 79 | """ 80 | def __init__( 81 | self, 82 | event: Event[TSender, TEvent], 83 | disconnect_event: Event = None, 84 | event_loop: asyncio.AbstractEventLoop = None 85 | ): 86 | self._event = event 87 | self._queue = asyncio.Queue() 88 | self._event_loop = event_loop or asyncio.get_event_loop() 89 | self._event.register(self._on_event, weak=True) 90 | self._disconnect_event = disconnect_event 91 | if disconnect_event: 92 | disconnect_event.register(self._on_disconnect, weak=True) 93 | 94 | def __aiter__(self): 95 | """Create the async iterator object""" 96 | return self 97 | 98 | async def __anext__(self) -> Tuple[TSender, TEvent]: 99 | """Asynchronously block until the next event arrives. Return the sender and event objects""" 100 | item = await self.get() 101 | if item is None: 102 | raise StopAsyncIteration 103 | return item 104 | 105 | async def get(self) -> Optional[Tuple[TSender, TEvent]]: 106 | """ 107 | Asynchronously gets the next item in the queue. 108 | If a disconnect event occurs, the queue will return ``None`` and not provide any other events afterward. 109 | 110 | :return: The next item in the queue, or None if the disconnect event occurred 111 | """ 112 | item = await self._queue.get() 113 | if item is _disconnect_sentinel: 114 | # Disconnection occurred, clear the handlers from the events 115 | self._event.deregister(self._on_event) 116 | if self._disconnect_event: 117 | self._disconnect_event.deregister(self._on_disconnect) 118 | item = None 119 | return item 120 | 121 | def _on_event(self, sender: TSender, event: TEvent): 122 | asyncio.run_coroutine_threadsafe(self._queue.put((sender, event)), self._event_loop) 123 | 124 | def _on_disconnect(self, sender, event): 125 | asyncio.run_coroutine_threadsafe(self._queue.put(_disconnect_sentinel), self._event_loop) 126 | -------------------------------------------------------------------------------- /blatann/waitables/event_waitable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from typing import Callable, Generic, Tuple 5 | 6 | from blatann.event_type import Event, TEvent, TSender 7 | from blatann.waitables.waitable import Waitable 8 | 9 | 10 | class EventWaitable(Waitable[Tuple[TSender, TEvent]]): 11 | """ 12 | Waitable implementation which waits on an :class:`~blatann.event_type.Event`. 13 | """ 14 | def __init__(self, event: Event[TSender, TEvent]): 15 | super().__init__(n_args=2) 16 | self._event = event 17 | self._event.register(self._on_event) 18 | 19 | def _on_event(self, *args): 20 | self._event.deregister(self._on_event) 21 | self._notify(*args) 22 | 23 | def _on_timeout(self): 24 | self._event.deregister(self._on_event) 25 | 26 | def wait(self, timeout=None, exception_on_timeout=True) -> Tuple[TSender, TEvent]: 27 | res = super().wait(timeout, exception_on_timeout) 28 | if res is None: # Timeout, send None, None for the sender and event_args 29 | return None, None 30 | return res 31 | 32 | async def as_async( 33 | self, timeout: float = None, 34 | exception_on_timeout=True, 35 | loop: asyncio.AbstractEventLoop = None 36 | ) -> Tuple[TSender, TEvent]: 37 | 38 | res = await super().as_async(timeout, exception_on_timeout, loop) 39 | if res is None: # Timeout, send None, None for the sender and event_args 40 | return None, None 41 | return res 42 | 43 | def then(self, callback: Callable[[TSender, TEvent], None]): 44 | return super().then(callback) 45 | 46 | 47 | class IdBasedEventWaitable(EventWaitable): 48 | """ 49 | Extension of :class:`EventWaitable` for high-churn events which require IDs to ensure the correct operation is waited upon, 50 | such as characteristic read, write and notify operations 51 | """ 52 | def __init__(self, event, event_id): 53 | self.id = event_id 54 | super().__init__(event) 55 | 56 | def _on_event(self, sender, event_args): 57 | if event_args.id == self.id: 58 | super()._on_event(sender, event_args) 59 | -------------------------------------------------------------------------------- /blatann/waitables/scan_waitable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import queue 5 | import threading 6 | from typing import Iterable 7 | 8 | from blatann.gap.advertise_data import ScanReport, ScanReportCollection 9 | from blatann.nrf import nrf_events, nrf_types 10 | from blatann.waitables.waitable import Waitable 11 | 12 | 13 | class ScanFinishedWaitable(Waitable): 14 | """ 15 | Waitable that triggers when a scan operation completes. It also provides a mechanism to acquire the received scan reports 16 | in real-time 17 | """ 18 | def __init__(self, ble_device): 19 | super().__init__() 20 | self.scanner = ble_device.scanner 21 | ble_device.ble_driver.event_subscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) 22 | self.scanner.on_scan_received.register(self._on_scan_report) 23 | self.ble_driver = ble_device.ble_driver 24 | self._scan_report_queue = queue.Queue() 25 | self._event_loop: asyncio.AbstractEventLoop = None 26 | self._lock = threading.Lock() 27 | 28 | @property 29 | def scan_reports(self) -> Iterable[ScanReport]: 30 | """ 31 | Iterable which yields the scan reports in real-time as they're received. 32 | The iterable will block until scanning has timed out/finished 33 | """ 34 | scan_report = self._scan_report_queue.get() 35 | while scan_report: 36 | yield scan_report 37 | scan_report = self._scan_report_queue.get() 38 | 39 | @property 40 | async def scan_reports_async(self) -> Iterable[ScanReport]: 41 | """ 42 | Async iterable which yields the scan reports in real-time as they're received. 43 | The iterable will block until scanning has timed out/finished. 44 | 45 | .. warning:: 46 | This method is experimental! 47 | """ 48 | # Copy any reports received before calling this to the asyncio queue 49 | with self._lock: 50 | existing_queue = self._scan_report_queue 51 | self._scan_report_queue = asyncio.Queue() 52 | self._event_loop = asyncio.get_event_loop() 53 | while True: 54 | try: 55 | item = existing_queue.get_nowait() 56 | self._scan_report_queue.put_nowait(item) 57 | except queue.Empty: 58 | break 59 | 60 | scan_report = await self._scan_report_queue.get() 61 | while scan_report: 62 | yield scan_report 63 | scan_report = await self._scan_report_queue.get() 64 | 65 | def _event_occurred(self, ble_driver): 66 | ble_driver.event_unsubscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) 67 | self.scanner.on_scan_received.deregister(self._on_scan_report) 68 | self._notify(self.scanner.scan_report) 69 | 70 | def _on_timeout(self): 71 | self.ble_driver.event_unsubscribe(self._on_timeout_event, nrf_events.GapEvtTimeout) 72 | self.scanner.on_scan_received.deregister(self._on_scan_report) 73 | 74 | def _add_item(self, scan_report): 75 | with self._lock: 76 | q = self._scan_report_queue 77 | if self._event_loop: 78 | asyncio.run_coroutine_threadsafe(q.put(scan_report), self._event_loop) 79 | else: 80 | q.put(scan_report) 81 | 82 | def _on_scan_report(self, device, scan_report): 83 | self._add_item(scan_report) 84 | 85 | def _on_timeout_event(self, ble_driver, event: nrf_events.GapEvtTimeout): 86 | if event.src == nrf_types.BLEGapTimeoutSrc.scan: 87 | self._event_occurred(ble_driver) 88 | self._add_item(None) 89 | 90 | def wait(self, timeout: float = None, exception_on_timeout: bool = True) -> ScanReportCollection: 91 | """ 92 | Waits for the scanning operation to complete then returns the scan report collection 93 | 94 | :param timeout: How long to wait for, in seconds 95 | :param exception_on_timeout: Flag if to throw an exception if the operation timed out. 96 | If false and a timeout occurs, will return None 97 | :return: The scan report collection 98 | """ 99 | return super().wait(timeout, exception_on_timeout) 100 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | RM := rm 5 | 6 | # You can set these variables from the command line. 7 | SPHINXOPTS = 8 | SPHINXBUILD = sphinx-build 9 | SOURCEDIR = source 10 | BUILDDIR = _build 11 | PYTHON_SOURCE = ../blatann 12 | 13 | .PHONY: help Makefile all apidoc 14 | 15 | # Put it first so that "make" without argument is like "make help". 16 | help: 17 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 18 | 19 | all: apidoc 20 | @$(MAKE) --no-print-directory html 21 | 22 | clean-apidoc: 23 | @$(RM) -rf $(SOURCEDIR)/blatann.*.rst 24 | 25 | apidoc: clean-apidoc 26 | @sphinx-apidoc -e -M -o $(SOURCEDIR) $(PYTHON_SOURCE) 27 | 28 | # Catch-all target: route all unknown targets to Sphinx using the new 29 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 30 | %: Makefile 31 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 32 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-autodoc-typehints~=2.4 2 | pc-ble-driver-py 3 | sphinx-rtd-theme>=3.0.0rc1 4 | sphinx~=8.0 5 | -------------------------------------------------------------------------------- /docs/source/_static/images/blatann_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/docs/source/_static/images/blatann_architecture.png -------------------------------------------------------------------------------- /docs/source/api_reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | modules -------------------------------------------------------------------------------- /docs/source/architecture.rst: -------------------------------------------------------------------------------- 1 | Library Architecture 2 | ==================== 3 | 4 | Class Hierarchy (WIP) 5 | --------------------- 6 | 7 | Very high-level diagram outlining major/public components 8 | 9 | .. figure:: _static/images/blatann_architecture.png 10 | :target: _static/images/blatann_architecture.png 11 | 12 | **BleDevice** 13 | 14 | The :class:`~blatann.device.BleDevice` class represents the Nordic hardware itself and is the primary entry point for the library. 15 | It provides the high-level APIs for all BLE operations, such as advertising, scanning, and connections. 16 | It also manages the device's configuration and bonding database. 17 | 18 | **Advertiser** 19 | 20 | The :class:`~blatann.gap.advertising.Advertiser` class provides the API for 21 | setting advertising data, intervals, and starting/stopping of advertising. 22 | It is accessed via BleDevice.:attr:`~blatann.device.BleDevice.advertiser` attribute. 23 | 24 | **Scanner** 25 | 26 | The :class:`~blatann.gap.scanning.Scanner` class provides the API for scanning for advertising packets. 27 | Scan reports are emitted during scanning and can be used to initiate connections with the advertising peripherals. 28 | It is accessed via BleDevice.:attr:`~blatann.device.BleDevice.scanner` attribute. 29 | 30 | **Peer** 31 | 32 | The :class:`~blatann.peer.Peer` class represents a connection with another device over Bluetooth. The BLE Device 33 | contains a single :class:`~blatann.peer.Client` object, which is the connection with a Client/Central device. 34 | When connecting to Server/Peripheral devices as a Client/Central via :meth:`BleDevice.connect() `, 35 | a :class:`~blatann.peer.Peripheral` object is created and returned as a result of the ``connect()`` call. 36 | 37 | The Peer object provides everything necessary for communications with the device, including security, 38 | database creation (as a server) and discovery (as a client), connection parameters, etc. 39 | 40 | Threading Model 41 | --------------- 42 | 43 | Most BLE operations are inherently asynchronous. The Nordic has a combination of synchronous function calls and asynchronous 44 | events. Synchronous function calls may not return the result immediately and instead return within an asynchronous event 45 | as to not block the main context. 46 | 47 | Blatann uses a second python thread for handling asynchronous events received over BLE. 48 | This event thread (named ``"Thread"``) handles a queue of events received from the C++ Driver 49 | and dispatches them to the registered callbacks. These callbacks are the triggers for the various :class:`~blatann.event_type.Event` 50 | objects that exist throughout the ``Peer``, ``Scanner``, ``Advertiser``, and other objects. 51 | 52 | In order to support both both event-driven and procedural styles of programming, a mechanism needs to exist in order to 53 | communicate events back to the main thread so asynchronous functionality (such as characteristic reads/writes) 54 | can be made synchronous. The result of this is the :class:`~blatann.waitables.waitable.Waitable` class. 55 | 56 | Asynchronous method calls in the library will return a ``Waitable`` object which can either then have callbacks registered (to keep things asynchronous) 57 | or waited on (with or without timeout) from the main thread to make it synchronous. 58 | Additionally, it can be awaited on for use with ``asyncio``. 59 | 60 | This is a very similar concept to 61 | :class:`concurrent.futures.Future `, just a different implementation. 62 | 63 | Since there is only a single thread which handles all events, 64 | **do not call** :meth:`Waitable.wait() ` **within an event handler as it will cause a deadlock.** 65 | Calling BLE methods from the event handler context is perfectly fine and can use 66 | :meth:`Waitable.then(callback) ` to handle the result of the operation asynchronously. 67 | -------------------------------------------------------------------------------- /docs/source/blatann.bt_sig.assigned_numbers.rst: -------------------------------------------------------------------------------- 1 | blatann.bt\_sig.assigned\_numbers module 2 | ======================================== 3 | 4 | .. automodule:: blatann.bt_sig.assigned_numbers 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.bt_sig.rst: -------------------------------------------------------------------------------- 1 | blatann.bt\_sig package 2 | ======================= 3 | 4 | .. automodule:: blatann.bt_sig 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.bt_sig.assigned_numbers 16 | blatann.bt_sig.uuids 17 | -------------------------------------------------------------------------------- /docs/source/blatann.bt_sig.uuids.rst: -------------------------------------------------------------------------------- 1 | blatann.bt\_sig.uuids module 2 | ============================ 3 | 4 | .. automodule:: blatann.bt_sig.uuids 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.device.rst: -------------------------------------------------------------------------------- 1 | blatann.device module 2 | ===================== 3 | 4 | .. automodule:: blatann.device 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.event_args.rst: -------------------------------------------------------------------------------- 1 | blatann.event\_args module 2 | ========================== 3 | 4 | .. automodule:: blatann.event_args 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.event_type.rst: -------------------------------------------------------------------------------- 1 | blatann.event\_type module 2 | ========================== 3 | 4 | .. automodule:: blatann.event_type 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.broadcaster.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.broadcaster module 2 | =================================== 3 | 4 | .. automodule:: blatann.examples.broadcaster 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central module 2 | =============================== 3 | 4 | .. automodule:: blatann.examples.central 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central_async.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central\_async module 2 | ====================================== 3 | 4 | .. automodule:: blatann.examples.central_async 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central_battery_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central\_battery\_service module 2 | ================================================= 3 | 4 | .. automodule:: blatann.examples.central_battery_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central_descriptors.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central\_descriptors module 2 | ============================================ 3 | 4 | .. automodule:: blatann.examples.central_descriptors 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central_device_info_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central\_device\_info\_service module 2 | ====================================================== 3 | 4 | .. automodule:: blatann.examples.central_device_info_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central_event_driven.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central\_event\_driven module 2 | ============================================== 3 | 4 | .. automodule:: blatann.examples.central_event_driven 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.central_uart_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.central\_uart\_service module 2 | ============================================== 3 | 4 | .. automodule:: blatann.examples.central_uart_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.constants.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.constants module 2 | ================================= 3 | 4 | .. automodule:: blatann.examples.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.example_utils.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.example\_utils module 2 | ====================================== 3 | 4 | .. automodule:: blatann.examples.example_utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral module 2 | ================================== 3 | 4 | .. automodule:: blatann.examples.peripheral 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_async.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_async module 2 | ========================================= 3 | 4 | .. automodule:: blatann.examples.peripheral_async 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_battery_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_battery\_service module 2 | ==================================================== 3 | 4 | .. automodule:: blatann.examples.peripheral_battery_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_current_time_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_current\_time\_service module 2 | ========================================================== 3 | 4 | .. automodule:: blatann.examples.peripheral_current_time_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_descriptors.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_descriptors module 2 | =============================================== 3 | 4 | .. automodule:: blatann.examples.peripheral_descriptors 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_device_info_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_device\_info\_service module 2 | ========================================================= 3 | 4 | .. automodule:: blatann.examples.peripheral_device_info_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_glucose_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_glucose\_service module 2 | ==================================================== 3 | 4 | .. automodule:: blatann.examples.peripheral_glucose_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_rssi.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_rssi module 2 | ======================================== 3 | 4 | .. automodule:: blatann.examples.peripheral_rssi 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.peripheral_uart_service.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.peripheral\_uart\_service module 2 | ================================================= 3 | 4 | .. automodule:: blatann.examples.peripheral_uart_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.rst: -------------------------------------------------------------------------------- 1 | blatann.examples package 2 | ======================== 3 | 4 | .. automodule:: blatann.examples 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.examples.broadcaster 16 | blatann.examples.central 17 | blatann.examples.central_async 18 | blatann.examples.central_battery_service 19 | blatann.examples.central_descriptors 20 | blatann.examples.central_device_info_service 21 | blatann.examples.central_event_driven 22 | blatann.examples.central_uart_service 23 | blatann.examples.constants 24 | blatann.examples.example_utils 25 | blatann.examples.peripheral 26 | blatann.examples.peripheral_async 27 | blatann.examples.peripheral_battery_service 28 | blatann.examples.peripheral_current_time_service 29 | blatann.examples.peripheral_descriptors 30 | blatann.examples.peripheral_device_info_service 31 | blatann.examples.peripheral_glucose_service 32 | blatann.examples.peripheral_rssi 33 | blatann.examples.peripheral_uart_service 34 | blatann.examples.scanner 35 | blatann.examples.scanner_async 36 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.scanner.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.scanner module 2 | =============================== 3 | 4 | .. automodule:: blatann.examples.scanner 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.examples.scanner_async.rst: -------------------------------------------------------------------------------- 1 | blatann.examples.scanner\_async module 2 | ====================================== 3 | 4 | .. automodule:: blatann.examples.scanner_async 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.exceptions.rst: -------------------------------------------------------------------------------- 1 | blatann.exceptions module 2 | ========================= 3 | 4 | .. automodule:: blatann.exceptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.advertise_data.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.advertise\_data module 2 | ================================== 3 | 4 | .. automodule:: blatann.gap.advertise_data 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.advertising.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.advertising module 2 | ============================== 3 | 4 | .. automodule:: blatann.gap.advertising 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.bond_db.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.bond\_db module 2 | =========================== 3 | 4 | .. automodule:: blatann.gap.bond_db 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.default_bond_db.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.default\_bond\_db module 2 | ==================================== 3 | 4 | .. automodule:: blatann.gap.default_bond_db 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.gap_types.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.gap\_types module 2 | ============================= 3 | 4 | .. automodule:: blatann.gap.gap_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.generic_access_service.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.generic\_access\_service module 2 | =========================================== 3 | 4 | .. automodule:: blatann.gap.generic_access_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.rst: -------------------------------------------------------------------------------- 1 | blatann.gap package 2 | =================== 3 | 4 | .. automodule:: blatann.gap 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.gap.advertise_data 16 | blatann.gap.advertising 17 | blatann.gap.bond_db 18 | blatann.gap.default_bond_db 19 | blatann.gap.gap_types 20 | blatann.gap.generic_access_service 21 | blatann.gap.scanning 22 | blatann.gap.smp 23 | blatann.gap.smp_crypto 24 | blatann.gap.smp_types 25 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.scanning.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.scanning module 2 | =========================== 3 | 4 | .. automodule:: blatann.gap.scanning 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.smp.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.smp module 2 | ====================== 3 | 4 | .. automodule:: blatann.gap.smp 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.smp_crypto.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.smp\_crypto module 2 | ============================== 3 | 4 | .. automodule:: blatann.gap.smp_crypto 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gap.smp_types.rst: -------------------------------------------------------------------------------- 1 | blatann.gap.smp\_types module 2 | ============================= 3 | 4 | .. automodule:: blatann.gap.smp_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.gattc.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.gattc module 2 | ========================= 3 | 4 | .. automodule:: blatann.gatt.gattc 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.gattc_attribute.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.gattc\_attribute module 2 | ==================================== 3 | 4 | .. automodule:: blatann.gatt.gattc_attribute 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.gatts.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.gatts module 2 | ========================= 3 | 4 | .. automodule:: blatann.gatt.gatts 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.gatts_attribute.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.gatts\_attribute module 2 | ==================================== 3 | 4 | .. automodule:: blatann.gatt.gatts_attribute 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.managers.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.managers module 2 | ============================ 3 | 4 | .. automodule:: blatann.gatt.managers 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.reader.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.reader module 2 | ========================== 3 | 4 | .. automodule:: blatann.gatt.reader 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt package 2 | ==================== 3 | 4 | .. automodule:: blatann.gatt 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.gatt.gattc 16 | blatann.gatt.gattc_attribute 17 | blatann.gatt.gatts 18 | blatann.gatt.gatts_attribute 19 | blatann.gatt.managers 20 | blatann.gatt.reader 21 | blatann.gatt.service_discovery 22 | blatann.gatt.writer 23 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.service_discovery.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.service\_discovery module 2 | ====================================== 3 | 4 | .. automodule:: blatann.gatt.service_discovery 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.gatt.writer.rst: -------------------------------------------------------------------------------- 1 | blatann.gatt.writer module 2 | ========================== 3 | 4 | .. automodule:: blatann.gatt.writer 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_dll_load.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_dll\_load module 2 | ================================= 3 | 4 | .. automodule:: blatann.nrf.nrf_dll_load 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_driver.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_driver module 2 | ============================== 3 | 4 | .. automodule:: blatann.nrf.nrf_driver 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_driver_types.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_driver\_types module 2 | ===================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_driver_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_events.gap_events.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_events.gap\_events module 2 | ========================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_events.gap_events 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_events.gatt_events.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_events.gatt\_events module 2 | =========================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_events.gatt_events 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_events.generic_events.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_events.generic\_events module 2 | ============================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_events.generic_events 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_events.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_events package 2 | =============================== 3 | 4 | .. automodule:: blatann.nrf.nrf_events 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.nrf.nrf_events.gap_events 16 | blatann.nrf.nrf_events.gatt_events 17 | blatann.nrf.nrf_events.generic_events 18 | blatann.nrf.nrf_events.smp_events 19 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_events.smp_events.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_events.smp\_events module 2 | ========================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_events.smp_events 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.config.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types.config module 2 | ==================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_types.config 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.enums.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types.enums module 2 | =================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_types.enums 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.gap.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types.gap module 2 | ================================= 3 | 4 | .. automodule:: blatann.nrf.nrf_types.gap 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.gatt.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types.gatt module 2 | ================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_types.gatt 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.generic.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types.generic module 2 | ===================================== 3 | 4 | .. automodule:: blatann.nrf.nrf_types.generic 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types package 2 | ============================== 3 | 4 | .. automodule:: blatann.nrf.nrf_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.nrf.nrf_types.config 16 | blatann.nrf.nrf_types.enums 17 | blatann.nrf.nrf_types.gap 18 | blatann.nrf.nrf_types.gatt 19 | blatann.nrf.nrf_types.generic 20 | blatann.nrf.nrf_types.smp 21 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.nrf_types.smp.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf.nrf\_types.smp module 2 | ================================= 3 | 4 | .. automodule:: blatann.nrf.nrf_types.smp 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.nrf.rst: -------------------------------------------------------------------------------- 1 | blatann.nrf package 2 | =================== 3 | 4 | .. automodule:: blatann.nrf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.nrf.nrf_events 16 | blatann.nrf.nrf_types 17 | 18 | Submodules 19 | ---------- 20 | 21 | .. toctree:: 22 | :maxdepth: 4 23 | 24 | blatann.nrf.nrf_dll_load 25 | blatann.nrf.nrf_driver 26 | blatann.nrf.nrf_driver_types 27 | -------------------------------------------------------------------------------- /docs/source/blatann.peer.rst: -------------------------------------------------------------------------------- 1 | blatann.peer module 2 | =================== 3 | 4 | .. automodule:: blatann.peer 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.rst: -------------------------------------------------------------------------------- 1 | blatann package 2 | =============== 3 | 4 | .. automodule:: blatann 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.bt_sig 16 | blatann.examples 17 | blatann.gap 18 | blatann.gatt 19 | blatann.nrf 20 | blatann.services 21 | blatann.utils 22 | blatann.waitables 23 | 24 | Submodules 25 | ---------- 26 | 27 | .. toctree:: 28 | :maxdepth: 4 29 | 30 | blatann.device 31 | blatann.event_args 32 | blatann.event_type 33 | blatann.exceptions 34 | blatann.peer 35 | blatann.uuid 36 | -------------------------------------------------------------------------------- /docs/source/blatann.services.battery.constants.rst: -------------------------------------------------------------------------------- 1 | blatann.services.battery.constants module 2 | ========================================= 3 | 4 | .. automodule:: blatann.services.battery.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.battery.data_types.rst: -------------------------------------------------------------------------------- 1 | blatann.services.battery.data\_types module 2 | =========================================== 3 | 4 | .. automodule:: blatann.services.battery.data_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.battery.rst: -------------------------------------------------------------------------------- 1 | blatann.services.battery package 2 | ================================ 3 | 4 | .. automodule:: blatann.services.battery 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.services.battery.constants 16 | blatann.services.battery.data_types 17 | blatann.services.battery.service 18 | -------------------------------------------------------------------------------- /docs/source/blatann.services.battery.service.rst: -------------------------------------------------------------------------------- 1 | blatann.services.battery.service module 2 | ======================================= 3 | 4 | .. automodule:: blatann.services.battery.service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.ble_data_types.rst: -------------------------------------------------------------------------------- 1 | blatann.services.ble\_data\_types module 2 | ======================================== 3 | 4 | .. automodule:: blatann.services.ble_data_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.current_time.constants.rst: -------------------------------------------------------------------------------- 1 | blatann.services.current\_time.constants module 2 | =============================================== 3 | 4 | .. automodule:: blatann.services.current_time.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.current_time.data_types.rst: -------------------------------------------------------------------------------- 1 | blatann.services.current\_time.data\_types module 2 | ================================================= 3 | 4 | .. automodule:: blatann.services.current_time.data_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.current_time.rst: -------------------------------------------------------------------------------- 1 | blatann.services.current\_time package 2 | ====================================== 3 | 4 | .. automodule:: blatann.services.current_time 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.services.current_time.constants 16 | blatann.services.current_time.data_types 17 | blatann.services.current_time.service 18 | -------------------------------------------------------------------------------- /docs/source/blatann.services.current_time.service.rst: -------------------------------------------------------------------------------- 1 | blatann.services.current\_time.service module 2 | ============================================= 3 | 4 | .. automodule:: blatann.services.current_time.service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.decoded_event_dispatcher.rst: -------------------------------------------------------------------------------- 1 | blatann.services.decoded\_event\_dispatcher module 2 | ================================================== 3 | 4 | .. automodule:: blatann.services.decoded_event_dispatcher 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.device_info.constants.rst: -------------------------------------------------------------------------------- 1 | blatann.services.device\_info.constants module 2 | ============================================== 3 | 4 | .. automodule:: blatann.services.device_info.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.device_info.data_types.rst: -------------------------------------------------------------------------------- 1 | blatann.services.device\_info.data\_types module 2 | ================================================ 3 | 4 | .. automodule:: blatann.services.device_info.data_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.device_info.rst: -------------------------------------------------------------------------------- 1 | blatann.services.device\_info package 2 | ===================================== 3 | 4 | .. automodule:: blatann.services.device_info 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.services.device_info.constants 16 | blatann.services.device_info.data_types 17 | blatann.services.device_info.service 18 | -------------------------------------------------------------------------------- /docs/source/blatann.services.device_info.service.rst: -------------------------------------------------------------------------------- 1 | blatann.services.device\_info.service module 2 | ============================================ 3 | 4 | .. automodule:: blatann.services.device_info.service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.glucose.constants.rst: -------------------------------------------------------------------------------- 1 | blatann.services.glucose.constants module 2 | ========================================= 3 | 4 | .. automodule:: blatann.services.glucose.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.glucose.data_types.rst: -------------------------------------------------------------------------------- 1 | blatann.services.glucose.data\_types module 2 | =========================================== 3 | 4 | .. automodule:: blatann.services.glucose.data_types 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.glucose.database.rst: -------------------------------------------------------------------------------- 1 | blatann.services.glucose.database module 2 | ======================================== 3 | 4 | .. automodule:: blatann.services.glucose.database 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.glucose.racp.rst: -------------------------------------------------------------------------------- 1 | blatann.services.glucose.racp module 2 | ==================================== 3 | 4 | .. automodule:: blatann.services.glucose.racp 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.glucose.rst: -------------------------------------------------------------------------------- 1 | blatann.services.glucose package 2 | ================================ 3 | 4 | .. automodule:: blatann.services.glucose 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.services.glucose.constants 16 | blatann.services.glucose.data_types 17 | blatann.services.glucose.database 18 | blatann.services.glucose.racp 19 | blatann.services.glucose.service 20 | -------------------------------------------------------------------------------- /docs/source/blatann.services.glucose.service.rst: -------------------------------------------------------------------------------- 1 | blatann.services.glucose.service module 2 | ======================================= 3 | 4 | .. automodule:: blatann.services.glucose.service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.nordic_uart.constants.rst: -------------------------------------------------------------------------------- 1 | blatann.services.nordic\_uart.constants module 2 | ============================================== 3 | 4 | .. automodule:: blatann.services.nordic_uart.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.nordic_uart.rst: -------------------------------------------------------------------------------- 1 | blatann.services.nordic\_uart package 2 | ===================================== 3 | 4 | .. automodule:: blatann.services.nordic_uart 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.services.nordic_uart.constants 16 | blatann.services.nordic_uart.service 17 | -------------------------------------------------------------------------------- /docs/source/blatann.services.nordic_uart.service.rst: -------------------------------------------------------------------------------- 1 | blatann.services.nordic\_uart.service module 2 | ============================================ 3 | 4 | .. automodule:: blatann.services.nordic_uart.service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.services.rst: -------------------------------------------------------------------------------- 1 | blatann.services package 2 | ======================== 3 | 4 | .. automodule:: blatann.services 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.services.battery 16 | blatann.services.current_time 17 | blatann.services.device_info 18 | blatann.services.glucose 19 | blatann.services.nordic_uart 20 | 21 | Submodules 22 | ---------- 23 | 24 | .. toctree:: 25 | :maxdepth: 4 26 | 27 | blatann.services.ble_data_types 28 | blatann.services.decoded_event_dispatcher 29 | -------------------------------------------------------------------------------- /docs/source/blatann.utils.queued_tasks_manager.rst: -------------------------------------------------------------------------------- 1 | blatann.utils.queued\_tasks\_manager module 2 | =========================================== 3 | 4 | .. automodule:: blatann.utils.queued_tasks_manager 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.utils.rst: -------------------------------------------------------------------------------- 1 | blatann.utils package 2 | ===================== 3 | 4 | .. automodule:: blatann.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.utils.queued_tasks_manager 16 | -------------------------------------------------------------------------------- /docs/source/blatann.uuid.rst: -------------------------------------------------------------------------------- 1 | blatann.uuid module 2 | =================== 3 | 4 | .. automodule:: blatann.uuid 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.waitables.connection_waitable.rst: -------------------------------------------------------------------------------- 1 | blatann.waitables.connection\_waitable module 2 | ============================================= 3 | 4 | .. automodule:: blatann.waitables.connection_waitable 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.waitables.event_queue.rst: -------------------------------------------------------------------------------- 1 | blatann.waitables.event\_queue module 2 | ===================================== 3 | 4 | .. automodule:: blatann.waitables.event_queue 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.waitables.event_waitable.rst: -------------------------------------------------------------------------------- 1 | blatann.waitables.event\_waitable module 2 | ======================================== 3 | 4 | .. automodule:: blatann.waitables.event_waitable 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.waitables.rst: -------------------------------------------------------------------------------- 1 | blatann.waitables package 2 | ========================= 3 | 4 | .. automodule:: blatann.waitables 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | blatann.waitables.connection_waitable 16 | blatann.waitables.event_queue 17 | blatann.waitables.event_waitable 18 | blatann.waitables.scan_waitable 19 | blatann.waitables.waitable 20 | -------------------------------------------------------------------------------- /docs/source/blatann.waitables.scan_waitable.rst: -------------------------------------------------------------------------------- 1 | blatann.waitables.scan\_waitable module 2 | ======================================= 3 | 4 | .. automodule:: blatann.waitables.scan_waitable 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann.waitables.waitable.rst: -------------------------------------------------------------------------------- 1 | blatann.waitables.waitable module 2 | ================================= 3 | 4 | .. automodule:: blatann.waitables.waitable 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/blatann_architecture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | blatann_architecture.html 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/source/compatibility_matrix.rst: -------------------------------------------------------------------------------- 1 | Compatibility Matrix 2 | ==================== 3 | 4 | .. list-table:: Software/Firmware Compatibility Matrix 5 | :header-rows: 1 6 | 7 | * - | Blatann 8 | | Version 9 | - | Python 10 | | Version 11 | - | Connectivity Firmware 12 | | Version 13 | - | SoftDevice 14 | | Version 15 | - | pc-ble-driver-py 16 | | Version 17 | * - v0.2.x 18 | - 2.7 Only 19 | - v1.2.x 20 | - v3 21 | - <= 0.11.4 22 | * - v0.3+ 23 | - 3.7+ 24 | - v4.1.x 25 | - v5 26 | - >= 0.12.0 27 | 28 | Firmware images are shipped within the ``pc-ble-driver-py`` package under the ``hex/`` directory. 29 | Below maps which firmware images to use for which devices. 30 | For Blatann v0.2.x, firmware images are under subdir ``sd_api_v3``. 31 | For Blatann v0.3+, firmware images are under subdir ``sd_api_v5``. 32 | 33 | .. list-table:: Firmware/Hardware Compatibility Matrix 34 | :header-rows: 1 35 | 36 | * - Hardware 37 | - Firmware Image 38 | * - nRF52832 Dev Kit 39 | - connectivity_x.y.z__with_s132_x.y.hex (note the baud rate in use!) 40 | * - nRF52840 Dev Kit 41 | - | connectivity_x.y.z__with_s132_x.y.hex (note the baud rate in use!) **or** 42 | | connectivity_x.y.z_usb_with_s132_x.y.hex if using the USB port on the side 43 | * - nRF52840 USB Dongle 44 | - connectivity_x.y.z_usb_with_s132_x.y.hex 45 | 46 | .. note:: 47 | Blatann provides a default setting for the baud rate to use with the device. 48 | For v0.2.x, the default baud is 115200 whereas for v0.3+ the default is 1M (and USB doesn't care). 49 | This is only an issue when running examples through the command line as it 50 | doesn't expose a setting for the baud rate. When writing your own script, it can be configured however it's needed. 51 | -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | *This section is a work in progress. Still need to add specific sections giving an overview for each example* 5 | 6 | Examples can be found here: `Blatann examples`_ 7 | 8 | .. _Blatann examples: https://github.com/ThomasGerstenberg/blatann/tree/master/blatann/examples 9 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | As of v0.3.0, blatann will only support Python 3.7+. 5 | v0.2.x will be partially maintained for Python 2.7 by backporting issues/bugs found in 0.3.x. 6 | 7 | Introduction 8 | ^^^^^^^^^^^^ 9 | 10 | This library relies on a Nordic nRF52 connected via USB to the PC and flashed with the 11 | Nordic Connectivity firmware in order to operate. 12 | 13 | .. note:: 14 | This library will not work as a driver for any generic Bluetooth HCI USB device nor built-in Bluetooth radios. 15 | The driver is very specific to Nordic nRF52 and the associated Connectivity firmware, 16 | thus other Bluetooth vendors will not work. 17 | 18 | Using the library with an nRF52 device to communicate over BLE with other non-Nordic devices is supported. 19 | 20 | This library has been tested with the following hardware: 21 | 22 | * the `nRF52-DK`_: a Dev Kit for the nRF52832 (PCA10040) 23 | * the `nRF52840-DK`_: a Dev Kit for the nRF52840 (PCA10056) 24 | * the `nRF52840-Dongle`_: a nRF52840 USB Dongle (PCA10059) 25 | * the `ABSniffer-528`_: a nRF52832 USB Dongle 26 | 27 | Install 28 | ^^^^^^^ 29 | 30 | Blatann can be installed through pip: ``pip install blatann`` 31 | 32 | Running with macOS brew python 33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 34 | 35 | ``pc-ble-driver-py`` consists of a shared object which is linked to mac's system python. 36 | In order to use it with brew's python install, you'll need to run ``install_name_tool`` to modify the ``.so`` to 37 | point to brew python instead of system python. 38 | 39 | Example shell script to do so (with more info) can be found here: 40 | `macos script`_ 41 | 42 | Setting up Hardware 43 | ^^^^^^^^^^^^^^^^^^^ 44 | 45 | Once one of the hardware devices above is connected via USB, the Nordic Connectivity firmware can be flashed using 46 | Nordic's `nRF Connect`_ Application. 47 | There are other methods you can use (such as ``nrfutil``), however this is the least-complicated way. Further instructions 48 | for using nRF Connect are out of scope for this as Nordic has great documentation for using their app already. 49 | 50 | The firmware image to use can be found within the installed ``pc-ble-driver-py`` python package under the ``hex/`` directory. 51 | From there, it's a drag and drop operation to get the fimrware image onto the hardware. 52 | 53 | See the :doc:`./compatibility_matrix` which lists what software, firmware, and hardware components work together. 54 | 55 | Smoke Test the Setup 56 | ^^^^^^^^^^^^^^^^^^^^ 57 | 58 | Once the hardware is flashed and Blatann is installed, 59 | the Scanner example can be executed to ensure everything is working. 60 | Blatann's examples can be executed from the command line using 61 | 62 | ``python -m blatann.examples `` 63 | 64 | For the smoke test, use the ``scanner`` example which will stream any advertising packets found for about 4 seconds: 65 | ``python -m blatann.examples scanner `` 66 | 67 | If everything goes well, head on over to :doc:`./examples` to look at the library in action or 68 | visit :doc:`./architecture` to get an overview of the library. 69 | If things do not seem to be working, check out the :doc:`./troubleshooting` page. 70 | 71 | 72 | .. _nRF Connect: https://www.nordicsemi.com/Software-and-tools/Development-Tools/nRF-Connect-for-desktop 73 | .. _macos script: https://github.com/ThomasGerstenberg/blatann/blob/master/tools/macos_retarget_pc_ble_driver_py.sh 74 | .. _nRF52-DK: https://www.nordicsemi.com/Products/Development-hardware/nrf52-dk 75 | .. _nRF52840-DK: https://www.nordicsemi.com/Products/Development-hardware/nRF52840-DK 76 | .. _nRF52840-Dongle: https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle 77 | .. _ABSniffer-528: https://wiki.aprbrother.com/en/ABSniffer_USB_Dongle_528.html 78 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Blatann 2 | ======= 3 | 4 | blåtann: Norwegian word for "blue tooth" 5 | 6 | Blatann aims to provide a high-level, object-oriented interface for interacting 7 | with Bluetooth Low Energy (BLE) devices through Python. It operates using 8 | the Nordic Semiconductor nRF52 through Nordic's ``pc-ble-driver-py`` library 9 | and the associated Connectivity firmware for the device. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | getting_started 16 | core_classes 17 | examples 18 | compatibility_matrix 19 | troubleshooting 20 | architecture 21 | changelog 22 | api_reference 23 | 24 | 25 | Indices and tables 26 | ================== 27 | 28 | * :ref:`genindex` 29 | * :ref:`modindex` 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | blatann 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 6 6 | 7 | blatann 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "blatann" 7 | requires-python = ">=3.7" 8 | authors = [ 9 | {name = "Thomas Gerstenberg", email = "tgerst6@gmail.com"} 10 | ] 11 | description = "Python BLE library for using Nordic nRF52 with Connectivity firmware" 12 | keywords = ["ble", "bluetooth", "nrf52", "nordic"] 13 | readme = "README.md" 14 | license = {text = "BSD 3-Clause"} 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | ] 20 | 21 | dynamic = ["version", "dependencies"] 22 | 23 | [project.optional-dependencies] 24 | dev = [ 25 | "build", 26 | "ruff", 27 | "isort", 28 | ] 29 | 30 | [project.urls] 31 | Documentation = "https://blatann.readthedocs.io/en/latest/" 32 | Repository = "https://github.com/ThomasGerstenberg/blatann" 33 | Changelog = "https://blatann.readthedocs.io/en/latest/changelog.html" 34 | 35 | [tool.setuptools.dynamic] 36 | version = {attr = "blatann.__version__"} 37 | dependencies = {file = "requirements.txt"} 38 | 39 | [tool.ruff] 40 | line-length = 120 41 | target-version = "py37" 42 | 43 | [tool.ruff.lint] 44 | select = ["E", "F", "W", "PLC", "PLW", "PLE", "RUF"] 45 | ignore = [ 46 | "F401", # ignore unused imports globally, revisit this suppression later 47 | "E501", # ignore line lengths. While it's set at 120, it's not a hard limit 48 | "RUF012", # ignore docstring required for mutable class var (revisit later) 49 | "RUF013", # allow implicit optional, e.g. allow 'def my_fun(x: int = None)' 50 | ] 51 | 52 | [tool.ruff.lint.per-file-ignores] 53 | "blatann/nrf/nrf_driver.py" = ["F405"] 54 | "blatann/nrf/nrf_events/__init__.py" = ["F403", "F405"] 55 | "blatann/nrf/nrf_types/__init__.py" = ["F403"] 56 | "blatann/bt_sig/*.py" = ["RUF001"] 57 | 58 | [tool.ruff.format] 59 | quote-style = "double" 60 | indent-style = "space" 61 | 62 | [tool.isort] 63 | py_version = 37 64 | src_paths = ["blatann", "tests"] 65 | 66 | add_imports = ["from __future__ import annotations"] 67 | forced_separate = ["tests"] 68 | 69 | line_length = 120 70 | multi_line_output = 5 # Hanging Grid Grouped 71 | use_parentheses = true 72 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pc-ble-driver-py>=0.13 2 | cryptography 3 | pytz 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/tests/__init__.py -------------------------------------------------------------------------------- /tests/integrated/.gitignore: -------------------------------------------------------------------------------- 1 | # Bonding database used for integrated tests 2 | bond_db*.pkl 3 | bond_db*.json 4 | -------------------------------------------------------------------------------- /tests/integrated/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasGerstenberg/blatann/5b12af897551209e0ef73eb6f9e837a68989ac2a/tests/integrated/__init__.py -------------------------------------------------------------------------------- /tests/integrated/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | 5 | from blatann import BleDevice 6 | from blatann.gap import AdvertisingData 7 | from blatann.gatt.gattc import GattcCharacteristic 8 | from blatann.gatt.gatts import GattsCharacteristic 9 | from blatann.nrf.nrf_types import conn_interval_range 10 | from blatann.peer import Client, ConnectionParameters, Peripheral 11 | from blatann.uuid import generate_random_uuid128 12 | 13 | 14 | class PeriphConn: 15 | def __init__(self): 16 | self.dev: BleDevice = None 17 | self.peer: Client = None 18 | 19 | 20 | class CentralConn: 21 | def __init__(self): 22 | self.dev: BleDevice = None 23 | self.peer: Peripheral = None 24 | 25 | @property 26 | def db(self): 27 | return self.peer.database 28 | 29 | 30 | def setup_connection(periph_conn: PeriphConn, central_conn: CentralConn, 31 | conn_params=None, discover_services=True, preferred_mtu=None, preferred_phy=None): 32 | if conn_params is None: 33 | conn_params = ConnectionParameters(conn_interval_range.min, conn_interval_range.min, 4000) 34 | periph_conn.dev.set_default_peripheral_connection_params(conn_params.max_conn_interval_ms, 35 | conn_params.max_conn_interval_ms, 36 | conn_params.conn_sup_timeout_ms) 37 | periph_conn.dev.advertiser.set_advertise_data(AdvertisingData(flags=0x06, local_name="Blatann Test")) 38 | adv_addr = periph_conn.dev.address 39 | 40 | # Start advertising, then initiate connection from central. 41 | # Once central reports its connected wait for the peripheral to be connected before continuing 42 | waitable = periph_conn.dev.advertiser.start(timeout_sec=30) 43 | central_conn.peer = central_conn.dev.connect(adv_addr, conn_params, preferred_mtu, preferred_phy).wait(10) 44 | 45 | periph_conn.peer = waitable.wait(10) 46 | 47 | if discover_services: 48 | central_conn.peer.discover_services().wait(10) 49 | 50 | 51 | def rand_bytes(n): 52 | return bytes(random.randint(0, 255) for _ in range(n)) 53 | -------------------------------------------------------------------------------- /tests/integrated/test_advertising_duration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import threading 4 | import unittest 5 | 6 | from blatann.gap.advertise_data import AdvertisingData, AdvertisingFlags 7 | from blatann.gap.advertising import AdvertisingMode 8 | from blatann.gap.scanning import Scanner, ScanReport 9 | from blatann.utils import Stopwatch 10 | 11 | from tests.integrated.base import BlatannTestCase, TestParams, long_running 12 | 13 | # TODO: The acceptable duration deltas are generous because the nRF52 dev kits (being UART) are slower 14 | # than the nRF52840 dongles by roughly an order of magnitude (1M baud UART vs. USB-CDC) 15 | 16 | 17 | class TestAdvertisingDuration(BlatannTestCase): 18 | def setUp(self) -> None: 19 | self.adv_interval_ms = 50 20 | self.adv_duration = 5 21 | self.adv_mode = AdvertisingMode.non_connectable_undirected 22 | self.adv_data = AdvertisingData(flags=0x06, local_name="Blatann Test") 23 | 24 | self.dev1.advertiser.set_advertise_data(self.adv_data) 25 | self.dev1.advertiser.set_default_advertise_params(self.adv_interval_ms, self.adv_duration, self.adv_mode) 26 | 27 | def tearDown(self) -> None: 28 | self.dev1.advertiser.stop() 29 | self.dev2.scanner.stop() 30 | 31 | @TestParams([dict(duration=x) for x in [1, 4, 10]], long_running_params= 32 | [dict(duration=x) for x in [120, 180]]) 33 | def test_advertise_duration(self, duration): 34 | acceptable_delta = 0.100 35 | acceptable_delta_scan = 1.000 36 | scan_stopwatch = Stopwatch() 37 | dev1_addr = self.dev1.address 38 | 39 | def on_scan_report(scanner: Scanner, report: ScanReport): 40 | if report.peer_address != dev1_addr: 41 | return 42 | if scan_stopwatch.is_running: 43 | scan_stopwatch.mark() 44 | else: 45 | scan_stopwatch.start() 46 | 47 | self.dev2.scanner.set_default_scan_params(100, 100, duration+2, False) 48 | 49 | with self.dev2.scanner.on_scan_received.register(on_scan_report): 50 | self.dev2.scanner.start_scan() 51 | with Stopwatch() as wait_stopwatch: 52 | self.dev1.advertiser.start(timeout_sec=duration, auto_restart=False).wait(duration + 2) 53 | 54 | self.assertFalse(wait_stopwatch.is_running) 55 | self.assertFalse(self.dev1.advertiser.is_advertising) 56 | 57 | self.assertDeltaWithin(duration, wait_stopwatch.elapsed, acceptable_delta) 58 | self.assertDeltaWithin(duration, scan_stopwatch.elapsed, acceptable_delta_scan) 59 | 60 | @TestParams([dict(duration=x) for x in [1, 2, 4, 10]], long_running_params= 61 | [dict(duration=x) for x in [30, 60]]) 62 | def test_advertise_duration_timeout_event(self, duration): 63 | acceptable_delta = 0.100 64 | on_timeout_event = threading.Event() 65 | 66 | def on_timeout(*args, **kwargs): 67 | on_timeout_event.set() 68 | 69 | with self.dev1.advertiser.on_advertising_timeout.register(on_timeout): 70 | with Stopwatch() as stopwatch: 71 | self.dev1.advertiser.start(timeout_sec=duration, auto_restart=False) 72 | on_timeout_event.wait(duration + 2) 73 | 74 | self.assertTrue(on_timeout_event.is_set()) 75 | self.assertFalse(self.dev1.advertiser.is_advertising) 76 | 77 | self.assertDeltaWithin(duration, stopwatch.elapsed, acceptable_delta) 78 | 79 | def test_advertise_auto_restart(self): 80 | # Scan for longer than the advertising duration, 81 | # but with auto-restart it should advertise for the full scan duration 82 | scan_duration = 10 83 | advertise_duration = 2 84 | acceptable_delta = 0.500 85 | dev1_addr = self.dev1.address 86 | 87 | self.dev2.scanner.set_default_scan_params(100, 100, scan_duration, False) 88 | 89 | w = self.dev2.scanner.start_scan() 90 | self.dev1.advertiser.start(timeout_sec=advertise_duration, auto_restart=True) 91 | w.wait() 92 | 93 | self.dev1.advertiser.stop() 94 | self.dev2.scanner.stop() 95 | 96 | report_timestamps = [r.timestamp for r in self.dev2.scanner.scan_report.all_scan_reports 97 | if r.peer_address == dev1_addr] 98 | self.assertGreater(len(report_timestamps), 0) 99 | 100 | report_seen_duration = report_timestamps[-1] - report_timestamps[0] 101 | 102 | self.assertDeltaWithin(scan_duration, report_seen_duration, acceptable_delta) 103 | 104 | 105 | if __name__ == '__main__': 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /tests/integrated/test_scanner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import threading 4 | import unittest 5 | 6 | from blatann.gap.advertise_data import AdvertisingData, AdvertisingFlags, AdvertisingPacketType 7 | from blatann.gap.advertising import AdvertisingMode 8 | from blatann.gap.scanning import MIN_SCAN_INTERVAL_MS, MIN_SCAN_WINDOW_MS, ScanParameters 9 | from blatann.utils import Stopwatch 10 | from blatann.uuid import Uuid16 11 | 12 | from tests.integrated.base import BlatannTestCase, TestParams, long_running 13 | 14 | 15 | class TestScanner(BlatannTestCase): 16 | def setUp(self) -> None: 17 | self.adv_interval_ms = 20 18 | self.adv_mac_addr = self.dev1.address 19 | self.adv_mode = AdvertisingMode.scanable_undirected 20 | self.scan_params = ScanParameters(MIN_SCAN_INTERVAL_MS, MIN_SCAN_WINDOW_MS, 4) 21 | self.flags = AdvertisingFlags.GENERAL_DISCOVERY_MODE | AdvertisingFlags.BR_EDR_NOT_SUPPORTED 22 | self.uuid16s = [Uuid16(0xABCD), Uuid16(0xDEF0)] 23 | self.default_adv_data = AdvertisingData(flags=self.flags, local_name="Blatann Test") 24 | self.default_adv_data_bytes = self.default_adv_data.to_bytes() 25 | self.default_scan_data = AdvertisingData(service_uuid16s=self.uuid16s) 26 | self.default_scan_data_bytes = self.default_scan_data.to_bytes() 27 | 28 | def tearDown(self) -> None: 29 | self.dev1.advertiser.stop() 30 | self.dev2.scanner.stop() 31 | 32 | def _get_packets_for_adv(self, results): 33 | all_packets = [p for p in results.all_scan_reports if p.peer_address == self.adv_mac_addr] 34 | adv_packets = [p for p in all_packets if p.packet_type == self.adv_mode] 35 | scan_response_packets = [p for p in all_packets if p.packet_type == AdvertisingPacketType.scan_response] 36 | return all_packets, adv_packets, scan_response_packets 37 | 38 | @TestParams([dict(duration=x) for x in [1, 2, 4, 10]], long_running_params= 39 | [dict(duration=x) for x in [60, 120]]) 40 | def test_scan_duration(self, duration): 41 | acceptable_delta = 0.100 42 | on_timeout_event = threading.Event() 43 | self.scan_params.timeout_s = duration 44 | 45 | self.dev1.advertiser.start(self.adv_interval_ms, duration+2) 46 | 47 | def on_timeout(*args, **kwargs): 48 | on_timeout_event.set() 49 | 50 | with self.dev2.scanner.on_scan_timeout.register(on_timeout): 51 | with Stopwatch() as stopwatch: 52 | self.dev2.scanner.start_scan(self.scan_params) 53 | on_timeout_event.wait(duration + 2) 54 | 55 | self.assertTrue(on_timeout_event.is_set()) 56 | self.assertFalse(self.dev2.scanner.is_scanning) 57 | 58 | actual_delta = abs(duration - stopwatch.elapsed) 59 | self.assertLessEqual(actual_delta, acceptable_delta) 60 | self.logger.info("Delta: {:.3f}".format(actual_delta)) 61 | 62 | def test_scan_iterator(self): 63 | acceptable_delta = 0.100 64 | self.scan_params.timeout_s = 5 65 | 66 | self.dev1.advertiser.start(self.adv_interval_ms, self.scan_params.timeout_s+2) 67 | 68 | adv_address = self.dev1.address 69 | report_count_from_advertiser = 0 70 | with Stopwatch() as stopwatch: 71 | for report in self.dev2.scanner.start_scan(self.scan_params).scan_reports: 72 | if report.peer_address == adv_address: 73 | report_count_from_advertiser += 1 74 | 75 | self.assertGreater(report_count_from_advertiser, 0) 76 | self.assertDeltaWithin(self.scan_params.timeout_s, stopwatch.elapsed, acceptable_delta) 77 | 78 | def test_non_active_scanning_no_scan_response_packets_received(self): 79 | self.dev1.advertiser.set_advertise_data(self.default_adv_data, self.default_scan_data) 80 | self.dev1.advertiser.start(advertise_mode=self.adv_mode) 81 | self.scan_params.active = False 82 | results = self.dev2.scanner.start_scan(self.scan_params).wait(10) 83 | 84 | # Get the list of all advertising packets from the advertiser 85 | all_packets, adv_packets, scan_response_packets = self._get_packets_for_adv(results) 86 | self.assertGreater(len(all_packets), 0) 87 | self.assertEqual(len(all_packets), len(adv_packets)) 88 | self.assertEqual(0, len(scan_response_packets)) 89 | 90 | for p in adv_packets: 91 | self.assertEqual(self.default_adv_data_bytes, p.raw_bytes) 92 | 93 | 94 | if __name__ == '__main__': 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /tools/macos_retarget_pc_ble_driver_py.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script to retarget the pc-ble-driver-py dependencies to a different python install. 4 | # The shared object in the library is linked against the system-installed python, which is not 5 | # ideal since people commonly use brew-installed python instead (or anaconda). 6 | # 7 | # This script should be run within the python environment you want to use, 8 | # and blatann + pc-ble-driver-py should already be pip-installed into the environment. 9 | # This script also need the "Command Line Tools for Xcode" to use xcrun command 10 | # You can find it here: https://developer.apple.com/download/all/?q=command%20line%20tools 11 | # 12 | # NOTE: This will need to be run any time pc-ble-driver-py is installed or updated, even virtual environments. 13 | # Easy test to see if it needs to be run: `import blatann` will fail 14 | 15 | # Python executable to use. Change this out if something other than python3 should be used 16 | PYTHON=python3 17 | 18 | # Get the location 19 | PYTHON_LOC=`which $PYTHON` 20 | 21 | if [[ -z $PYTHON_LOC ]] 22 | then 23 | echo "ERROR: Unable to find python location" 24 | exit -1 25 | fi 26 | 27 | echo "targeting python @ $PYTHON_LOC" 28 | echo 29 | 30 | # Ensure blatann and pc-ble-driver-py are already installed 31 | BLATANN=`$PYTHON -m pip list | grep blatann` 32 | PC_BLE_DRIVER_PY=`$PYTHON -m pip list | grep "pc_ble_driver_py\|pc-ble-driver-py"` 33 | 34 | if [[ -z $BLATANN ]] 35 | then 36 | echo "ERROR: unable to find blatann install. Run 'pip install blatann' prior to running this script" 37 | exit -1 38 | fi 39 | 40 | echo "found lib: $BLATANN" 41 | 42 | if [[ -z $PC_BLE_DRIVER_PY ]] 43 | then 44 | echo "ERROR: unable to find pc-ble-driver-py install. Run 'pip install pc-ble-driver-py' prior to running this script" 45 | exit -1 46 | fi 47 | 48 | echo "found lib: $PC_BLE_DRIVER_PY" 49 | 50 | # Get the location of pc-ble-driver-py by importing the lib and printing out its directory 51 | PC_BLE_DRIVER_LOC=`$PYTHON -c "import os, pc_ble_driver_py; print(os.path.dirname(pc_ble_driver_py.__file__))"` 52 | # For the python2.7 version change the path to 'lib/macos_osx/_pc_ble_driver_sd_api_v3.so' 53 | PC_BLE_DRIVER_LIB=$PC_BLE_DRIVER_LOC/lib/_nrf_ble_driver_sd_api_v5.so 54 | 55 | echo "pc-ble-driver location: $PC_BLE_DRIVER_LOC" 56 | echo "library to patch: $PC_BLE_DRIVER_LIB" 57 | echo 58 | 59 | # Determine the location of the Python libraries for the target python and currently-linked python. 60 | # Output of otool -L is: 61 | # path/to/lib 62 | # path/to/dependency1 (version compat info) 63 | # path/to/dependency2 (version compat info) 64 | # ... 65 | # 66 | # We need to find the Python.framework dependency so we can switch it out using install_name_tool 67 | # 68 | # Command breakdown: 69 | # otool -L : Print out library dependencies (above) 70 | # tail -n +2 : Skip the first line (path of the library itself), in case it also includes 'Python.framework' 71 | # grep : Find the entry with the path to the Python framework dependency 72 | # cut : Cut on spaces and take only the first part (the path) 73 | # xargs : strip off whitespace from the otool output 74 | TARGET_LIB=`otool -L $PYTHON_LOC | tail -n +2 | grep Python.framework | cut -d " " -f 1 | xargs` 75 | CURRENT_LIB=`otool -L $PC_BLE_DRIVER_LIB | tail -n +2 | grep Python.framework | cut -d " " -f 1 | xargs` 76 | 77 | # Sanity-check both paths are found 78 | if [[ -z $TARGET_LIB || -z $CURRENT_LIB ]] 79 | then 80 | echo "ERROR: unable to find paths in otool!" 81 | exit -1 82 | fi 83 | 84 | echo "re-linking library" 85 | echo "old: $CURRENT_LIB" 86 | echo "new: $TARGET_LIB" 87 | echo 88 | 89 | # Change out the current lib with the target 90 | install_name_tool -change $CURRENT_LIB $TARGET_LIB $PC_BLE_DRIVER_LIB 91 | 92 | # Verify the change worked 93 | echo "testing that blatann can be imported..." 94 | if $PYTHON -c "import blatann" 95 | then 96 | echo "Success!" 97 | else 98 | echo "Re-target failed!" 99 | exit -1 100 | fi 101 | --------------------------------------------------------------------------------