Skip to content

Commit e3f3cfc

Browse files
Address PR feedback
1 parent e521e68 commit e3f3cfc

File tree

8 files changed

+170
-126
lines changed

8 files changed

+170
-126
lines changed

rust/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Next
2+
3+
- GATT server support
4+
- Tests w/ rootcanal
5+
- Battery service example
6+
- Address is now pure Rust that's convertible to its Python equivalent rather than just holding a PyObject
7+
18
# 0.2.0
29

310
- Code-gen company ID table

rust/examples/battery_service.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@
3131
//! client \
3232
//! --target-addr F0:F1:F2:F3:F4:F5
3333
//! ```
34+
//!
35+
//! Any combo will work, e.g. a Rust server and Python client:
36+
//!
37+
//! ```
38+
//! PYTHONPATH=..:/path/to/virtualenv/site-packages/ \
39+
//! cargo run --example battery_service -- \
40+
//! --transport android-netsim \
41+
//! server
42+
//! ```
43+
//!
44+
//! ```
45+
//! PYTHONPATH=. python examples/battery_client.py \
46+
//! android-netsim F0:F1:F2:F3:F4:F5
47+
//! ```
3448
3549
use anyhow::anyhow;
3650
use async_trait::async_trait;
@@ -53,6 +67,8 @@ use owo_colors::OwoColorize;
5367
use pyo3::prelude::*;
5468
use rand::Rng;
5569
use std::time::Duration;
70+
use tokio::select;
71+
use tokio_util::sync::CancellationToken;
5672

5773
#[pyo3_asyncio::tokio::main]
5874
async fn main() -> PyResult<()> {
@@ -116,22 +132,22 @@ async fn run_client(device: Device, target_addr: Address) -> anyhow::Result<()>
116132
}
117133

118134
async fn run_server(mut device: Device) -> anyhow::Result<()> {
119-
let uuid = services::BATTERY.uuid();
135+
let battery_service_uuid = services::BATTERY.uuid();
120136
let battery_level_uuid = Uuid16::from(0x2A19).into();
121137
let battery_level = Characteristic::new(
122138
battery_level_uuid,
123139
CharacteristicProperty::Read | CharacteristicProperty::Notify,
124140
AttributePermission::Readable.into(),
125141
CharacteristicValueHandler::new(Box::new(BatteryRead), Box::new(NoOpWrite)),
126142
);
127-
let service = Service::new(uuid.into(), vec![battery_level]);
143+
let service = Service::new(battery_service_uuid.into(), vec![battery_level]);
128144
let service_handle = device.add_service(&service)?;
129145

130146
let mut builder = AdvertisementDataBuilder::new();
131147
builder.append(CommonDataType::CompleteLocalName, "Bumble Battery")?;
132148
builder.append(
133149
CommonDataType::IncompleteListOf16BitServiceClassUuids,
134-
&uuid,
150+
&battery_service_uuid,
135151
)?;
136152
builder.append(
137153
CommonDataType::Appearance,
@@ -146,10 +162,28 @@ async fn run_server(mut device: Device) -> anyhow::Result<()> {
146162
.characteristic_handle(battery_level_uuid)
147163
.expect("Battery level should be present");
148164

165+
let cancellation_token = CancellationToken::new();
166+
167+
let ct = cancellation_token.clone();
168+
tokio::spawn(async move {
169+
let _ = tokio::signal::ctrl_c().await;
170+
println!("Ctrl-C caught");
171+
ct.cancel();
172+
});
173+
149174
loop {
150-
tokio::time::sleep(Duration::from_secs(3)).await;
175+
select! {
176+
_ = tokio::time::sleep(Duration::from_secs(3)) => {}
177+
_ = cancellation_token.cancelled() => { break }
178+
}
179+
151180
device.notify_subscribers(char_handle).await?;
152181
}
182+
183+
println!("Stopping advertising");
184+
device.stop_advertising().await?;
185+
186+
Ok(())
153187
}
154188

155189
struct BatteryRead;

rust/pytests/rootcanal.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,16 @@ async fn with_dir_lock<T>(
271271
closure: impl Future<Output = anyhow::Result<T>>,
272272
) -> anyhow::Result<T> {
273273
// wait until we can create the dir
274+
let mut printed_contention_msg = false;
274275
loop {
275276
match fs::create_dir(dir) {
276277
Ok(_) => break,
277278
Err(e) => {
278279
if e.kind() == io::ErrorKind::AlreadyExists {
280+
if !printed_contention_msg {
281+
printed_contention_msg = true;
282+
eprintln!("Dir lock contention; sleeping");
283+
}
279284
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
280285
} else {
281286
warn!(

rust/src/cli/firmware/rtk.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
1717
use crate::{Download, Source};
1818
use anyhow::anyhow;
19+
use bumble::project_dir;
1920
use bumble::wrapper::{
2021
drivers::rtk::{Driver, DriverInfo, Firmware},
2122
host::{DriverFactory, Host},
@@ -28,10 +29,7 @@ use std::{fs, path};
2829
pub(crate) async fn download(dl: Download) -> PyResult<()> {
2930
let data_dir = dl
3031
.output_dir
31-
.or_else(|| {
32-
directories::ProjectDirs::from("com", "google", "bumble")
33-
.map(|pd| pd.data_local_dir().join("firmware").join("realtek"))
34-
})
32+
.or_else(|| project_dir().map(|pd| pd.data_local_dir().join("firmware").join("realtek")))
3533
.unwrap_or_else(|| {
3634
eprintln!("Could not determine standard data directory");
3735
path::PathBuf::from(".")

rust/src/internal/hci/mod.rs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use itertools::Itertools;
1516
pub use pdl_runtime::{Error, Packet};
17+
use std::fmt;
1618

17-
use crate::internal::hci::packets::{Acl, Command, Event, Sco};
19+
use crate::internal::hci::packets::{Acl, AddressType, Command, Event, Sco};
1820
use pdl_derive::pdl;
1921

2022
#[allow(missing_docs, warnings, clippy::all)]
@@ -23,6 +25,110 @@ pub mod packets {}
2325
#[cfg(test)]
2426
mod tests;
2527

28+
/// A Bluetooth address
29+
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
30+
pub struct Address {
31+
/// Little-endian bytes
32+
le_bytes: [u8; 6],
33+
address_type: AddressType,
34+
}
35+
36+
impl Address {
37+
/// Creates a new address with the provided little-endian bytes.
38+
pub fn from_le_bytes(le_bytes: [u8; 6], address_type: AddressType) -> Self {
39+
Self {
40+
le_bytes,
41+
address_type,
42+
}
43+
}
44+
45+
/// Creates a new address with the provided big endian hex (with or without `:` separators).
46+
///
47+
/// # Examples
48+
///
49+
/// ```
50+
/// use bumble::{wrapper::{hci::{Address, packets::AddressType}}};
51+
/// let hex = "F0:F1:F2:F3:F4:F5";
52+
/// assert_eq!(
53+
/// hex,
54+
/// Address::from_be_hex(hex, AddressType::PublicDeviceAddress).unwrap().as_be_hex()
55+
/// );
56+
/// ```
57+
pub fn from_be_hex(
58+
address: &str,
59+
address_type: AddressType,
60+
) -> Result<Self, InvalidAddressHex> {
61+
let filtered: String = address.chars().filter(|c| *c != ':').collect();
62+
let mut bytes: [u8; 6] = hex::decode(filtered)
63+
.map_err(|_| InvalidAddressHex { address })?
64+
.try_into()
65+
.map_err(|_| InvalidAddressHex { address })?;
66+
bytes.reverse();
67+
68+
Ok(Self {
69+
le_bytes: bytes,
70+
address_type,
71+
})
72+
}
73+
74+
/// The type of address
75+
pub fn address_type(&self) -> AddressType {
76+
self.address_type
77+
}
78+
79+
/// True if the address is static
80+
pub fn is_static(&self) -> bool {
81+
!self.is_public() && self.le_bytes[5] >> 6 == 3
82+
}
83+
84+
/// True if the address type is [AddressType::PublicIdentityAddress] or
85+
/// [AddressType::PublicDeviceAddress]
86+
pub fn is_public(&self) -> bool {
87+
matches!(
88+
self.address_type,
89+
AddressType::PublicDeviceAddress | AddressType::PublicIdentityAddress
90+
)
91+
}
92+
93+
/// True if the address is resolvable
94+
pub fn is_resolvable(&self) -> bool {
95+
self.address_type == AddressType::RandomDeviceAddress && self.le_bytes[5] >> 6 == 1
96+
}
97+
98+
/// Address bytes in _little-endian_ format
99+
pub fn as_le_bytes(&self) -> [u8; 6] {
100+
self.le_bytes
101+
}
102+
103+
/// Address bytes as big-endian colon-separated hex
104+
pub fn as_be_hex(&self) -> String {
105+
self.le_bytes
106+
.into_iter()
107+
.rev()
108+
.map(|byte| hex::encode_upper([byte]))
109+
.join(":")
110+
}
111+
}
112+
113+
// show a more readable form than default Debug for a byte array
114+
impl fmt::Debug for Address {
115+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116+
write!(
117+
f,
118+
"Address {{ address: {}, type: {:?} }}",
119+
self.as_be_hex(),
120+
self.address_type
121+
)
122+
}
123+
}
124+
125+
/// Error type for [Address::from_be_hex].
126+
#[derive(Debug, thiserror::Error)]
127+
#[error("Invalid address hex: {address}")]
128+
pub struct InvalidAddressHex<'a> {
129+
address: &'a str,
130+
}
131+
26132
/// HCI Packet type, prepended to the packet.
27133
/// Rootcanal's PDL declaration excludes this from ser/deser and instead is implemented in code.
28134
/// To maintain the ability to easily use future versions of their packet PDL, packet type is

rust/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,10 @@ pub mod wrapper;
3030

3131
pub use internal::adv;
3232
pub(crate) mod internal;
33+
34+
/// Directory for Bumble local storage for the current user according to the OS's conventions,
35+
/// if a convention is known for the current OS
36+
#[cfg(any(feature = "bumble-tools", test))]
37+
pub fn project_dir() -> Option<directories::ProjectDirs> {
38+
directories::ProjectDirs::from("com", "google", "bumble")
39+
}

0 commit comments

Comments
 (0)