forked from auracaster/bumble_mirror
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bbb37b2da | |||
| 3fa5d320de | |||
| 16d684c199 | |||
| c28aa2ebb6 | |||
| 28586382f4 | |||
| 76f08977c4 | |||
| 15cbf52da4 | |||
| f4f84dffef | |||
| 6dfb07d7b9 | |||
| d7ce62beaa | |||
| 0e2a184edb | |||
| e6ee5ae996 | |||
| f1836e659f | |||
| 99218d3abf | |||
| b5ba0bef63 |
@@ -16,7 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v3
|
||||
- name: Get history and tags for SCM versioning to work
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v3
|
||||
- name: Get history and tags for SCM versioning to work
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package to PyPI
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
@@ -21,6 +21,29 @@ or see the documentation source under `docs/mkdocs/src`, or build the static HTM
|
||||
mkdocs build -f docs/mkdocs/mkdocs.yml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Getting Started
|
||||
|
||||
For a quick start to using Bumble, see the [Getting Started](docs/mkdocs/src/getting_started.md) guide.
|
||||
|
||||
### Dependencies
|
||||
|
||||
To install package dependencies needed to run the bumble examples execute the following commands:
|
||||
|
||||
```
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ".[test,development,documentation]"
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Refer to the [Example Documentation](examples/README.md) for details on the included example scripts and how to run them.
|
||||
|
||||
The complete [list of Examples](/docs/mkdocs/src/examples/index.md), and what they are designed to do is here.
|
||||
|
||||
There are also a set of [Apps and Tools](docs/mkdocs/src/apps_and_tools/index.md) that show the utility of Bumble.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [Apache 2.0](LICENSE) License.
|
||||
|
||||
+54
-18
@@ -32,6 +32,7 @@ from bumble.core import UUID, AdvertisingData
|
||||
from bumble.device import Device, Connection, Peer
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.gatt import Characteristic
|
||||
|
||||
from prompt_toolkit import Application
|
||||
from prompt_toolkit.history import FileHistory
|
||||
@@ -330,9 +331,24 @@ class ConsoleApp:
|
||||
|
||||
await self.show_attributes(attributes)
|
||||
|
||||
def find_attribute(self, param):
|
||||
parts = param.split('.')
|
||||
if len(parts) == 2:
|
||||
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
||||
characteristic_uuid = UUID(parts[1])
|
||||
for service in self.connected_peer.services:
|
||||
if service_uuid is None or service.uuid == service_uuid:
|
||||
for characteristic in service.characteristics:
|
||||
if characteristic.uuid == characteristic_uuid:
|
||||
return characteristic
|
||||
elif len(parts) == 1:
|
||||
if parts[0].startswith('#'):
|
||||
attribute_handle = int(f'{parts[0][1:]}', 16)
|
||||
return attribute_handle
|
||||
|
||||
async def command(self, command):
|
||||
try:
|
||||
(keyword, *params) = command.strip().split(' ', 1)
|
||||
(keyword, *params) = command.strip().split(' ')
|
||||
keyword = keyword.replace('-', '_').lower()
|
||||
handler = getattr(self, f'do_{keyword}', None)
|
||||
if handler:
|
||||
@@ -441,26 +457,46 @@ class ConsoleApp:
|
||||
self.show_error('invalid syntax', 'expected read <attribute>')
|
||||
return
|
||||
|
||||
parts = params[0].split('.')
|
||||
if len(parts) == 2:
|
||||
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
||||
characteristic_uuid = UUID(parts[1])
|
||||
for service in self.connected_peer.services:
|
||||
if service_uuid is None or service.uuid == service_uuid:
|
||||
for characteristic in service.characteristics:
|
||||
if characteristic.uuid == characteristic_uuid:
|
||||
value = await self.connected_peer.read_value(characteristic)
|
||||
self.append_to_output(f'VALUE: {value}')
|
||||
return
|
||||
attribute = self.find_attribute(params[0])
|
||||
if attribute is None:
|
||||
self.show_error('no such characteristic')
|
||||
elif len(parts) == 1:
|
||||
if parts[0].startswith('#'):
|
||||
attribute_handle = int(f'{parts[0][1:]}', 16)
|
||||
value = await self.connected_peer.read_value(attribute_handle)
|
||||
self.append_to_output(f'VALUE: {value}')
|
||||
return
|
||||
return
|
||||
|
||||
value = await self.connected_peer.read_value(attribute)
|
||||
self.append_to_output(f'VALUE: {value}')
|
||||
|
||||
async def do_write(self, params):
|
||||
if not self.connected_peer:
|
||||
self.show_error('not connected')
|
||||
return
|
||||
|
||||
if len(params) != 2:
|
||||
self.show_error('invalid syntax', 'expected write <attribute> <value>')
|
||||
return
|
||||
|
||||
if params[1].upper().startswith("0X"):
|
||||
value = bytes.fromhex(params[1][2:]) # parse as hex string
|
||||
else:
|
||||
try:
|
||||
value = int(params[1]) # try as integer
|
||||
except ValueError:
|
||||
value = str.encode(params[1]) # must be a string
|
||||
|
||||
attribute = self.find_attribute(params[0])
|
||||
if attribute is None:
|
||||
self.show_error('no such characteristic')
|
||||
return
|
||||
|
||||
# use write with response if supported
|
||||
with_response = (
|
||||
(attribute.properties & Characteristic.WRITE)
|
||||
if hasattr(attribute, "properties")
|
||||
else False
|
||||
)
|
||||
|
||||
await self.connected_peer.write_value(
|
||||
attribute, value, with_response=with_response
|
||||
)
|
||||
|
||||
async def do_exit(self, params):
|
||||
self.ui.exit()
|
||||
|
||||
@@ -320,6 +320,12 @@ class CharacteristicAdapter:
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.wrapped_characteristic, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in {'wrapped_characteristic', 'read_value', 'write_value', 'subscribe'}:
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
setattr(self.wrapped_characteristic, name, value)
|
||||
|
||||
def read_encoded_value(self, connection):
|
||||
return self.encode_value(self.wrapped_characteristic.read_value(connection))
|
||||
|
||||
@@ -343,6 +349,10 @@ class CharacteristicAdapter:
|
||||
None if subscriber is None else lambda value: subscriber(self.decode_value(value))
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
wrapped = str(self.wrapped_characteristic)
|
||||
return f'{self.__class__.__name__}({wrapped})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DelegatedCharacteristicAdapter(CharacteristicAdapter):
|
||||
|
||||
@@ -545,13 +545,13 @@ class Server(EventEmitter):
|
||||
value = attribute.read_value(connection)
|
||||
if request.value_offset > len(value):
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_INVALID_OFFSET_ERROR
|
||||
)
|
||||
elif len(value) <= mtu - 1:
|
||||
response = ATT_Error_Response(
|
||||
request_opcode_in_error = request.op_code,
|
||||
request_opcode_in_error = request.op_code,
|
||||
attribute_handle_in_error = request.attribute_handle,
|
||||
error_code = ATT_ATTRIBUTE_NOT_LONG_ERROR
|
||||
)
|
||||
|
||||
@@ -3276,6 +3276,9 @@ class HCI_Event(HCI_Packet):
|
||||
parameters = b'' if self.parameters is None else self.parameters
|
||||
return bytes([HCI_EVENT_PACKET, self.event_code, len(parameters)]) + parameters
|
||||
|
||||
def __bytes__(self):
|
||||
return self.to_bytes()
|
||||
|
||||
def __str__(self):
|
||||
result = color(self.name, 'magenta')
|
||||
if fields := getattr(self, 'fields', None):
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
name = bumble
|
||||
use_scm_version = True
|
||||
description = Bluetooth Stack for Apps, Emulation, Test and Experimentation
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
author = Google
|
||||
author_email = tbd@tbd.com
|
||||
url = https://github.com/google/bumble
|
||||
|
||||
+40
-1
@@ -163,6 +163,44 @@ async def test_self_gatt():
|
||||
assert(result == c1.value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_gatt_long_read():
|
||||
# Create two devices, each with a controller, attached to the same link
|
||||
two_devices = TwoDevices()
|
||||
|
||||
# Add some GATT characteristics to device 1
|
||||
characteristics = [
|
||||
Characteristic(
|
||||
f'3A143AD7-D4A7-436B-97D6-5B62C315{i:04X}',
|
||||
Characteristic.READ,
|
||||
Characteristic.READABLE,
|
||||
bytes([x & 255 for x in range(i)])
|
||||
)
|
||||
for i in range(0, 513)
|
||||
]
|
||||
|
||||
service = Service('8140E247-04F0-42C1-BC34-534C344DAFCA', characteristics)
|
||||
two_devices.devices[1].add_service(service)
|
||||
|
||||
# Start
|
||||
await two_devices.devices[0].power_on()
|
||||
await two_devices.devices[1].power_on()
|
||||
|
||||
# Connect the two devices
|
||||
connection = await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
||||
peer = Peer(connection)
|
||||
|
||||
result = await peer.discover_service(service.uuid)
|
||||
assert(len(result) == 1)
|
||||
found_service = result[0]
|
||||
found_characteristics = await found_service.discover_characteristics()
|
||||
assert(len(found_characteristics) == 513)
|
||||
for (i, characteristic) in enumerate(found_characteristics):
|
||||
value = await characteristic.read_value()
|
||||
assert(value == characteristics[i].value)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
||||
# Create two devices, each with a controller, attached to the same link
|
||||
@@ -274,7 +312,7 @@ async def test_self_smp(io_cap, sc, mitm, key_dist):
|
||||
pairing_config2.delegate.peer_delegate = pairing_config1.delegate
|
||||
|
||||
await _test_self_smp_with_configs(pairing_config1, pairing_config2)
|
||||
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -323,6 +361,7 @@ async def test_self_smp_wrong_pin():
|
||||
async def run_test_self():
|
||||
await test_self_connection()
|
||||
await test_self_gatt()
|
||||
await test_self_gatt_long_read()
|
||||
await test_self_smp()
|
||||
await test_self_smp_reject()
|
||||
await test_self_smp_wrong_pin()
|
||||
|
||||
Reference in New Issue
Block a user