Skip to content

Commit 83ac70e

Browse files
authored
Merge pull request #619 from zxzxwu/cs
Channel Sounding
2 parents 079cf6b + 745e107 commit 83ac70e

8 files changed

Lines changed: 640 additions & 9 deletions

File tree

bumble/device.py

Lines changed: 416 additions & 2 deletions
Large diffs are not rendered by default.

bumble/hci.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,27 @@ class CsSnr(OpenIntEnum):
791791
NOT_APPLIED = 0xFF
792792

793793

794+
class CsDoneStatus(OpenIntEnum):
795+
ALL_RESULTS_COMPLETED = 0x00
796+
PARTIAL = 0x01
797+
ABORTED = 0x0F
798+
799+
800+
class CsProcedureAbortReason(OpenIntEnum):
801+
NO_ABORT = 0x00
802+
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
803+
CHANNEL_MAP_UPDATE_INSTANT_PASSED = 0x02
804+
UNSPECIFIED = 0x0F
805+
806+
807+
class CsSubeventAbortReason(OpenIntEnum):
808+
NO_ABORT = 0x00
809+
LOCAL_HOST_OR_REMOTE_REQUEST = 0x01
810+
NO_CS_SYNC_RECEIVED = 0x02
811+
SCHEDULING_CONFLICT_OR_LIMITED_RESOURCES = 0x03
812+
UNSPECIFIED = 0x0F
813+
814+
794815
# Connection Parameters
795816
HCI_CONNECTION_INTERVAL_MS_PER_UNIT = 1.25
796817
HCI_CONNECTION_LATENCY_MS_PER_UNIT = 1.25
@@ -6506,7 +6527,7 @@ class Action(OpenIntEnum):
65066527
('config_id', 1),
65076528
('state', 1),
65086529
('tone_antenna_config_selection', 1),
6509-
('selected_tx_power', 1),
6530+
('selected_tx_power', -1),
65106531
('subevent_len', 3),
65116532
('subevents_per_event', 1),
65126533
('subevent_interval', 2),
@@ -6548,7 +6569,7 @@ class State(OpenIntEnum):
65486569
('start_acl_conn_event_counter', 2),
65496570
('procedure_counter', 2),
65506571
('frequency_compensation', 2),
6551-
('reference_power_level', 1),
6572+
('reference_power_level', -1),
65526573
('procedure_done_status', 1),
65536574
('subevent_done_status', 1),
65546575
('abort_reason', 1),
@@ -6565,7 +6586,7 @@ class HCI_LE_CS_Subevent_Result_Event(HCI_LE_Meta_Event):
65656586
See Bluetooth spec @ 7.7.65.44 LE CS Subevent Result event
65666587
'''
65676588

6568-
status: int
6589+
connection_handle: int
65696590
config_id: int
65706591
start_acl_conn_event_counter: int
65716592
procedure_counter: int
@@ -6601,7 +6622,7 @@ class HCI_LE_CS_Subevent_Result_Continue_Event(HCI_LE_Meta_Event):
66016622
See Bluetooth spec @ 7.7.65.45 LE CS Subevent Result Continue event
66026623
'''
66036624

6604-
status: int
6625+
connection_handle: int
66056626
config_id: int
66066627
procedure_done_status: int
66076628
subevent_done_status: int

bumble/host.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,12 @@ async def reset(self, driver_factory=drivers.get_driver_for_host):
476476
hci.HCI_LE_TRANSMIT_POWER_REPORTING_EVENT,
477477
hci.HCI_LE_BIGINFO_ADVERTISING_REPORT_EVENT,
478478
hci.HCI_LE_SUBRATE_CHANGE_EVENT,
479+
hci.HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT,
480+
hci.HCI_LE_CS_PROCEDURE_ENABLE_COMPLETE_EVENT,
481+
hci.HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT,
482+
hci.HCI_LE_CS_CONFIG_COMPLETE_EVENT,
483+
hci.HCI_LE_CS_SUBEVENT_RESULT_EVENT,
484+
hci.HCI_LE_CS_SUBEVENT_RESULT_CONTINUE_EVENT,
479485
]
480486
)
481487

@@ -1495,5 +1501,23 @@ def on_hci_le_read_remote_features_complete_event(self, event):
14951501
int.from_bytes(event.le_features, 'little'),
14961502
)
14971503

1504+
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
1505+
self.emit('cs_remote_supported_capabilities', event)
1506+
1507+
def on_hci_le_cs_security_enable_complete_event(self, event):
1508+
self.emit('cs_security', event)
1509+
1510+
def on_hci_le_cs_config_complete_event(self, event):
1511+
self.emit('cs_config', event)
1512+
1513+
def on_hci_le_cs_procedure_enable_complete_event(self, event):
1514+
self.emit('cs_procedure', event)
1515+
1516+
def on_hci_le_cs_subevent_result_event(self, event):
1517+
self.emit('cs_subevent_result', event)
1518+
1519+
def on_hci_le_cs_subevent_result_continue_event(self, event):
1520+
self.emit('cs_subevent_result_continue', event)
1521+
14981522
def on_hci_vendor_event(self, event):
14991523
self.emit('vendor_event', event)

bumble/smp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,7 @@ def on_peer_key_distribution_complete(self) -> None:
13261326
self.connection.abort_on('disconnection', self.on_pairing())
13271327

13281328
def on_connection_encryption_change(self) -> None:
1329-
if self.connection.is_encrypted:
1329+
if self.connection.is_encrypted and not self.completed:
13301330
if self.is_responder:
13311331
# The responder distributes its keys first, the initiator later
13321332
self.distribute_keys()

bumble/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ def deprecated(msg: str):
447447
def wrapper(function):
448448
@functools.wraps(function)
449449
def inner(*args, **kwargs):
450-
warnings.warn(msg, DeprecationWarning)
450+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
451451
return function(*args, **kwargs)
452452

453453
return inner
@@ -464,7 +464,7 @@ def experimental(msg: str):
464464
def wrapper(function):
465465
@functools.wraps(function)
466466
def inner(*args, **kwargs):
467-
warnings.warn(msg, FutureWarning)
467+
warnings.warn(msg, FutureWarning, stacklevel=2)
468468
return function(*args, **kwargs)
469469

470470
return inner

examples/cs_initiator.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "Bumble CS Initiator",
3+
"address": "F0:F1:F2:F3:F4:F5",
4+
"advertising_interval": 100,
5+
"keystore": "JsonKeyStore",
6+
"irk": "865F81FF5A8B486EAAE29A27AD9F77DC",
7+
"identity_address_type": 1,
8+
"channel_sounding_enabled": true
9+
}

examples/cs_reflector.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "Bumble CS Reflector",
3+
"address": "F0:F1:F2:F3:F4:F6",
4+
"advertising_interval": 100,
5+
"keystore": "JsonKeyStore",
6+
"irk": "0c7d74db03a1c98e7be691f76141d53d",
7+
"identity_address_type": 1,
8+
"channel_sounding_enabled": true
9+
}

examples/run_channel_sounding.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# -----------------------------------------------------------------------------
16+
# Imports
17+
# -----------------------------------------------------------------------------
18+
from __future__ import annotations
19+
20+
import asyncio
21+
import logging
22+
import sys
23+
import os
24+
import functools
25+
26+
from bumble import core
27+
from bumble import hci
28+
from bumble.device import Connection, Device, ChannelSoundingCapabilities
29+
from bumble.transport import open_transport_or_link
30+
31+
# From https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/system/gd/hci/distance_measurement_manager.cc.
32+
CS_TONE_ANTENNA_CONFIG_MAPPING_TABLE = [
33+
[0, 4, 5, 6],
34+
[1, 7, 7, 7],
35+
[2, 7, 7, 7],
36+
[3, 7, 7, 7],
37+
]
38+
CS_PREFERRED_PEER_ANTENNA_MAPPING_TABLE = [1, 1, 1, 1, 3, 7, 15, 3]
39+
CS_ANTENNA_PERMUTATION_ARRAY = [
40+
[1, 2, 3, 4],
41+
[2, 1, 3, 4],
42+
[1, 3, 2, 4],
43+
[3, 1, 2, 4],
44+
[3, 2, 1, 4],
45+
[2, 3, 1, 4],
46+
[1, 2, 4, 3],
47+
[2, 1, 4, 3],
48+
[1, 4, 2, 3],
49+
[4, 1, 2, 3],
50+
[4, 2, 1, 3],
51+
[2, 4, 1, 3],
52+
[1, 4, 3, 2],
53+
[4, 1, 3, 2],
54+
[1, 3, 4, 2],
55+
[3, 1, 4, 2],
56+
[3, 4, 1, 2],
57+
[4, 3, 1, 2],
58+
[4, 2, 3, 1],
59+
[2, 4, 3, 1],
60+
[4, 3, 2, 1],
61+
[3, 4, 2, 1],
62+
[3, 2, 4, 1],
63+
[2, 3, 4, 1],
64+
]
65+
66+
67+
# -----------------------------------------------------------------------------
68+
async def main() -> None:
69+
if len(sys.argv) < 3:
70+
print(
71+
'Usage: run_channel_sounding.py <config-file> <transport-spec-for-device>'
72+
'[target_address](If missing, run as reflector)'
73+
)
74+
print('example: run_channel_sounding.py cs_reflector.json usb:0')
75+
print(
76+
'example: run_channel_sounding.py cs_initiator.json usb:0 F0:F1:F2:F3:F4:F5'
77+
)
78+
return
79+
80+
print('<<< connecting to HCI...')
81+
async with await open_transport_or_link(sys.argv[2]) as hci_transport:
82+
print('<<< connected')
83+
84+
device = Device.from_config_file_with_hci(
85+
sys.argv[1], hci_transport.source, hci_transport.sink
86+
)
87+
await device.power_on()
88+
assert (local_cs_capabilities := device.cs_capabilities)
89+
90+
if len(sys.argv) == 3:
91+
print('<<< Start Advertising')
92+
await device.start_advertising(
93+
own_address_type=hci.OwnAddressType.RANDOM, auto_restart=True
94+
)
95+
96+
def on_cs_capabilities(
97+
connection: Connection, capabilities: ChannelSoundingCapabilities
98+
):
99+
del capabilities
100+
print('<<< Set CS Settings')
101+
asyncio.create_task(device.set_default_cs_settings(connection))
102+
103+
device.on(
104+
'connection',
105+
lambda connection: connection.on(
106+
'channel_sounding_capabilities',
107+
functools.partial(on_cs_capabilities, connection),
108+
),
109+
)
110+
else:
111+
target_address = hci.Address(sys.argv[3])
112+
113+
print(f'<<< Connecting to {target_address}')
114+
connection = await device.connect(
115+
target_address, transport=core.BT_LE_TRANSPORT
116+
)
117+
print('<<< ACL Connected')
118+
if not (await device.get_long_term_key(connection.handle, b'', 0)):
119+
print('<<< No bond, start pairing')
120+
await connection.pair()
121+
print('<<< Pairing complete')
122+
123+
print('<<< Encrypting Connection')
124+
await connection.encrypt()
125+
126+
print('<<< Getting remote CS Capabilities...')
127+
remote_capabilities = await device.get_remote_cs_capabilities(connection)
128+
print('<<< Set CS Settings...')
129+
await device.set_default_cs_settings(connection)
130+
print('<<< Set CS Config...')
131+
config = await device.create_cs_config(connection)
132+
print('<<< Enable CS Security...')
133+
await device.enable_cs_security(connection)
134+
tone_antenna_config_selection = CS_TONE_ANTENNA_CONFIG_MAPPING_TABLE[
135+
local_cs_capabilities.num_antennas_supported - 1
136+
][remote_capabilities.num_antennas_supported - 1]
137+
print('<<< Set CS Procedure Parameters...')
138+
await device.set_cs_procedure_parameters(
139+
connection=connection,
140+
config=config,
141+
tone_antenna_config_selection=tone_antenna_config_selection,
142+
preferred_peer_antenna=CS_PREFERRED_PEER_ANTENNA_MAPPING_TABLE[
143+
tone_antenna_config_selection
144+
],
145+
)
146+
print('<<< Enable CS Procedure...')
147+
await device.enable_cs_procedure(connection=connection, config=config)
148+
149+
await hci_transport.source.terminated
150+
151+
152+
# -----------------------------------------------------------------------------
153+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
154+
asyncio.run(main())

0 commit comments

Comments
 (0)