├── .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 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------