use crate::transport::{TransportAddr, TransportError};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BleAddr {
pub adapter: String,
pub device: [u8; 6],
}
impl BleAddr {
pub fn parse(s: &str) -> Result<Self, TransportError> {
let (adapter, mac_str) = s.split_once('/').ok_or_else(|| {
TransportError::InvalidAddress(format!("missing '/' in BLE address: {s}"))
})?;
if adapter.is_empty() {
return Err(TransportError::InvalidAddress("empty adapter name".into()));
}
let device = parse_mac(mac_str).ok_or_else(|| {
TransportError::InvalidAddress(format!("invalid MAC address: {mac_str}"))
})?;
Ok(Self {
adapter: adapter.to_string(),
device,
})
}
pub fn to_string_repr(&self) -> String {
format!(
"{}/{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
self.adapter,
self.device[0],
self.device[1],
self.device[2],
self.device[3],
self.device[4],
self.device[5],
)
}
pub fn to_transport_addr(&self) -> TransportAddr {
TransportAddr::from_string(&self.to_string_repr())
}
}
#[cfg(bluer_available)]
impl BleAddr {
pub fn from_bluer(addr: bluer::Address, adapter: &str) -> Self {
Self {
adapter: adapter.to_string(),
device: addr.0,
}
}
pub fn to_bluer_address(&self) -> bluer::Address {
bluer::Address(self.device)
}
pub fn to_socket_addr(&self, psm: u16) -> bluer::l2cap::SocketAddr {
bluer::l2cap::SocketAddr::new(self.to_bluer_address(), bluer::AddressType::LePublic, psm)
}
}
impl std::fmt::Display for BleAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_string_repr())
}
}
fn parse_mac(s: &str) -> Option<[u8; 6]> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 6 {
return None;
}
let mut mac = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
mac[i] = u8::from_str_radix(part, 16).ok()?;
}
Some(mac)
}
pub fn adapter_from_addr(addr: &TransportAddr) -> Option<&str> {
addr.as_str()?.split_once('/').map(|(adapter, _)| adapter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid() {
let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
assert_eq!(addr.adapter, "hci0");
assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
}
#[test]
fn test_parse_lowercase() {
let addr = BleAddr::parse("hci1/aa:bb:cc:dd:ee:ff").unwrap();
assert_eq!(addr.adapter, "hci1");
assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
}
#[test]
fn test_roundtrip() {
let original = "hci0/AA:BB:CC:DD:EE:FF";
let addr = BleAddr::parse(original).unwrap();
assert_eq!(addr.to_string_repr(), original);
}
#[test]
fn test_display() {
let addr = BleAddr::parse("hci0/01:02:03:04:05:06").unwrap();
assert_eq!(format!("{addr}"), "hci0/01:02:03:04:05:06");
}
#[test]
fn test_to_transport_addr() {
let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
let ta = addr.to_transport_addr();
assert_eq!(ta.as_str(), Some("hci0/AA:BB:CC:DD:EE:FF"));
}
#[test]
fn test_parse_missing_slash() {
assert!(BleAddr::parse("hci0-AA:BB:CC:DD:EE:FF").is_err());
}
#[test]
fn test_parse_empty_adapter() {
assert!(BleAddr::parse("/AA:BB:CC:DD:EE:FF").is_err());
}
#[test]
fn test_parse_invalid_mac_short() {
assert!(BleAddr::parse("hci0/AA:BB:CC").is_err());
}
#[test]
fn test_parse_invalid_mac_hex() {
assert!(BleAddr::parse("hci0/GG:HH:II:JJ:KK:LL").is_err());
}
#[test]
fn test_adapter_from_addr() {
let ta = TransportAddr::from_string("hci0/AA:BB:CC:DD:EE:FF");
assert_eq!(adapter_from_addr(&ta), Some("hci0"));
}
#[test]
fn test_adapter_from_addr_no_slash() {
let ta = TransportAddr::from_string("invalid");
assert_eq!(adapter_from_addr(&ta), None);
}
}