Bluetooth B1: HCI protocol types, USB transport, daemon state machine

Add complete HCI protocol module (hci.rs) with packet types, 55+ constants,
command builders (Reset, Read BD Addr, Read Local Version, LE scan/connect),
event parsers, and structured result types. Add USB transport abstraction
(usb_transport.rs) with UsbHciTransport trait and StubTransport for testing.

Wire btusb daemon with endpoint descriptor parsing, HCI init sequence
(Reset → Read BD Addr → Read Local Version), ControllerState state machine,
and enhanced status output. Replace all expect()/unwrap() calls in btctl
and wifictl with proper error handling and graceful fallback.

91 btusb tests, 27 btctl tests, 2 wifictl tests passing.
This commit is contained in:
2026-04-24 22:46:35 +01:00
parent 183354fcb1
commit f392c7bf7d
5 changed files with 2360 additions and 38 deletions
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,6 @@
mod hci;
mod usb_transport;
use std::fs; use std::fs;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -8,6 +11,11 @@ use std::thread;
use std::time::Duration; use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use hci::{cmd_read_bd_addr, cmd_read_local_version, cmd_reset, parse_read_bd_addr, parse_local_version};
use usb_transport::UsbHciTransport;
#[cfg(target_os = "redox")]
use usb_transport::{StubTransport, UsbTransportConfig};
const STATUS_FRESHNESS_SECS: u64 = 90; const STATUS_FRESHNESS_SECS: u64 = 90;
const BLUETOOTH_USB_CLASS: u8 = 0xE0; const BLUETOOTH_USB_CLASS: u8 = 0xE0;
const BLUETOOTH_USB_SUBCLASS: u8 = 0x01; const BLUETOOTH_USB_SUBCLASS: u8 = 0x01;
@@ -21,6 +29,7 @@ struct UsbBluetoothAdapter {
device_id: u16, device_id: u16,
bus: String, bus: String,
device_path: PathBuf, device_path: PathBuf,
endpoints: HciEndpoints,
} }
impl UsbBluetoothAdapter { impl UsbBluetoothAdapter {
@@ -32,17 +41,21 @@ impl UsbBluetoothAdapter {
vendor_id: 0, vendor_id: 0,
device_id: 0, device_id: 0,
bus: "stub".to_string(), bus: "stub".to_string(),
endpoints: HciEndpoints::default(),
} }
} }
fn detail_line(&self, index: usize) -> String { fn detail_line(&self, index: usize) -> String {
format!( format!(
"adapter_{index}=name={};vendor_id={:04x};device_id={:04x};bus={};device_path={}", "adapter_{index}=name={};vendor_id={:04x};device_id={:04x};bus={};device_path={};event_ep={};acl_in_ep={};acl_out_ep={}",
self.name, self.name,
self.vendor_id, self.vendor_id,
self.device_id, self.device_id,
self.bus, self.bus,
self.device_path.display() self.device_path.display(),
self.endpoints.event_endpoint,
self.endpoints.acl_in_endpoint,
self.endpoints.acl_out_endpoint,
) )
} }
} }
@@ -68,11 +81,157 @@ impl UsbDeviceDescriptor {
} }
} }
/// USB HCI transport endpoint addresses for a Bluetooth controller
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HciEndpoints {
/// Interrupt IN endpoint for HCI events (required)
pub event_endpoint: u8,
/// Bulk IN endpoint for ACL data from controller (required)
pub acl_in_endpoint: u8,
/// Bulk OUT endpoint for ACL data to controller (required)
pub acl_out_endpoint: u8,
/// Maximum packet size for the event endpoint
pub event_max_packet_size: u16,
/// Maximum packet size for the bulk IN endpoint
pub acl_in_max_packet_size: u16,
/// Maximum packet size for the bulk OUT endpoint
pub acl_out_max_packet_size: u16,
}
/// Controller initialization state
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ControllerState {
/// No communication with controller yet
#[default]
Closed,
/// Sending HCI initialization commands
Initializing,
/// Controller is ready for use
Active,
/// Initialization or communication failed
Error,
}
/// Information gathered from the controller during initialization
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ControllerInfo {
pub state: ControllerState,
pub bd_address: Option<[u8; 6]>,
pub hci_version: Option<u8>,
pub hci_revision: Option<u16>,
pub manufacturer_name: Option<u16>,
pub init_error: Option<String>,
}
/// USB endpoint descriptor fields extracted from raw descriptor data
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct UsbEndpointDescriptor {
endpoint_address: u8,
attributes: u8,
max_packet_size: u16,
interval: u8,
}
/// USB transfer type from bmAttributes bits 0-1
const USB_TRANSFER_INTERRUPT: u8 = 3;
const USB_TRANSFER_BULK: u8 = 2;
/// Parse a single USB endpoint descriptor from raw bytes.
///
/// USB endpoint descriptor is 7 bytes:
/// [0] bLength = 7
/// [1] bDescriptorType = 5 (ENDPOINT)
/// [2] bEndpointAddress (bit 7 = direction: 0=OUT, 1=IN; bits 0-3 = endpoint number)
/// [3] bmAttributes (bits 0-1 = transfer type: 0=control, 1=isochronous, 2=bulk, 3=interrupt)
/// [4-5] wMaxPacketSize (little-endian)
/// [6] bInterval
fn parse_usb_endpoint_descriptor(raw: &[u8]) -> Option<UsbEndpointDescriptor> {
if raw.len() < 7 {
return None;
}
if raw[0] != 7 {
return None; // bLength must be 7
}
if raw[1] != 5 {
return None; // bDescriptorType must be ENDPOINT (5)
}
Some(UsbEndpointDescriptor {
endpoint_address: raw[2],
attributes: raw[3],
max_packet_size: u16::from_le_bytes([raw[4], raw[5]]),
interval: raw[6],
})
}
/// Parse HCI endpoints from raw USB descriptors blob.
///
/// Walks the descriptor blob looking for endpoint descriptors that match
/// the Bluetooth HCI interface (interrupt IN, bulk IN, bulk OUT).
pub fn parse_hci_endpoints_from_descriptors(raw: &[u8]) -> Result<HciEndpoints, String> {
let mut endpoints = HciEndpoints::default();
let mut found_interrupt_in = false;
let mut found_bulk_in = false;
let mut found_bulk_out = false;
let mut offset = 0;
while offset + 2 <= raw.len() {
let desc_len = raw[offset] as usize;
let desc_type = raw[offset + 1];
if desc_len < 2 || offset + desc_len > raw.len() {
break;
}
if desc_type == 5 {
// ENDPOINT descriptor
if let Some(ep) = parse_usb_endpoint_descriptor(&raw[offset..]) {
let direction_in = (ep.endpoint_address & 0x80) != 0;
let endpoint_num = ep.endpoint_address & 0x0F;
let transfer_type = ep.attributes & 0x03;
match (transfer_type, direction_in) {
(USB_TRANSFER_INTERRUPT, true) => {
endpoints.event_endpoint = endpoint_num;
endpoints.event_max_packet_size = ep.max_packet_size;
found_interrupt_in = true;
}
(USB_TRANSFER_BULK, true) => {
endpoints.acl_in_endpoint = endpoint_num;
endpoints.acl_in_max_packet_size = ep.max_packet_size;
found_bulk_in = true;
}
(USB_TRANSFER_BULK, false) => {
endpoints.acl_out_endpoint = endpoint_num;
endpoints.acl_out_max_packet_size = ep.max_packet_size;
found_bulk_out = true;
}
_ => {}
}
}
}
offset += desc_len.max(1); // Avoid infinite loop on zero-length descriptor
}
if !found_interrupt_in {
return Err("missing HCI interrupt IN endpoint in USB descriptors".to_string());
}
if !found_bulk_in {
return Err("missing HCI bulk IN endpoint in USB descriptors".to_string());
}
if !found_bulk_out {
return Err("missing HCI bulk OUT endpoint in USB descriptors".to_string());
}
Ok(endpoints)
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
struct TransportConfig { struct TransportConfig {
adapters: Vec<UsbBluetoothAdapter>, adapters: Vec<UsbBluetoothAdapter>,
controller_family: String, controller_family: String,
status_file: PathBuf, status_file: PathBuf,
controller_info: ControllerInfo,
} }
impl TransportConfig { impl TransportConfig {
@@ -84,6 +243,7 @@ impl TransportConfig {
status_file: std::env::var_os("REDBEAR_BTUSB_STATUS_FILE") status_file: std::env::var_os("REDBEAR_BTUSB_STATUS_FILE")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/var/run/redbear-btusb/status")), .unwrap_or_else(|| PathBuf::from("/var/run/redbear-btusb/status")),
controller_info: ControllerInfo::default(),
} }
} }
@@ -140,6 +300,34 @@ impl TransportConfig {
} }
)); ));
lines.push(format!("status_file={}", self.status_file.display())); lines.push(format!("status_file={}", self.status_file.display()));
let state_str = match self.controller_info.state {
ControllerState::Closed => "closed",
ControllerState::Initializing => "initializing",
ControllerState::Active => "active",
ControllerState::Error => "error",
};
lines.push(format!("controller_state={state_str}"));
if let Some(addr) = &self.controller_info.bd_address {
lines.push(format!(
"bd_address={:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]
));
}
if let Some(version) = &self.controller_info.hci_version {
lines.push(format!("hci_version={version}"));
}
if let Some(revision) = &self.controller_info.hci_revision {
lines.push(format!("hci_revision={revision}"));
}
if let Some(manufacturer) = &self.controller_info.manufacturer_name {
lines.push(format!("manufacturer={manufacturer}"));
}
if let Some(err) = &self.controller_info.init_error {
lines.push(format!("init_error={err}"));
}
lines lines
} }
@@ -413,12 +601,14 @@ fn try_collect_bluetooth_adapter(
Err(_) => return Ok(()), Err(_) => return Ok(()),
}; };
if descriptor.looks_like_bluetooth() { if descriptor.looks_like_bluetooth() {
let endpoints = parse_hci_endpoints_from_descriptors(&raw).unwrap_or_default();
adapters.push(UsbBluetoothAdapter { adapters.push(UsbBluetoothAdapter {
name: String::new(), name: String::new(),
vendor_id: descriptor.vendor_id, vendor_id: descriptor.vendor_id,
device_id: descriptor.device_id, device_id: descriptor.device_id,
bus: bus.to_string(), bus: bus.to_string(),
device_path: device_path.to_path_buf(), device_path: device_path.to_path_buf(),
endpoints,
}); });
} }
@@ -501,6 +691,69 @@ fn probe_usb_bluetooth_adapters() -> Result<Vec<UsbBluetoothAdapter>, String> {
probe_usb_bluetooth_adapters_in(Path::new("/scheme/usb")) probe_usb_bluetooth_adapters_in(Path::new("/scheme/usb"))
} }
fn hci_init_sequence(transport: &mut dyn UsbHciTransport) -> Result<ControllerInfo, String> {
let mut info = ControllerInfo::default();
info.state = ControllerState::Initializing;
let reset_cmd = cmd_reset();
transport
.send_command(&reset_cmd)
.map_err(|err| format!("HCI Reset send failed: {err}"))?;
let event = transport
.recv_event()
.map_err(|err| format!("HCI Reset response failed: {err}"))?;
let Some(event) = event else {
return Err("HCI Reset: no response from controller".to_string());
};
if !event.is_command_complete() {
return Err(format!(
"HCI Reset: unexpected event code 0x{:02X}",
event.event_code
));
}
let addr_cmd = cmd_read_bd_addr();
transport
.send_command(&addr_cmd)
.map_err(|err| format!("HCI Read BD Addr send failed: {err}"))?;
if let Some(event) =
transport
.recv_event()
.map_err(|err| format!("HCI Read BD Addr response: {err}"))?
{
if let Some(result) = parse_read_bd_addr(&event) {
if result.status == 0x00 {
info.bd_address = Some(result.address);
}
}
}
let version_cmd = cmd_read_local_version();
transport
.send_command(&version_cmd)
.map_err(|err| format!("HCI Read Local Version send failed: {err}"))?;
if let Some(event) = transport
.recv_event()
.map_err(|err| format!("HCI Read Local Version response: {err}"))?
{
if let Some(result) = parse_local_version(&event) {
if result.status == 0x00 {
info.hci_version = Some(result.hci_version);
info.hci_revision = Some(result.hci_revision);
info.manufacturer_name = Some(result.manufacturer_name);
}
}
}
info.state = ControllerState::Active;
Ok(info)
}
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
fn daemon_main(_config: &TransportConfig) -> Result<(), String> { fn daemon_main(_config: &TransportConfig) -> Result<(), String> {
Err("daemon mode is only supported on Redox; use --probe or --status on host".to_string()) Err("daemon mode is only supported on Redox; use --probe or --status on host".to_string())
@@ -519,6 +772,31 @@ fn daemon_main(config: &TransportConfig) -> Result<(), String> {
} }
let mut runtime_config = config.refreshed(); let mut runtime_config = config.refreshed();
for adapter in &runtime_config.adapters {
let transport_config = UsbTransportConfig {
device_path: adapter.device_path.clone(),
vendor_id: adapter.vendor_id,
device_id: adapter.device_id,
interrupt_endpoint: adapter.endpoints.event_endpoint,
bulk_in_endpoint: adapter.endpoints.acl_in_endpoint,
bulk_out_endpoint: adapter.endpoints.acl_out_endpoint,
};
let mut transport = StubTransport::new(transport_config);
match hci_init_sequence(&mut transport) {
Ok(info) => {
runtime_config.controller_info = info;
}
Err(err) => {
runtime_config.controller_info.state = ControllerState::Error;
runtime_config.controller_info.init_error = Some(err);
}
}
break;
}
runtime_config.write_status_file()?; runtime_config.write_status_file()?;
let _status_file_guard = StatusFileGuard { let _status_file_guard = StatusFileGuard {
path: &config.status_file, path: &config.status_file,
@@ -526,7 +804,9 @@ fn daemon_main(config: &TransportConfig) -> Result<(), String> {
loop { loop {
thread::sleep(Duration::from_secs(30)); thread::sleep(Duration::from_secs(30));
let controller_info = runtime_config.controller_info.clone();
runtime_config = config.refreshed(); runtime_config = config.refreshed();
runtime_config.controller_info = controller_info;
runtime_config.write_status_file()?; runtime_config.write_status_file()?;
} }
} }
@@ -554,6 +834,7 @@ mod tests {
adapters: vec![stub_adapter("hci0")], adapters: vec![stub_adapter("hci0")],
controller_family: "usb-bounded-test".to_string(), controller_family: "usb-bounded-test".to_string(),
status_file, status_file,
controller_info: ControllerInfo::default(),
} }
} }
@@ -702,4 +983,294 @@ mod tests {
let adapters = probe_usb_bluetooth_adapters_in(&root).unwrap(); let adapters = probe_usb_bluetooth_adapters_in(&root).unwrap();
assert!(adapters.is_empty()); assert!(adapters.is_empty());
} }
#[test]
fn parse_usb_endpoint_descriptor_valid() {
let raw: &[u8] = &[7, 5, 0x81, 0x03, 0x40, 0x00, 0x01];
let ep = parse_usb_endpoint_descriptor(raw).unwrap();
assert_eq!(ep.endpoint_address, 0x81);
assert_eq!(ep.attributes, 0x03);
assert_eq!(ep.max_packet_size, 64);
assert_eq!(ep.interval, 1);
}
#[test]
fn parse_usb_endpoint_descriptor_too_short() {
let raw: &[u8] = &[7, 5, 0x81, 0x03, 0x40];
assert!(parse_usb_endpoint_descriptor(raw).is_none());
}
#[test]
fn parse_usb_endpoint_descriptor_wrong_length() {
let raw: &[u8] = &[9, 5, 0x81, 0x03, 0x40, 0x00, 0x01];
assert!(parse_usb_endpoint_descriptor(raw).is_none());
}
#[test]
fn parse_usb_endpoint_descriptor_wrong_type() {
let raw: &[u8] = &[7, 4, 0x81, 0x03, 0x40, 0x00, 0x01];
assert!(parse_usb_endpoint_descriptor(raw).is_none());
}
#[test]
fn parse_hci_endpoints_from_descriptors_extracts_all_three() {
let blob: Vec<u8> = vec![
// Interface descriptor (9 bytes)
9, 4, 0, 0, 3, 0xE0, 0x01, 0x01, 0x00,
// Interrupt IN endpoint: address=0x81 (EP1 IN), attributes=0x03 (interrupt), max_packet=64
7, 5, 0x81, 0x03, 0x40, 0x00, 0x01,
// Bulk IN endpoint: address=0x82 (EP2 IN), attributes=0x02 (bulk), max_packet=512
7, 5, 0x82, 0x02, 0x00, 0x02, 0x00,
// Bulk OUT endpoint: address=0x02 (EP2 OUT), attributes=0x02 (bulk), max_packet=512
7, 5, 0x02, 0x02, 0x00, 0x02, 0x00,
];
let endpoints = parse_hci_endpoints_from_descriptors(&blob).unwrap();
assert_eq!(endpoints.event_endpoint, 1);
assert_eq!(endpoints.event_max_packet_size, 64);
assert_eq!(endpoints.acl_in_endpoint, 2);
assert_eq!(endpoints.acl_in_max_packet_size, 512);
assert_eq!(endpoints.acl_out_endpoint, 2);
assert_eq!(endpoints.acl_out_max_packet_size, 512);
}
#[test]
fn parse_hci_endpoints_from_descriptors_missing_interrupt_in() {
let blob: Vec<u8> = vec![
// Bulk IN only, no interrupt IN
7, 5, 0x82, 0x02, 0x00, 0x02, 0x00,
7, 5, 0x02, 0x02, 0x00, 0x02, 0x00,
];
let result = parse_hci_endpoints_from_descriptors(&blob);
assert!(result.is_err());
assert!(result.unwrap_err().contains("interrupt IN"));
}
#[test]
fn parse_hci_endpoints_from_descriptors_empty_blob() {
let result = parse_hci_endpoints_from_descriptors(&[]);
assert!(result.is_err());
}
#[test]
fn parse_hci_endpoints_from_descriptors_ignores_other_endpoints() {
let blob: Vec<u8> = vec![
// Isochronous IN endpoint (should be ignored)
7, 5, 0x83, 0x01, 0x00, 0x01, 0x01,
// Control endpoint (should be ignored)
7, 5, 0x04, 0x00, 0x40, 0x00, 0x00,
// Interrupt IN endpoint (should be picked up)
7, 5, 0x81, 0x03, 0x40, 0x00, 0x01,
// Bulk IN endpoint (should be picked up)
7, 5, 0x82, 0x02, 0x00, 0x02, 0x00,
// Bulk OUT endpoint (should be picked up)
7, 5, 0x02, 0x02, 0x00, 0x02, 0x00,
];
let endpoints = parse_hci_endpoints_from_descriptors(&blob).unwrap();
assert_eq!(endpoints.event_endpoint, 1);
assert_eq!(endpoints.acl_in_endpoint, 2);
assert_eq!(endpoints.acl_out_endpoint, 2);
}
#[test]
fn hci_endpoints_default_is_zeroed() {
let ep = HciEndpoints::default();
assert_eq!(ep.event_endpoint, 0);
assert_eq!(ep.acl_in_endpoint, 0);
assert_eq!(ep.acl_out_endpoint, 0);
assert_eq!(ep.event_max_packet_size, 0);
assert_eq!(ep.acl_in_max_packet_size, 0);
assert_eq!(ep.acl_out_max_packet_size, 0);
}
#[test]
fn detail_line_includes_endpoint_fields() {
let adapter = UsbBluetoothAdapter {
name: "hci0".to_string(),
vendor_id: 0x8087,
device_id: 0x0032,
bus: "usb.0000:00:14.0".to_string(),
device_path: PathBuf::from("/scheme/usb/usb.0000:00:14.0/port1"),
endpoints: HciEndpoints {
event_endpoint: 1,
acl_in_endpoint: 2,
acl_out_endpoint: 2,
event_max_packet_size: 64,
acl_in_max_packet_size: 512,
acl_out_max_packet_size: 512,
},
};
let line = adapter.detail_line(0);
assert!(line.contains("event_ep=1"));
assert!(line.contains("acl_in_ep=2"));
assert!(line.contains("acl_out_ep=2"));
assert!(line.contains("vendor_id=8087"));
assert!(line.contains("device_id=0032"));
}
// -- Controller state and HCI init tests --------------------------------
#[test]
fn controller_state_default_is_closed() {
let info = ControllerInfo::default();
assert_eq!(info.state, ControllerState::Closed);
assert!(info.bd_address.is_none());
assert!(info.hci_version.is_none());
assert!(info.hci_revision.is_none());
assert!(info.manufacturer_name.is_none());
assert!(info.init_error.is_none());
}
struct TestTransport {
pending_events: Vec<hci::HciEvent>,
}
impl TestTransport {
fn new() -> Self {
Self {
pending_events: Vec::new(),
}
}
fn inject_event(&mut self, event: hci::HciEvent) {
self.pending_events.push(event);
}
}
impl usb_transport::UsbHciTransport for TestTransport {
fn send_command(&mut self, _command: &hci::HciCommand) -> std::io::Result<()> {
Ok(())
}
fn recv_event(&mut self) -> std::io::Result<Option<hci::HciEvent>> {
Ok(if self.pending_events.is_empty() {
None
} else {
Some(self.pending_events.remove(0))
})
}
fn send_acl(&mut self, _acl: &hci::HciAcl) -> std::io::Result<()> {
Ok(())
}
fn recv_acl(&mut self) -> std::io::Result<Option<hci::HciAcl>> {
Ok(None)
}
fn state(&self) -> usb_transport::TransportState {
usb_transport::TransportState::Active
}
fn close(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn inject_cc_event(transport: &mut TestTransport, opcode: u16, return_params: Vec<u8>) {
let mut params = vec![0x01];
params.push(opcode as u8);
params.push((opcode >> 8) as u8);
params.extend(return_params);
let event = hci::HciEvent {
event_code: hci::EVT_COMMAND_COMPLETE,
parameters: params,
};
transport.inject_event(event);
}
#[test]
fn hci_init_sequence_with_stub_succeeds() {
let mut transport = TestTransport::new();
// Reset CC: status=0x00
inject_cc_event(&mut transport, hci::OP_RESET, vec![0x00]);
// Read BD Addr CC: status=0x00 + 6-byte address
inject_cc_event(
&mut transport,
hci::OP_READ_BD_ADDR,
vec![0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
);
// Read Local Version CC: status + hci_version + hci_revision(2) + lmp_version + manufacturer(2) + lmp_subversion(2)
inject_cc_event(
&mut transport,
hci::OP_READ_LOCAL_VERSION,
vec![0x00, 0x09, 0x01, 0x00, 0x09, 0x02, 0x00, 0x01, 0x00],
);
let info = hci_init_sequence(&mut transport).expect("init should succeed");
assert_eq!(info.state, ControllerState::Active);
assert_eq!(info.bd_address, Some([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]));
assert_eq!(info.hci_version, Some(0x09));
assert_eq!(info.hci_revision, Some(0x0001));
assert_eq!(info.manufacturer_name, Some(0x0002));
assert!(info.init_error.is_none());
}
#[test]
fn hci_init_sequence_fails_when_reset_gets_no_response() {
let mut transport = TestTransport::new();
// No events injected — recv_event returns None
let result = hci_init_sequence(&mut transport);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(
err.contains("no response"),
"expected 'no response' error, got: {err}"
);
}
#[test]
fn status_lines_include_controller_state() {
let status_file = temp_path("rbos-btusb-status-active");
let mut config = test_config(status_file);
config.controller_info = ControllerInfo {
state: ControllerState::Active,
bd_address: Some([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]),
hci_version: Some(9),
hci_revision: Some(1),
manufacturer_name: Some(2),
init_error: None,
};
let lines = config.render_status_lines(true);
let output = lines.join("\n");
assert!(
output.contains("controller_state=active"),
"missing controller_state=active, got: {output}"
);
assert!(
output.contains("bd_address="),
"missing bd_address, got: {output}"
);
assert!(
output.contains("hci_version=9"),
"missing hci_version, got: {output}"
);
assert!(
output.contains("manufacturer=2"),
"missing manufacturer, got: {output}"
);
}
#[test]
fn status_lines_include_init_error() {
let status_file = temp_path("rbos-btusb-status-error");
let mut config = test_config(status_file);
config.controller_info = ControllerInfo {
state: ControllerState::Error,
bd_address: None,
hci_version: None,
hci_revision: None,
manufacturer_name: None,
init_error: Some("HCI Reset send failed: transport is closed".to_string()),
};
let lines = config.render_status_lines(true);
let output = lines.join("\n");
assert!(
output.contains("controller_state=error"),
"missing controller_state=error, got: {output}"
);
assert!(
output.contains("init_error=HCI Reset send failed"),
"missing init_error, got: {output}"
);
}
} }
@@ -0,0 +1,330 @@
//! USB transport abstraction for HCI communication with Bluetooth controllers.
use std::io;
use std::path::PathBuf;
use crate::hci::{HciAcl, HciCommand, HciEvent};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UsbEndpointType {
Control,
Interrupt,
BulkIn,
BulkOut,
}
#[derive(Clone, Debug)]
pub struct UsbTransportConfig {
pub device_path: PathBuf,
pub vendor_id: u16,
pub device_id: u16,
pub interrupt_endpoint: u8,
pub bulk_in_endpoint: u8,
pub bulk_out_endpoint: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransportState {
Closed,
Opening,
Active,
Error,
}
pub trait UsbHciTransport {
fn send_command(&mut self, command: &HciCommand) -> io::Result<()>;
fn recv_event(&mut self) -> io::Result<Option<HciEvent>>;
fn send_acl(&mut self, acl: &HciAcl) -> io::Result<()>;
fn recv_acl(&mut self) -> io::Result<Option<HciAcl>>;
fn state(&self) -> TransportState;
fn close(&mut self) -> io::Result<()>;
}
pub struct StubTransport {
config: UsbTransportConfig,
state: TransportState,
sent_commands: Vec<HciCommand>,
sent_acl: Vec<HciAcl>,
pending_events: Vec<HciEvent>,
pending_acl: Vec<HciAcl>,
}
impl StubTransport {
pub fn new(config: UsbTransportConfig) -> Self {
Self {
config,
state: TransportState::Closed,
sent_commands: Vec::new(),
sent_acl: Vec::new(),
pending_events: Vec::new(),
pending_acl: Vec::new(),
}
}
pub fn inject_event(&mut self, event: HciEvent) {
self.pending_events.push(event);
}
pub fn inject_acl(&mut self, acl: HciAcl) {
self.pending_acl.push(acl);
}
pub fn drain_sent_commands(&mut self) -> Vec<HciCommand> {
let drained = self.sent_commands.clone();
self.sent_commands.clear();
drained
}
pub fn drain_sent_acl(&mut self) -> Vec<HciAcl> {
let drained = self.sent_acl.clone();
self.sent_acl.clear();
drained
}
#[allow(dead_code)]
pub fn config(&self) -> &UsbTransportConfig {
&self.config
}
}
impl UsbHciTransport for StubTransport {
fn send_command(&mut self, command: &HciCommand) -> io::Result<()> {
if self.state == TransportState::Closed {
return Err(io::Error::new(io::ErrorKind::NotConnected, "transport is closed"));
}
self.sent_commands.push(command.clone());
Ok(())
}
fn recv_event(&mut self) -> io::Result<Option<HciEvent>> {
if self.state == TransportState::Closed {
return Err(io::Error::new(io::ErrorKind::NotConnected, "transport is closed"));
}
Ok(if self.pending_events.is_empty() {
None
} else {
Some(self.pending_events.remove(0))
})
}
fn send_acl(&mut self, acl: &HciAcl) -> io::Result<()> {
if self.state == TransportState::Closed {
return Err(io::Error::new(io::ErrorKind::NotConnected, "transport is closed"));
}
self.sent_acl.push(acl.clone());
Ok(())
}
fn recv_acl(&mut self) -> io::Result<Option<HciAcl>> {
if self.state == TransportState::Closed {
return Err(io::Error::new(io::ErrorKind::NotConnected, "transport is closed"));
}
Ok(if self.pending_acl.is_empty() {
None
} else {
Some(self.pending_acl.remove(0))
})
}
fn state(&self) -> TransportState {
self.state
}
fn close(&mut self) -> io::Result<()> {
self.state = TransportState::Closed;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hci::{EVT_COMMAND_COMPLETE, EVT_COMMAND_STATUS, OP_RESET};
fn test_config() -> UsbTransportConfig {
UsbTransportConfig {
device_path: PathBuf::from("/scheme/usb/test/hci0"),
vendor_id: 0x8087,
device_id: 0x0A2B,
interrupt_endpoint: 0x81,
bulk_in_endpoint: 0x82,
bulk_out_endpoint: 0x01,
}
}
fn open_stub() -> StubTransport {
let mut stub = StubTransport::new(test_config());
stub.state = TransportState::Active;
stub
}
fn make_cc_event(opcode: u16, status: u8) -> HciEvent {
let params = vec![0x01, opcode as u8, (opcode >> 8) as u8, status];
HciEvent {
event_code: EVT_COMMAND_COMPLETE,
parameters: params,
}
}
fn make_cs_event(status: u8, opcode: u16) -> HciEvent {
let params = vec![status, 0x01, opcode as u8, (opcode >> 8) as u8];
HciEvent {
event_code: EVT_COMMAND_STATUS,
parameters: params,
}
}
#[test]
fn stub_starts_closed() {
let stub = StubTransport::new(test_config());
assert_eq!(stub.state(), TransportState::Closed);
}
#[test]
fn send_command_appears_in_drain() {
let mut stub = open_stub();
let cmd = HciCommand::new(OP_RESET, vec![]);
stub.send_command(&cmd).unwrap();
let sent = stub.drain_sent_commands();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0], cmd);
}
#[test]
fn inject_event_recv_returns_it() {
let mut stub = open_stub();
let evt = make_cc_event(OP_RESET, 0x00);
stub.inject_event(evt.clone());
let received = stub.recv_event().unwrap();
assert_eq!(received, Some(evt));
}
#[test]
fn send_acl_appears_in_drain() {
let mut stub = open_stub();
let acl = HciAcl::new(0x0001, 0x00, 0x00, vec![0xDE, 0xAD]);
stub.send_acl(&acl).unwrap();
let sent = stub.drain_sent_acl();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0], acl);
}
#[test]
fn inject_acl_recv_returns_it() {
let mut stub = open_stub();
let acl = HciAcl::new(0x0001, 0x00, 0x00, vec![0xCA, 0xFE]);
stub.inject_acl(acl.clone());
let received = stub.recv_acl().unwrap();
assert_eq!(received, Some(acl));
}
#[test]
fn close_transitions_to_closed() {
let mut stub = open_stub();
assert_eq!(stub.state(), TransportState::Active);
stub.close().unwrap();
assert_eq!(stub.state(), TransportState::Closed);
}
#[test]
fn recv_event_returns_none_when_empty() {
let mut stub = open_stub();
assert_eq!(stub.recv_event().unwrap(), None);
}
#[test]
fn recv_acl_returns_none_when_empty() {
let mut stub = open_stub();
assert_eq!(stub.recv_acl().unwrap(), None);
}
#[test]
fn drain_sent_commands_empty_returns_empty_vec() {
let mut stub = open_stub();
assert!(stub.drain_sent_commands().is_empty());
}
#[test]
fn drain_sent_acl_empty_returns_empty_vec() {
let mut stub = open_stub();
assert!(stub.drain_sent_acl().is_empty());
}
#[test]
fn send_command_on_closed_returns_error() {
let mut stub = StubTransport::new(test_config());
let cmd = HciCommand::new(OP_RESET, vec![]);
let result = stub.send_command(&cmd);
assert!(result.is_err());
assert_eq!(result.err().map(|e| e.kind()), Some(io::ErrorKind::NotConnected));
}
#[test]
fn recv_event_on_closed_returns_error() {
let mut stub = StubTransport::new(test_config());
let result = stub.recv_event();
assert!(result.is_err());
assert_eq!(result.err().map(|e| e.kind()), Some(io::ErrorKind::NotConnected));
}
#[test]
fn send_acl_on_closed_returns_error() {
let mut stub = StubTransport::new(test_config());
let acl = HciAcl::new(0x0001, 0x00, 0x00, vec![]);
let result = stub.send_acl(&acl);
assert!(result.is_err());
assert_eq!(result.err().map(|e| e.kind()), Some(io::ErrorKind::NotConnected));
}
#[test]
fn recv_acl_on_closed_returns_error() {
let mut stub = StubTransport::new(test_config());
let result = stub.recv_acl();
assert!(result.is_err());
assert_eq!(result.err().map(|e| e.kind()), Some(io::ErrorKind::NotConnected));
}
#[test]
fn multiple_commands_queue_in_order() {
let mut stub = open_stub();
let cmd1 = HciCommand::new(OP_RESET, vec![]);
let cmd2 = HciCommand::new(0x1009, vec![0x01]);
stub.send_command(&cmd1).unwrap();
stub.send_command(&cmd2).unwrap();
let sent = stub.drain_sent_commands();
assert_eq!(sent.len(), 2);
assert_eq!(sent[0], cmd1);
assert_eq!(sent[1], cmd2);
}
#[test]
fn multiple_events_dequeue_in_order() {
let mut stub = open_stub();
let evt1 = make_cc_event(OP_RESET, 0x00);
let evt2 = make_cs_event(0x00, 0x0405);
stub.inject_event(evt1.clone());
stub.inject_event(evt2.clone());
assert_eq!(stub.recv_event().unwrap(), Some(evt1));
assert_eq!(stub.recv_event().unwrap(), Some(evt2));
assert_eq!(stub.recv_event().unwrap(), None);
}
#[test]
fn drain_clears_so_second_drain_is_empty() {
let mut stub = open_stub();
stub.send_command(&HciCommand::new(OP_RESET, vec![])).unwrap();
assert_eq!(stub.drain_sent_commands().len(), 1);
assert!(stub.drain_sent_commands().is_empty());
}
#[test]
fn close_then_reopen_cycle() {
let mut stub = open_stub();
stub.close().unwrap();
assert_eq!(stub.state(), TransportState::Closed);
stub.state = TransportState::Active;
let cmd = HciCommand::new(OP_RESET, vec![]);
stub.send_command(&cmd).unwrap();
assert_eq!(stub.drain_sent_commands().len(), 1);
}
}
@@ -14,6 +14,8 @@ use backend::connection_state_lines;
use backend::{Backend, StubBackend}; use backend::{Backend, StubBackend};
use bond_store::BondRecord; use bond_store::BondRecord;
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
use log::error;
#[cfg(target_os = "redox")]
use log::info; use log::info;
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
use log::warn; use log::warn;
@@ -47,12 +49,14 @@ fn notify_scheme_ready(notify_fd: Option<RawFd>, socket: &Socket, scheme: &mut B
return; return;
}; };
let cap_id = scheme let Ok(cap_id) = scheme.scheme_root() else {
.scheme_root() warn!("redbear-btctl: scheme_root failed; continuing without scheme notification");
.expect("redbear-btctl: scheme_root failed"); return;
let cap_fd = socket };
.create_this_scheme_fd(0, cap_id, 0, 0) let Ok(cap_fd) = socket.create_this_scheme_fd(0, cap_id, 0, 0) else {
.expect("redbear-btctl: create_this_scheme_fd failed"); warn!("redbear-btctl: create_this_scheme_fd failed; continuing without scheme notification");
return;
};
if let Err(err) = syscall::call_wo( if let Err(err) = syscall::call_wo(
notify_fd as usize, notify_fd as usize,
@@ -435,27 +439,53 @@ fn main() {
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
{ {
let notify_fd = unsafe { get_init_notify_fd() }; let notify_fd = unsafe { get_init_notify_fd() };
let socket = Socket::create().expect("redbear-btctl: failed to create scheme socket"); let socket = match Socket::create() {
Ok(s) => s,
Err(err) => {
error!("redbear-btctl: failed to create scheme socket: {err}");
process::exit(1);
}
};
let mut scheme = BtCtlScheme::new(build_backend()); let mut scheme = BtCtlScheme::new(build_backend());
let mut state = redox_scheme::scheme::SchemeState::new(); let mut state = redox_scheme::scheme::SchemeState::new();
notify_scheme_ready(notify_fd, &socket, &mut scheme); notify_scheme_ready(notify_fd, &socket, &mut scheme);
libredox::call::setrens(0, 0).expect("redbear-btctl: failed to enter null namespace"); match libredox::call::setrens(0, 0) {
info!("redbear-btctl: registered scheme:btctl"); Ok(_) => info!("redbear-btctl: registered scheme:btctl"),
Err(err) => {
while let Some(request) = socket error!("redbear-btctl: failed to enter null namespace: {err}");
.next_request(SignalBehavior::Restart) process::exit(1);
.expect("redbear-btctl: failed to read scheme request")
{
if let redox_scheme::RequestKind::Call(request) = request.kind() {
let response = request.handle_sync(&mut scheme, &mut state);
socket
.write_response(response, SignalBehavior::Restart)
.expect("redbear-btctl: failed to write response");
} }
} }
process::exit(0); let mut exit_code = 0;
loop {
let request = match socket.next_request(SignalBehavior::Restart) {
Ok(Some(req)) => req,
Ok(None) => {
info!("redbear-btctl: scheme socket closed, shutting down");
break;
}
Err(err) => {
error!("redbear-btctl: failed to read scheme request: {err}");
exit_code = 1;
break;
}
};
match request.kind() {
redox_scheme::RequestKind::Call(request) => {
let response = request.handle_sync(&mut scheme, &mut state);
if let Err(err) = socket.write_response(response, SignalBehavior::Restart) {
error!("redbear-btctl: failed to write response: {err}");
exit_code = 1;
break;
}
}
_ => {}
}
}
process::exit(exit_code);
} }
} }
@@ -21,33 +21,44 @@ fn init_logging(level: LevelFilter) {
} }
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
unsafe fn get_init_notify_fd() -> RawFd { unsafe fn get_init_notify_fd() -> Option<RawFd> {
let fd: RawFd = env::var("INIT_NOTIFY") let Ok(value) = env::var("INIT_NOTIFY") else {
.expect("redbear-wifictl: INIT_NOTIFY not set") return None;
.parse() };
.expect("redbear-wifictl: INIT_NOTIFY is not a valid fd"); let Ok(fd) = value.parse::<RawFd>() else {
return None;
};
unsafe { unsafe {
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC); libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
} }
fd Some(fd)
} }
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut WifiCtlScheme) { fn notify_scheme_ready(notify_fd: Option<RawFd>, socket: &Socket, scheme: &mut WifiCtlScheme) {
let cap_id = scheme let Some(notify_fd) = notify_fd else {
.scheme_root() return;
.expect("redbear-wifictl: scheme_root failed"); };
let cap_fd = socket
.create_this_scheme_fd(0, cap_id, 0, 0)
.expect("redbear-wifictl: create_this_scheme_fd failed");
syscall::call_wo( let Ok(cap_id) = scheme.scheme_root() else {
log::warn!("redbear-wifictl: scheme_root failed; continuing without scheme notification");
return;
};
let Ok(cap_fd) = socket.create_this_scheme_fd(0, cap_id, 0, 0) else {
log::warn!("redbear-wifictl: create_this_scheme_fd failed; continuing without scheme notification");
return;
};
if let Err(err) = syscall::call_wo(
notify_fd as usize, notify_fd as usize,
&libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(), &libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(),
syscall::CallFlags::FD, syscall::CallFlags::FD,
&[], &[],
) ) {
.expect("redbear-wifictl: failed to notify init that scheme is ready"); log::warn!(
"redbear-wifictl: failed to notify init that scheme is ready ({err}); continuing with manual startup"
);
}
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]