Skip to content

Commit c8cb4d9

Browse files
Address PR feedback
1 parent e521e68 commit c8cb4d9

File tree

8 files changed

+149
-125
lines changed

8 files changed

+149
-125
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: 17 additions & 3 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;
@@ -116,22 +130,22 @@ async fn run_client(device: Device, target_addr: Address) -> anyhow::Result<()>
116130
}
117131

118132
async fn run_server(mut device: Device) -> anyhow::Result<()> {
119-
let uuid = services::BATTERY.uuid();
133+
let battery_service_uuid = services::BATTERY.uuid();
120134
let battery_level_uuid = Uuid16::from(0x2A19).into();
121135
let battery_level = Characteristic::new(
122136
battery_level_uuid,
123137
CharacteristicProperty::Read | CharacteristicProperty::Notify,
124138
AttributePermission::Readable.into(),
125139
CharacteristicValueHandler::new(Box::new(BatteryRead), Box::new(NoOpWrite)),
126140
);
127-
let service = Service::new(uuid.into(), vec![battery_level]);
141+
let service = Service::new(battery_service_uuid.into(), vec![battery_level]);
128142
let service_handle = device.add_service(&service)?;
129143

130144
let mut builder = AdvertisementDataBuilder::new();
131145
builder.append(CommonDataType::CompleteLocalName, "Bumble Battery")?;
132146
builder.append(
133147
CommonDataType::IncompleteListOf16BitServiceClassUuids,
134-
&uuid,
148+
&battery_service_uuid,
135149
)?;
136150
builder.append(
137151
CommonDataType::Appearance,

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+
}

rust/src/wrapper/hci.rs

Lines changed: 3 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414

1515
//! HCI
1616
17-
use std::fmt;
1817
// re-export here, and internal usages of these imports should refer to this mod, not the internal
1918
// mod
2019
pub(crate) use crate::internal::hci::WithPacketType;
21-
pub use crate::internal::hci::{packets, Error, Packet};
20+
pub use crate::internal::hci::{packets, Address, Error, InvalidAddressHex, Packet};
2221

2322
use crate::wrapper::core::{TryFromPy, TryToPy};
2423
use crate::wrapper::hci::packets::{AddressType, Command, ErrorCode};
25-
use itertools::Itertools as _;
2624
use pyo3::types::PyBytes;
2725
use pyo3::{
2826
exceptions::PyException, intern, types::PyModule, FromPyObject, IntoPy, PyAny, PyErr, PyObject,
@@ -72,111 +70,11 @@ impl IntoPy<PyObject> for HciCommand {
7270
}
7371
}
7472

75-
/// A Bluetooth address
76-
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
77-
pub struct Address {
78-
/// Little-endian bytes
79-
le_bytes: [u8; 6],
80-
address_type: AddressType,
81-
}
82-
83-
impl Address {
84-
/// Creates a new address with the provided little-endian bytes.
85-
pub fn from_le_bytes(le_bytes: [u8; 6], address_type: AddressType) -> Self {
86-
Self {
87-
le_bytes,
88-
address_type,
89-
}
90-
}
91-
92-
/// Creates a new address with the provided big endian hex (with or without `:` separators).
93-
///
94-
/// # Examples
95-
///
96-
/// ```
97-
/// use bumble::{wrapper::{hci::{Address, packets::AddressType}}};
98-
/// let hex = "F0:F1:F2:F3:F4:F5";
99-
/// assert_eq!(
100-
/// hex,
101-
/// Address::from_be_hex(hex, AddressType::PublicDeviceAddress).unwrap().as_be_hex()
102-
/// );
103-
/// ```
104-
pub fn from_be_hex(
105-
address: &str,
106-
address_type: AddressType,
107-
) -> Result<Self, InvalidAddressHex> {
108-
let filtered: String = address.chars().filter(|c| *c != ':').collect();
109-
let mut bytes: [u8; 6] = hex::decode(filtered)
110-
.map_err(|_| InvalidAddressHex { address })?
111-
.try_into()
112-
.map_err(|_| InvalidAddressHex { address })?;
113-
bytes.reverse();
114-
115-
Ok(Self {
116-
le_bytes: bytes,
117-
address_type,
118-
})
119-
}
120-
121-
/// The type of address
122-
pub fn address_type(&self) -> AddressType {
123-
self.address_type
124-
}
125-
126-
/// True if the address is static
127-
pub fn is_static(&self) -> bool {
128-
!self.is_public() && self.le_bytes[5] >> 6 == 3
129-
}
130-
131-
/// True if the address type is [AddressType::PublicIdentityAddress] or
132-
/// [AddressType::PublicDeviceAddress]
133-
pub fn is_public(&self) -> bool {
134-
matches!(
135-
self.address_type,
136-
AddressType::PublicDeviceAddress | AddressType::PublicIdentityAddress
137-
)
138-
}
139-
140-
/// True if the address is resolvable
141-
pub fn is_resolvable(&self) -> bool {
142-
matches!(
143-
self.address_type,
144-
AddressType::PublicIdentityAddress | AddressType::RandomIdentityAddress
145-
)
146-
}
147-
148-
/// Address bytes in _little-endian_ format
149-
pub fn as_le_bytes(&self) -> [u8; 6] {
150-
self.le_bytes
151-
}
152-
153-
/// Address bytes as big-endian colon-separated hex
154-
pub fn as_be_hex(&self) -> String {
155-
self.le_bytes
156-
.into_iter()
157-
.rev()
158-
.map(|byte| hex::encode_upper([byte]))
159-
.join(":")
160-
}
161-
}
162-
163-
// show a more readable form than default Debug for a byte array
164-
impl fmt::Debug for Address {
165-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166-
write!(
167-
f,
168-
"Address {{ address: {}, type: {:?} }}",
169-
self.as_be_hex(),
170-
self.address_type
171-
)
172-
}
173-
}
174-
17573
impl TryToPy for Address {
17674
fn try_to_py<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> {
17775
PyModule::import(py, intern!(py, "bumble.device"))?
17876
.getattr(intern!(py, "Address"))?
179-
.call1((PyBytes::new(py, &self.le_bytes), self.address_type))
77+
.call1((PyBytes::new(py, &self.as_le_bytes()), self.address_type()))
18078
}
18179
}
18280

@@ -194,20 +92,10 @@ impl TryFromPy for Address {
19492

19593
let address = obj.call_method0(intern!(py, "to_bytes"))?.extract()?;
19694

197-
Ok(Self {
198-
le_bytes: address,
199-
address_type,
200-
})
95+
Ok(Self::from_le_bytes(address, address_type))
20196
}
20297
}
20398

204-
/// Error type for [Address::from_be_hex].
205-
#[derive(Debug, thiserror::Error)]
206-
#[error("Invalid address hex: {address}")]
207-
pub struct InvalidAddressHex<'a> {
208-
address: &'a str,
209-
}
210-
21199
/// An error meaning that the internal u64 value used to convert to [packets::Address] did not
212100
/// represent a valid BT address.
213101
#[derive(Debug, thiserror::Error)]

rust/src/wrapper/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,12 @@ use pyo3::{
3131
};
3232

3333
pub mod assigned_numbers;
34+
pub mod att;
3435
pub mod common;
3536
pub mod controller;
3637
pub mod core;
3738
pub mod device;
3839
pub mod drivers;
39-
40-
pub mod att;
4140
pub mod gatt;
4241
pub mod hci;
4342
pub mod host;

0 commit comments

Comments
 (0)