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:
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,6 @@
|
||||
mod hci;
|
||||
mod usb_transport;
|
||||
|
||||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -8,6 +11,11 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
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 BLUETOOTH_USB_CLASS: u8 = 0xE0;
|
||||
const BLUETOOTH_USB_SUBCLASS: u8 = 0x01;
|
||||
@@ -21,6 +29,7 @@ struct UsbBluetoothAdapter {
|
||||
device_id: u16,
|
||||
bus: String,
|
||||
device_path: PathBuf,
|
||||
endpoints: HciEndpoints,
|
||||
}
|
||||
|
||||
impl UsbBluetoothAdapter {
|
||||
@@ -32,17 +41,21 @@ impl UsbBluetoothAdapter {
|
||||
vendor_id: 0,
|
||||
device_id: 0,
|
||||
bus: "stub".to_string(),
|
||||
endpoints: HciEndpoints::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn detail_line(&self, index: usize) -> String {
|
||||
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.vendor_id,
|
||||
self.device_id,
|
||||
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)]
|
||||
struct TransportConfig {
|
||||
adapters: Vec<UsbBluetoothAdapter>,
|
||||
controller_family: String,
|
||||
status_file: PathBuf,
|
||||
controller_info: ControllerInfo,
|
||||
}
|
||||
|
||||
impl TransportConfig {
|
||||
@@ -84,6 +243,7 @@ impl TransportConfig {
|
||||
status_file: std::env::var_os("REDBEAR_BTUSB_STATUS_FILE")
|
||||
.map(PathBuf::from)
|
||||
.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()));
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -413,12 +601,14 @@ fn try_collect_bluetooth_adapter(
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
if descriptor.looks_like_bluetooth() {
|
||||
let endpoints = parse_hci_endpoints_from_descriptors(&raw).unwrap_or_default();
|
||||
adapters.push(UsbBluetoothAdapter {
|
||||
name: String::new(),
|
||||
vendor_id: descriptor.vendor_id,
|
||||
device_id: descriptor.device_id,
|
||||
bus: bus.to_string(),
|
||||
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"))
|
||||
}
|
||||
|
||||
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"))]
|
||||
fn daemon_main(_config: &TransportConfig) -> Result<(), 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();
|
||||
|
||||
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()?;
|
||||
let _status_file_guard = StatusFileGuard {
|
||||
path: &config.status_file,
|
||||
@@ -526,7 +804,9 @@ fn daemon_main(config: &TransportConfig) -> Result<(), String> {
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(30));
|
||||
let controller_info = runtime_config.controller_info.clone();
|
||||
runtime_config = config.refreshed();
|
||||
runtime_config.controller_info = controller_info;
|
||||
runtime_config.write_status_file()?;
|
||||
}
|
||||
}
|
||||
@@ -554,6 +834,7 @@ mod tests {
|
||||
adapters: vec![stub_adapter("hci0")],
|
||||
controller_family: "usb-bounded-test".to_string(),
|
||||
status_file,
|
||||
controller_info: ControllerInfo::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,4 +983,294 @@ mod tests {
|
||||
let adapters = probe_usb_bluetooth_adapters_in(&root).unwrap();
|
||||
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 bond_store::BondRecord;
|
||||
#[cfg(target_os = "redox")]
|
||||
use log::error;
|
||||
#[cfg(target_os = "redox")]
|
||||
use log::info;
|
||||
#[cfg(target_os = "redox")]
|
||||
use log::warn;
|
||||
@@ -47,12 +49,14 @@ fn notify_scheme_ready(notify_fd: Option<RawFd>, socket: &Socket, scheme: &mut B
|
||||
return;
|
||||
};
|
||||
|
||||
let cap_id = scheme
|
||||
.scheme_root()
|
||||
.expect("redbear-btctl: scheme_root failed");
|
||||
let cap_fd = socket
|
||||
.create_this_scheme_fd(0, cap_id, 0, 0)
|
||||
.expect("redbear-btctl: create_this_scheme_fd failed");
|
||||
let Ok(cap_id) = scheme.scheme_root() else {
|
||||
warn!("redbear-btctl: scheme_root failed; continuing without scheme notification");
|
||||
return;
|
||||
};
|
||||
let Ok(cap_fd) = socket.create_this_scheme_fd(0, cap_id, 0, 0) else {
|
||||
warn!("redbear-btctl: create_this_scheme_fd failed; continuing without scheme notification");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = syscall::call_wo(
|
||||
notify_fd as usize,
|
||||
@@ -435,27 +439,53 @@ fn main() {
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
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 state = redox_scheme::scheme::SchemeState::new();
|
||||
|
||||
notify_scheme_ready(notify_fd, &socket, &mut scheme);
|
||||
libredox::call::setrens(0, 0).expect("redbear-btctl: failed to enter null namespace");
|
||||
info!("redbear-btctl: registered scheme:btctl");
|
||||
|
||||
while let Some(request) = socket
|
||||
.next_request(SignalBehavior::Restart)
|
||||
.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");
|
||||
match libredox::call::setrens(0, 0) {
|
||||
Ok(_) => info!("redbear-btctl: registered scheme:btctl"),
|
||||
Err(err) => {
|
||||
error!("redbear-btctl: failed to enter null namespace: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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")]
|
||||
unsafe fn get_init_notify_fd() -> RawFd {
|
||||
let fd: RawFd = env::var("INIT_NOTIFY")
|
||||
.expect("redbear-wifictl: INIT_NOTIFY not set")
|
||||
.parse()
|
||||
.expect("redbear-wifictl: INIT_NOTIFY is not a valid fd");
|
||||
unsafe fn get_init_notify_fd() -> Option<RawFd> {
|
||||
let Ok(value) = env::var("INIT_NOTIFY") else {
|
||||
return None;
|
||||
};
|
||||
let Ok(fd) = value.parse::<RawFd>() else {
|
||||
return None;
|
||||
};
|
||||
unsafe {
|
||||
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
|
||||
}
|
||||
fd
|
||||
Some(fd)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut WifiCtlScheme) {
|
||||
let cap_id = scheme
|
||||
.scheme_root()
|
||||
.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");
|
||||
fn notify_scheme_ready(notify_fd: Option<RawFd>, socket: &Socket, scheme: &mut WifiCtlScheme) {
|
||||
let Some(notify_fd) = notify_fd else {
|
||||
return;
|
||||
};
|
||||
|
||||
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,
|
||||
&libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(),
|
||||
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)]
|
||||
|
||||
Reference in New Issue
Block a user