diff --git a/local/recipes/drivers/redbear-btusb/source/src/hci.rs b/local/recipes/drivers/redbear-btusb/source/src/hci.rs new file mode 100644 index 00000000..08230d52 --- /dev/null +++ b/local/recipes/drivers/redbear-btusb/source/src/hci.rs @@ -0,0 +1,1380 @@ +//! HCI (Host Controller Interface) protocol types for Bluetooth USB transport. +//! +//! Defines wire-format types for HCI command, event, ACL, SCO, and ISO packets, +//! along with constants for opcodes, event codes, status codes, and LE meta subevents. + +// --------------------------------------------------------------------------- +// Constants — Opcodes (OGF | OCF, u16 little-endian on wire) +// --------------------------------------------------------------------------- + +// Link Control +pub const OP_CREATE_CONNECTION: u16 = 0x0405; +pub const OP_DISCONNECT: u16 = 0x0406; + +// Controller & Baseband +pub const OP_RESET: u16 = 0x0C03; +pub const OP_SET_EVENT_MASK: u16 = 0x0C01; +pub const OP_READ_LOCAL_VERSION: u16 = 0x1001; +pub const OP_READ_LOCAL_SUPPORTED_COMMANDS: u16 = 0x1002; +pub const OP_READ_BD_ADDR: u16 = 0x1009; +pub const OP_SET_EVENT_FILTER: u16 = 0x0C05; +pub const OP_WRITE_INQUIRY_MODE: u16 = 0x0C45; +pub const OP_WRITE_SCAN_ENABLE: u16 = 0x0C1A; + +// LE Controller +pub const OP_LE_SET_EVENT_MASK: u16 = 0x2001; +pub const OP_LE_READ_BUFFER_SIZE: u16 = 0x2002; +pub const OP_LE_READ_LOCAL_SUPPORTED_FEATURES: u16 = 0x2003; +pub const OP_LE_SET_RANDOM_ADDRESS: u16 = 0x2005; +pub const OP_LE_SET_ADVERTISING_PARAMETERS: u16 = 0x2006; +pub const OP_LE_SET_ADVERTISING_DATA: u16 = 0x2008; +pub const OP_LE_SET_SCAN_PARAMETERS: u16 = 0x200B; +pub const OP_LE_SET_SCAN_ENABLE: u16 = 0x200C; +pub const OP_LE_CREATE_CONNECTION: u16 = 0x200D; +pub const OP_LE_CREATE_CONNECTION_CANCEL: u16 = 0x200E; +pub const OP_LE_CONNECTION_UPDATE: u16 = 0x2013; +pub const OP_LE_READ_REMOTE_FEATURES: u16 = 0x2016; + +// --------------------------------------------------------------------------- +// Constants — Status Codes +// --------------------------------------------------------------------------- + +pub const STATUS_SUCCESS: u8 = 0x00; +pub const STATUS_UNKNOWN_COMMAND: u8 = 0x01; +pub const STATUS_UNKNOWN_CONNECTION_ID: u8 = 0x02; +pub const STATUS_HARDWARE_FAILURE: u8 = 0x03; +pub const STATUS_PAGE_TIMEOUT: u8 = 0x04; +pub const STATUS_AUTHENTICATION_FAILURE: u8 = 0x05; + +// --------------------------------------------------------------------------- +// Constants — Event Codes +// --------------------------------------------------------------------------- + +pub const EVT_INQUIRY_COMPLETE: u8 = 0x01; +pub const EVT_INQUIRY_RESULT: u8 = 0x02; +pub const EVT_CONNECTION_COMPLETE: u8 = 0x03; +pub const EVT_DISCONNECTION_COMPLETE: u8 = 0x05; +pub const EVT_AUTHENTICATION_COMPLETE: u8 = 0x06; +pub const EVT_REMOTE_NAME_REQUEST_COMPLETE: u8 = 0x07; +pub const EVT_ENCRYPTION_CHANGE: u8 = 0x08; +pub const EVT_COMMAND_COMPLETE: u8 = 0x0E; +pub const EVT_COMMAND_STATUS: u8 = 0x0F; +pub const EVT_NUMBER_OF_COMPLETED_PACKETS: u8 = 0x13; +pub const EVT_LE_META: u8 = 0x3E; + +// --------------------------------------------------------------------------- +// Constants — LE Meta Subevent Codes +// --------------------------------------------------------------------------- + +pub const LE_CONNECTION_COMPLETE: u8 = 0x01; +pub const LE_ADVERTISING_REPORT: u8 = 0x02; +pub const LE_CONNECTION_UPDATE_COMPLETE: u8 = 0x03; +pub const LE_READ_REMOTE_FEATURES_COMPLETE: u8 = 0x04; +pub const LE_LONG_TERM_KEY_REQUEST: u8 = 0x05; + +// --------------------------------------------------------------------------- +// Packet Indicator (u8 prefix on USB transport) +// --------------------------------------------------------------------------- + +/// USB HCI transport packet indicator byte. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum PacketIndicator { + Command = 0x01, + Acl = 0x02, + Sco = 0x03, + Event = 0x04, + Iso = 0x05, +} + +impl PacketIndicator { + pub fn from_u8(value: u8) -> Option { + match value { + 0x01 => Some(Self::Command), + 0x02 => Some(Self::Acl), + 0x03 => Some(Self::Sco), + 0x04 => Some(Self::Event), + 0x05 => Some(Self::Iso), + _ => None, + } + } +} + +// --------------------------------------------------------------------------- +// Parse Error +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HciParseError { + InsufficientData { expected: usize, actual: usize }, + InvalidPacketIndicator(u8), + InvalidParameterLength { declared: usize, available: usize }, +} + +// --------------------------------------------------------------------------- +// HciCommand +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HciCommand { + pub opcode: u16, + pub parameters: Vec, +} + +impl HciCommand { + pub fn new(opcode: u16, parameters: Vec) -> Self { + Self { opcode, parameters } + } + + /// Build the wire format: `[opcode_lo, opcode_hi, param_length, params...]` + pub fn to_bytes(&self) -> Vec { + let param_len = u8::try_from(self.parameters.len()).unwrap_or(0xFF); + let mut buf = Vec::with_capacity(3 + self.parameters.len()); + buf.push(self.opcode as u8); + buf.push((self.opcode >> 8) as u8); + buf.push(param_len); + buf.extend_from_slice(&self.parameters); + buf + } + + /// Parse from wire format (without the packet indicator byte). + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(HciParseError::InsufficientData { + expected: 3, + actual: data.len(), + }); + } + let opcode = u16::from_le_bytes([data[0], data[1]]); + let param_len = data[2] as usize; + if data.len() < 3 + param_len { + return Err(HciParseError::InvalidParameterLength { + declared: param_len, + available: data.len() - 3, + }); + } + let parameters = data[3..3 + param_len].to_vec(); + Ok(Self { opcode, parameters }) + } + + /// Total wire length including 3-byte header. + pub fn wire_length(&self) -> usize { + 3 + self.parameters.len() + } +} + +// --------------------------------------------------------------------------- +// HciEvent +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HciEvent { + pub event_code: u8, + pub parameters: Vec, +} + +impl HciEvent { + /// Parse from wire format (without the packet indicator byte). + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 2 { + return Err(HciParseError::InsufficientData { + expected: 2, + actual: data.len(), + }); + } + let event_code = data[0]; + let param_len = data[1] as usize; + if data.len() < 2 + param_len { + return Err(HciParseError::InvalidParameterLength { + declared: param_len, + available: data.len() - 2, + }); + } + let parameters = data[2..2 + param_len].to_vec(); + Ok(Self { + event_code, + parameters, + }) + } + + /// Total wire length including 2-byte header. + pub fn wire_length(&self) -> usize { + 2 + self.parameters.len() + } + + /// Check if this is a Command Complete event. + pub fn is_command_complete(&self) -> bool { + self.event_code == EVT_COMMAND_COMPLETE + } + + /// Check if this is a Command Status event. + pub fn is_command_status(&self) -> bool { + self.event_code == EVT_COMMAND_STATUS + } + + /// Check if this is an LE Meta event. + pub fn is_le_meta(&self) -> bool { + self.event_code == EVT_LE_META + } + + /// Extract return parameters from Command Complete event. + /// + /// CC format: `[num_hci_command_packets, opcode_lo, opcode_hi, status, return_params...]` + /// + /// Returns `(num_hci_command_packets, opcode, return_params)` or `None` if not CC + /// or parameters too short. + pub fn command_complete_params(&self) -> Option<(u8, u16, &[u8])> { + if !self.is_command_complete() { + return None; + } + // Need at least: num_hci_packets(1) + opcode(2) + status(1) = 4 bytes + if self.parameters.len() < 4 { + return None; + } + let num_packets = self.parameters[0]; + let opcode = u16::from_le_bytes([self.parameters[1], self.parameters[2]]); + // return_params starts after status byte (index 3) + let return_params = &self.parameters[4..]; + Some((num_packets, opcode, return_params)) + } + + /// Extract fields from Command Status event. + /// + /// CS format: `[status, num_hci_command_packets, opcode_lo, opcode_hi]` + /// + /// Returns `(status, num_hci_command_packets, opcode)` or `None` if not CS + /// or parameters too short. + pub fn command_status_params(&self) -> Option<(u8, u8, u16)> { + if !self.is_command_status() { + return None; + } + // Need: status(1) + num_packets(1) + opcode(2) = 4 bytes + if self.parameters.len() < 4 { + return None; + } + let status = self.parameters[0]; + let num_packets = self.parameters[1]; + let opcode = u16::from_le_bytes([self.parameters[2], self.parameters[3]]); + Some((status, num_packets, opcode)) + } +} + +// --------------------------------------------------------------------------- +// HciAcl +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HciAcl { + pub handle: u16, + pub pb_flag: u8, + pub bc_flag: u8, + pub data: Vec, +} + +impl HciAcl { + pub fn new(handle: u16, pb_flag: u8, bc_flag: u8, data: Vec) -> Self { + Self { + handle, + pb_flag, + bc_flag, + data, + } + } + + /// Build wire format: `[handle_lo (with PB|BC), handle_hi, length_lo, length_hi, data...]` + pub fn to_bytes(&self) -> Vec { + let handle_word = + (self.handle & 0x0FFF) | ((self.pb_flag as u16 & 0x03) << 12) | ((self.bc_flag as u16 & 0x03) << 14); + let data_len = self.data.len(); + let mut buf = Vec::with_capacity(4 + data_len); + buf.push(handle_word as u8); + buf.push((handle_word >> 8) as u8); + buf.push(data_len as u8); + buf.push((data_len >> 8) as u8); + buf.extend_from_slice(&self.data); + buf + } + + /// Parse from wire format (without the packet indicator byte). + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 4 { + return Err(HciParseError::InsufficientData { + expected: 4, + actual: data.len(), + }); + } + let handle_word = u16::from_le_bytes([data[0], data[1]]); + let data_len = u16::from_le_bytes([data[2], data[3]]) as usize; + if data.len() < 4 + data_len { + return Err(HciParseError::InvalidParameterLength { + declared: data_len, + available: data.len() - 4, + }); + } + let handle = handle_word & 0x0FFF; + let pb_flag = ((handle_word >> 12) & 0x03) as u8; + let bc_flag = ((handle_word >> 14) & 0x03) as u8; + let payload = data[4..4 + data_len].to_vec(); + Ok(Self { + handle, + pb_flag, + bc_flag, + data: payload, + }) + } +} + +// --------------------------------------------------------------------------- +// HciPacketData — parsed packet payload +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HciPacketData { + Command(HciCommand), + Event(HciEvent), + Acl(HciAcl), + Sco(Vec), + Iso(Vec), +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/// Build a complete USB HCI command packet with packet indicator. +/// +/// Result: `[0x01, opcode_lo, opcode_hi, param_len, params...]` +pub fn build_usb_command(opcode: u16, parameters: Vec) -> Vec { + let cmd = HciCommand::new(opcode, parameters); + let mut buf = vec![PacketIndicator::Command as u8]; + buf.extend_from_slice(&cmd.to_bytes()); + buf +} + +/// Parse any HCI packet from USB transport data. +/// +/// Returns the packet indicator and the parsed payload data. +pub fn parse_usb_hci_packet(data: &[u8]) -> Result<(PacketIndicator, HciPacketData), HciParseError> { + if data.is_empty() { + return Err(HciParseError::InsufficientData { + expected: 1, + actual: 0, + }); + } + let indicator = PacketIndicator::from_u8(data[0]).ok_or(HciParseError::InvalidPacketIndicator(data[0]))?; + let payload = &data[1..]; + let packet_data = match indicator { + PacketIndicator::Command => HciPacketData::Command(HciCommand::from_bytes(payload)?), + PacketIndicator::Event => HciPacketData::Event(HciEvent::from_bytes(payload)?), + PacketIndicator::Acl => HciPacketData::Acl(HciAcl::from_bytes(payload)?), + PacketIndicator::Sco => HciPacketData::Sco(payload.to_vec()), + PacketIndicator::Iso => HciPacketData::Iso(payload.to_vec()), + }; + Ok((indicator, packet_data)) +} + +// --------------------------------------------------------------------------- +// Command Builders +// --------------------------------------------------------------------------- + +/// HCI Reset command (opcode 0x0C03, no parameters). +pub fn cmd_reset() -> HciCommand { + HciCommand::new(OP_RESET, Vec::new()) +} + +/// HCI Read BD_ADDR command (opcode 0x1009, no parameters). +pub fn cmd_read_bd_addr() -> HciCommand { + HciCommand::new(OP_READ_BD_ADDR, Vec::new()) +} + +/// HCI Read Local Version Information (opcode 0x1001, no parameters). +pub fn cmd_read_local_version() -> HciCommand { + HciCommand::new(OP_READ_LOCAL_VERSION, Vec::new()) +} + +/// HCI Read Local Supported Commands (opcode 0x1002, no parameters). +pub fn cmd_read_local_supported_commands() -> HciCommand { + HciCommand::new(OP_READ_LOCAL_SUPPORTED_COMMANDS, Vec::new()) +} + +/// HCI Set Event Mask (opcode 0x0C01). +/// +/// `mask` is the 8-byte event mask. +pub fn cmd_set_event_mask(mask: [u8; 8]) -> HciCommand { + HciCommand::new(OP_SET_EVENT_MASK, mask.to_vec()) +} + +/// HCI LE Set Event Mask (opcode 0x2001). +/// +/// `mask` is the 8-byte LE event mask. +pub fn cmd_le_set_event_mask(mask: [u8; 8]) -> HciCommand { + HciCommand::new(OP_LE_SET_EVENT_MASK, mask.to_vec()) +} + +/// HCI LE Read Buffer Size (opcode 0x2002, no parameters). +pub fn cmd_le_read_buffer_size() -> HciCommand { + HciCommand::new(OP_LE_READ_BUFFER_SIZE, Vec::new()) +} + +/// HCI LE Set Scan Parameters (opcode 0x200B). +/// +/// * `scan_type` — 0 = passive, 1 = active +/// * `interval`, `window` — in 0.625 ms units (0x0004–0x4000) +/// * `own_address_type` — 0 = public, 1 = random +/// * `filter_policy` — 0 = accept all, 1 = ignore non-directed from whitelist +pub fn cmd_le_set_scan_parameters( + scan_type: u8, + interval: u16, + window: u16, + own_address_type: u8, + filter_policy: u8, +) -> HciCommand { + let mut params = Vec::with_capacity(7); + params.push(scan_type); + params.extend_from_slice(&interval.to_le_bytes()); + params.extend_from_slice(&window.to_le_bytes()); + params.push(own_address_type); + params.push(filter_policy); + HciCommand::new(OP_LE_SET_SCAN_PARAMETERS, params) +} + +/// HCI LE Set Scan Enable (opcode 0x200C). +/// +/// * `enable` — 0 = disable, 1 = enable +/// * `filter_duplicates` — 0 = disable, 1 = enable +pub fn cmd_le_set_scan_enable(enable: u8, filter_duplicates: u8) -> HciCommand { + HciCommand::new(OP_LE_SET_SCAN_ENABLE, vec![enable, filter_duplicates]) +} + +/// HCI LE Create Connection (opcode 0x200D). +#[allow(clippy::too_many_arguments)] +pub fn cmd_le_create_connection( + scan_interval: u16, + scan_window: u16, + initiator_filter_policy: u8, + peer_address_type: u8, + peer_address: &[u8; 6], + own_address_type: u8, + conn_interval_min: u16, + conn_interval_max: u16, + conn_latency: u16, + supervision_timeout: u16, + min_ce_length: u16, + max_ce_length: u16, +) -> HciCommand { + let mut params = Vec::with_capacity(25); + params.extend_from_slice(&scan_interval.to_le_bytes()); + params.extend_from_slice(&scan_window.to_le_bytes()); + params.push(initiator_filter_policy); + params.push(peer_address_type); + params.extend_from_slice(peer_address); + params.push(own_address_type); + params.extend_from_slice(&conn_interval_min.to_le_bytes()); + params.extend_from_slice(&conn_interval_max.to_le_bytes()); + params.extend_from_slice(&conn_latency.to_le_bytes()); + params.extend_from_slice(&supervision_timeout.to_le_bytes()); + params.extend_from_slice(&min_ce_length.to_le_bytes()); + params.extend_from_slice(&max_ce_length.to_le_bytes()); + HciCommand::new(OP_LE_CREATE_CONNECTION, params) +} + +/// HCI LE Create Connection Cancel (opcode 0x200E, no parameters). +pub fn cmd_le_create_connection_cancel() -> HciCommand { + HciCommand::new(OP_LE_CREATE_CONNECTION_CANCEL, Vec::new()) +} + +/// HCI Disconnect (opcode 0x0406). +/// +/// * `connection_handle` — the connection handle to disconnect +/// * `reason` — error code (e.g. 0x13 for remote user terminated, 0x16 for local host terminated) +pub fn cmd_disconnect(connection_handle: u16, reason: u8) -> HciCommand { + let mut params = Vec::with_capacity(3); + params.extend_from_slice(&connection_handle.to_le_bytes()); + params.push(reason); + HciCommand::new(OP_DISCONNECT, params) +} + +// --------------------------------------------------------------------------- +// Event Result Types +// --------------------------------------------------------------------------- + +/// Result of HCI Read BD_ADDR Command Complete. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BdAddrResult { + pub status: u8, + pub address: [u8; 6], +} + +/// Result of HCI Read Local Version Command Complete. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LocalVersionResult { + pub status: u8, + pub hci_version: u8, + pub hci_revision: u16, + pub lmp_version: u8, + pub manufacturer_name: u16, + pub lmp_subversion: u16, +} + +/// Parsed LE Advertising Report entry. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LeAdvertisingReport { + /// 0 = ADV_IND, 1 = ADV_DIRECT_IND, 2 = ADV_SCAN_IND, 3 = SCAN_RSP, 4 = ADV_NONCONN_IND + pub event_type: u8, + /// 0 = public, 1 = random + pub address_type: u8, + pub address: [u8; 6], + pub data: Vec, + pub rssi: i8, +} + +/// Parsed LE Connection Complete event. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LeConnectionComplete { + pub status: u8, + pub connection_handle: u16, + pub role: u8, + pub peer_address_type: u8, + pub peer_address: [u8; 6], + pub conn_interval: u16, + pub conn_latency: u16, + pub supervision_timeout: u16, + pub master_clock_accuracy: u8, +} + +/// Result of HCI LE Read Buffer Size Command Complete. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LeBufferSizeResult { + pub status: u8, + pub le_acl_data_packet_length: u16, + pub total_num_le_acl_data_packets: u8, +} + +// --------------------------------------------------------------------------- +// Event Parsers +// --------------------------------------------------------------------------- + +/// Parse Read BD_ADDR from Command Complete event return parameters. +/// +/// Returns `None` if the event is not a Command Complete for `OP_READ_BD_ADDR` +/// or the return parameters are too short. +pub fn parse_read_bd_addr(event: &HciEvent) -> Option { + let (num_packets, opcode, return_params) = event.command_complete_params()?; + if opcode != OP_READ_BD_ADDR { + return None; + } + let _ = num_packets; + // command_complete_params() returns parameters[4..] which already excludes + // num_packets(1) + opcode(2) + status(1), so return_params is just the BD addr + if return_params.len() < 6 { + return None; + } + let mut address = [0u8; 6]; + address.copy_from_slice(&return_params[0..6]); + Some(BdAddrResult { + status: event.parameters.get(3).copied().unwrap_or(0xFF), + address, + }) +} + +/// Parse Read Local Version from Command Complete event return parameters. +pub fn parse_local_version(event: &HciEvent) -> Option { + let (num_packets, opcode, return_params) = event.command_complete_params()?; + if opcode != OP_READ_LOCAL_VERSION { + return None; + } + let _ = num_packets; + // return_params excludes status (already stripped by command_complete_params): + // hci_version(1) + hci_revision(2) + lmp_version(1) + manufacturer(2) + lmp_subversion(2) = 8 + if return_params.len() < 8 { + return None; + } + Some(LocalVersionResult { + status: event.parameters.get(3).copied().unwrap_or(0xFF), + hci_version: return_params[0], + hci_revision: u16::from_le_bytes([return_params[1], return_params[2]]), + lmp_version: return_params[3], + manufacturer_name: u16::from_le_bytes([return_params[4], return_params[5]]), + lmp_subversion: u16::from_le_bytes([return_params[6], return_params[7]]), + }) +} + +/// Parse LE Advertising Report from LE Meta event. +/// +/// Returns `None` if the event is not LE Meta or the subevent is not +/// `LE_ADVERTISING_REPORT`. +pub fn parse_le_advertising_reports(event: &HciEvent) -> Option> { + if !event.is_le_meta() { + return None; + } + if event.parameters.is_empty() || event.parameters[0] != LE_ADVERTISING_REPORT { + return None; + } + if event.parameters.len() < 2 { + return None; + } + let num_reports = event.parameters[1] as usize; + let mut reports = Vec::with_capacity(num_reports); + let mut offset = 2; + for _ in 0..num_reports { + // Need at least: event_type(1) + addr_type(1) + addr(6) + data_len(1) = 9 bytes + if offset + 9 > event.parameters.len() { + break; + } + let event_type = event.parameters[offset]; + let address_type = event.parameters[offset + 1]; + let mut address = [0u8; 6]; + address.copy_from_slice(&event.parameters[offset + 2..offset + 8]); + let data_len = event.parameters[offset + 8] as usize; + offset += 9; + + if offset + data_len + 1 > event.parameters.len() { + break; + } + let data = event.parameters[offset..offset + data_len].to_vec(); + offset += data_len; + let rssi = event.parameters[offset] as i8; + offset += 1; + + reports.push(LeAdvertisingReport { + event_type, + address_type, + address, + data, + rssi, + }); + } + Some(reports) +} + +/// Parse LE Connection Complete from LE Meta event. +pub fn parse_le_connection_complete(event: &HciEvent) -> Option { + if !event.is_le_meta() { + return None; + } + if event.parameters.is_empty() || event.parameters[0] != LE_CONNECTION_COMPLETE { + return None; + } + // Subevent(1) + status(1) + handle(2) + role(1) + addr_type(1) + addr(6) + // + interval(2) + latency(2) + timeout(2) + mca(1) = 19 + if event.parameters.len() < 19 { + return None; + } + let mut address = [0u8; 6]; + address.copy_from_slice(&event.parameters[6..12]); + Some(LeConnectionComplete { + status: event.parameters[1], + connection_handle: u16::from_le_bytes([event.parameters[2], event.parameters[3]]), + role: event.parameters[4], + peer_address_type: event.parameters[5], + peer_address: address, + conn_interval: u16::from_le_bytes([event.parameters[12], event.parameters[13]]), + conn_latency: u16::from_le_bytes([event.parameters[14], event.parameters[15]]), + supervision_timeout: u16::from_le_bytes([event.parameters[16], event.parameters[17]]), + master_clock_accuracy: event.parameters[18], + }) +} + +/// Parse LE Read Buffer Size from Command Complete event. +pub fn parse_le_buffer_size(event: &HciEvent) -> Option { + let (_num_packets, opcode, return_params) = event.command_complete_params()?; + if opcode != OP_LE_READ_BUFFER_SIZE { + return None; + } + if return_params.len() < 2 { + return None; + } + Some(LeBufferSizeResult { + status: event.parameters.get(3).copied().unwrap_or(0xFF), + le_acl_data_packet_length: u16::from_le_bytes([return_params[0], return_params[1]]), + total_num_le_acl_data_packets: return_params.get(2).copied().unwrap_or(0), + }) +} + +// --------------------------------------------------------------------------- +// Unit Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // -- PacketIndicator tests ---------------------------------------------- + + #[test] + fn packet_indicator_from_u8_all_valid() { + assert_eq!(PacketIndicator::from_u8(0x01), Some(PacketIndicator::Command)); + assert_eq!(PacketIndicator::from_u8(0x02), Some(PacketIndicator::Acl)); + assert_eq!(PacketIndicator::from_u8(0x03), Some(PacketIndicator::Sco)); + assert_eq!(PacketIndicator::from_u8(0x04), Some(PacketIndicator::Event)); + assert_eq!(PacketIndicator::from_u8(0x05), Some(PacketIndicator::Iso)); + } + + #[test] + fn packet_indicator_from_u8_invalid() { + assert_eq!(PacketIndicator::from_u8(0x00), None); + assert_eq!(PacketIndicator::from_u8(0x06), None); + assert_eq!(PacketIndicator::from_u8(0xFF), None); + } + + #[test] + fn packet_indicator_repr_values() { + assert_eq!(PacketIndicator::Command as u8, 0x01); + assert_eq!(PacketIndicator::Acl as u8, 0x02); + assert_eq!(PacketIndicator::Sco as u8, 0x03); + assert_eq!(PacketIndicator::Event as u8, 0x04); + assert_eq!(PacketIndicator::Iso as u8, 0x05); + } + + // -- HciCommand tests --------------------------------------------------- + + #[test] + fn hci_command_to_bytes_round_trip() { + let cmd = HciCommand::new(0x0C03, vec![0x00, 0x01, 0x02]); + let bytes = cmd.to_bytes(); + // opcode 0x0C03 → [0x03, 0x0C], param_len 3, params [0x00, 0x01, 0x02] + assert_eq!(bytes, vec![0x03, 0x0C, 0x03, 0x00, 0x01, 0x02]); + let parsed = HciCommand::from_bytes(&bytes).unwrap(); + assert_eq!(parsed, cmd); + } + + #[test] + fn hci_command_empty_params() { + let cmd = HciCommand::new(0x1009, vec![]); + let bytes = cmd.to_bytes(); + assert_eq!(bytes, vec![0x09, 0x10, 0x00]); + let parsed = HciCommand::from_bytes(&bytes).unwrap(); + assert_eq!(parsed, cmd); + } + + #[test] + fn hci_command_wire_length() { + let cmd = HciCommand::new(0x0C03, vec![0xAA, 0xBB]); + assert_eq!(cmd.wire_length(), 5); // 3 header + 2 params + } + + #[test] + fn hci_command_parse_truncated_header() { + let result = HciCommand::from_bytes(&[0x03]); + assert_eq!( + result, + Err(HciParseError::InsufficientData { + expected: 3, + actual: 1, + }) + ); + } + + #[test] + fn hci_command_parse_truncated_params() { + // Declares 5 params but only provides 2 + let result = HciCommand::from_bytes(&[0x03, 0x0C, 0x05, 0x00, 0x01]); + assert_eq!( + result, + Err(HciParseError::InvalidParameterLength { + declared: 5, + available: 2, + }) + ); + } + + #[test] + fn hci_command_parse_extra_data_ignored() { + // Extra trailing bytes beyond declared param_len are not consumed + let data = vec![0x03, 0x0C, 0x02, 0xAA, 0xBB, 0xCC, 0xDD]; + let parsed = HciCommand::from_bytes(&data).unwrap(); + assert_eq!(parsed.opcode, 0x0C03); + assert_eq!(parsed.parameters, vec![0xAA, 0xBB]); + } + + // -- HciEvent tests ----------------------------------------------------- + + #[test] + fn hci_event_command_complete_reset() { + // Command Complete for HCI Reset (opcode 0x0C03), status 0x00 + // Event wire: [event_code=0x0E, param_len, num_packets, opcode_lo, opcode_hi, status] + let wire: Vec = vec![0x0E, 0x04, 0x01, 0x03, 0x0C, 0x00]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert_eq!(evt.event_code, EVT_COMMAND_COMPLETE); + assert!(evt.is_command_complete()); + assert!(!evt.is_command_status()); + assert!(!evt.is_le_meta()); + assert_eq!(evt.parameters.len(), 4); + + let (num_packets, opcode, return_params) = evt.command_complete_params().unwrap(); + assert_eq!(num_packets, 1); + assert_eq!(opcode, 0x0C03); + assert!(return_params.is_empty()); + } + + #[test] + fn hci_event_command_status() { + // Command Status: [event_code=0x0F, param_len=4, status=0x00, num_packets=1, opcode_lo, opcode_hi] + let wire: Vec = vec![0x0F, 0x04, 0x00, 0x01, 0x05, 0x04]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert_eq!(evt.event_code, EVT_COMMAND_STATUS); + assert!(evt.is_command_status()); + + let (status, num_packets, opcode) = evt.command_status_params().unwrap(); + assert_eq!(status, STATUS_SUCCESS); + assert_eq!(num_packets, 1); + assert_eq!(opcode, 0x0405); + } + + #[test] + fn hci_event_command_complete_not_cs() { + let wire: Vec = vec![0x0E, 0x04, 0x01, 0x03, 0x0C, 0x00]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + // CC event should not return CS params + assert!(evt.command_status_params().is_none()); + } + + #[test] + fn hci_event_command_status_not_cc() { + let wire: Vec = vec![0x0F, 0x04, 0x00, 0x01, 0x05, 0x04]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert!(evt.command_complete_params().is_none()); + } + + #[test] + fn hci_event_le_advertising_report() { + // LE Meta event containing an Advertising Report + // Payload after header: subevent(1)+num_reports(1)+event_type(1)+addr_type(1)+addr(6)+data_len(1)+data(2)+rssi(1) = 14 + let wire: Vec = vec![ + 0x3E, 0x0E, // event_code, param_len=14 + 0x02, 0x01, // subevent=LE_ADVERTISING_REPORT, num_reports=1 + 0x00, // event_type=ADV_IND + 0x00, // addr_type=public + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // addr + 0x02, // data_len=2 + 0x01, 0x02, // data + 0xC5, // rssi + ]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert_eq!(evt.event_code, EVT_LE_META); + assert!(evt.is_le_meta()); + assert_eq!(evt.parameters[0], LE_ADVERTISING_REPORT); + assert_eq!(evt.parameters[1], 1); // num_reports + } + + #[test] + fn hci_event_wire_length() { + let wire: Vec = vec![0x0E, 0x04, 0x01, 0x03, 0x0C, 0x00]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert_eq!(evt.wire_length(), 6); + } + + #[test] + fn hci_event_parse_truncated() { + let result = HciEvent::from_bytes(&[0x0E]); + assert_eq!( + result, + Err(HciParseError::InsufficientData { + expected: 2, + actual: 1, + }) + ); + } + + #[test] + fn hci_event_parse_truncated_params() { + // Declares 4 bytes of params but only 1 is present + let result = HciEvent::from_bytes(&[0x0E, 0x04, 0x01]); + assert_eq!( + result, + Err(HciParseError::InvalidParameterLength { + declared: 4, + available: 1, + }) + ); + } + + #[test] + fn hci_event_command_complete_params_too_short() { + // CC event with only 1 byte of params (needs at least 4) + let wire: Vec = vec![0x0E, 0x01, 0x01]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert!(evt.command_complete_params().is_none()); + } + + #[test] + fn hci_event_command_status_params_too_short() { + // CS event with only 2 bytes of params (needs at least 4) + let wire: Vec = vec![0x0F, 0x02, 0x00, 0x01]; + let evt = HciEvent::from_bytes(&wire).unwrap(); + assert!(evt.command_status_params().is_none()); + } + + // -- HciAcl tests ------------------------------------------------------- + + #[test] + fn hci_acl_round_trip() { + let acl = HciAcl::new(0x0001, 0x02, 0x01, vec![0xDE, 0xAD, 0xBE, 0xEF]); + let bytes = acl.to_bytes(); + let parsed = HciAcl::from_bytes(&bytes).unwrap(); + assert_eq!(parsed, acl); + } + + #[test] + fn hci_acl_wire_format() { + let acl = HciAcl::new(0x0042, 0b01, 0b00, vec![0xCA, 0xFE]); + let bytes = acl.to_bytes(); + // handle=0x0042, PB=01, BC=00 → handle_word = 0x0042 | (0x01 << 12) = 0x1042 + assert_eq!(bytes[0], 0x42); // lo byte of 0x1042 + assert_eq!(bytes[1], 0x10); // hi byte of 0x1042 + assert_eq!(bytes[2], 0x02); // data_len lo + assert_eq!(bytes[3], 0x00); // data_len hi + assert_eq!(&bytes[4..], &[0xCA, 0xFE]); + } + + #[test] + fn hci_acl_empty_data() { + let acl = HciAcl::new(0x0000, 0x00, 0x00, vec![]); + let bytes = acl.to_bytes(); + assert_eq!(bytes.len(), 4); + let parsed = HciAcl::from_bytes(&bytes).unwrap(); + assert_eq!(parsed, acl); + } + + #[test] + fn hci_acl_parse_truncated() { + let result = HciAcl::from_bytes(&[0x42, 0x10]); + assert_eq!( + result, + Err(HciParseError::InsufficientData { + expected: 4, + actual: 2, + }) + ); + } + + #[test] + fn hci_acl_parse_truncated_data() { + // Declares 8 bytes of data but only provides 2 + let result = HciAcl::from_bytes(&[0x42, 0x10, 0x08, 0x00, 0xCA, 0xFE]); + assert_eq!( + result, + Err(HciParseError::InvalidParameterLength { + declared: 8, + available: 2, + }) + ); + } + + #[test] + fn hci_acl_max_handle_bits() { + // Handle uses 12 bits, PB 2 bits, BC 2 bits + let acl = HciAcl::new(0x0FFF, 0x03, 0x03, vec![0x01]); + let bytes = acl.to_bytes(); + let parsed = HciAcl::from_bytes(&bytes).unwrap(); + assert_eq!(parsed.handle, 0x0FFF); + assert_eq!(parsed.pb_flag, 0x03); + assert_eq!(parsed.bc_flag, 0x03); + assert_eq!(parsed.data, vec![0x01]); + } + + // -- build_usb_command tests ------------------------------------------- + + #[test] + fn build_usb_command_produces_correct_sequence() { + let packet = build_usb_command(0x0C03, vec![]); + assert_eq!(packet, vec![0x01, 0x03, 0x0C, 0x00]); + } + + #[test] + fn build_usb_command_with_params() { + let packet = build_usb_command(0x1009, vec![0xAA, 0xBB]); + assert_eq!(packet, vec![0x01, 0x09, 0x10, 0x02, 0xAA, 0xBB]); + } + + // -- parse_usb_hci_packet tests ---------------------------------------- + + #[test] + fn parse_usb_command_packet() { + let wire = build_usb_command(0x0C03, vec![0x00]); + let (indicator, data) = parse_usb_hci_packet(&wire).unwrap(); + assert_eq!(indicator, PacketIndicator::Command); + match data { + HciPacketData::Command(cmd) => { + assert_eq!(cmd.opcode, 0x0C03); + assert_eq!(cmd.parameters, vec![0x00]); + } + other => panic!("expected Command, got {other:?}"), + } + } + + #[test] + fn parse_usb_event_packet() { + // Build: [0x04, 0x0E, 0x04, 0x01, 0x03, 0x0C, 0x00] + let mut wire = vec![0x04]; + wire.extend_from_slice(&[0x0E, 0x04, 0x01, 0x03, 0x0C, 0x00]); + let (indicator, data) = parse_usb_hci_packet(&wire).unwrap(); + assert_eq!(indicator, PacketIndicator::Event); + match data { + HciPacketData::Event(evt) => { + assert_eq!(evt.event_code, EVT_COMMAND_COMPLETE); + } + other => panic!("expected Event, got {other:?}"), + } + } + + #[test] + fn parse_usb_acl_packet() { + let acl = HciAcl::new(0x0001, 0x00, 0x00, vec![0xDE]); + let mut wire = vec![0x02]; + wire.extend_from_slice(&acl.to_bytes()); + let (indicator, data) = parse_usb_hci_packet(&wire).unwrap(); + assert_eq!(indicator, PacketIndicator::Acl); + match data { + HciPacketData::Acl(parsed) => { + assert_eq!(parsed, acl); + } + other => panic!("expected Acl, got {other:?}"), + } + } + + #[test] + fn parse_usb_sco_packet() { + let wire: Vec = vec![0x03, 0x01, 0x02, 0x03]; + let (indicator, data) = parse_usb_hci_packet(&wire).unwrap(); + assert_eq!(indicator, PacketIndicator::Sco); + match data { + HciPacketData::Sco(payload) => { + assert_eq!(payload, vec![0x01, 0x02, 0x03]); + } + other => panic!("expected Sco, got {other:?}"), + } + } + + #[test] + fn parse_usb_iso_packet() { + let wire: Vec = vec![0x05, 0xAA, 0xBB]; + let (indicator, data) = parse_usb_hci_packet(&wire).unwrap(); + assert_eq!(indicator, PacketIndicator::Iso); + match data { + HciPacketData::Iso(payload) => { + assert_eq!(payload, vec![0xAA, 0xBB]); + } + other => panic!("expected Iso, got {other:?}"), + } + } + + #[test] + fn parse_usb_empty_data() { + let result = parse_usb_hci_packet(&[]); + assert_eq!( + result, + Err(HciParseError::InsufficientData { + expected: 1, + actual: 0, + }) + ); + } + + #[test] + fn parse_usb_invalid_indicator() { + let result = parse_usb_hci_packet(&[0x00, 0x01, 0x02]); + assert_eq!(result, Err(HciParseError::InvalidPacketIndicator(0x00))); + } + + #[test] + fn parse_usb_command_truncated_payload() { + // Indicator byte says command, but no payload + let result = parse_usb_hci_packet(&[0x01]); + assert_eq!( + result, + Err(HciParseError::InsufficientData { + expected: 3, + actual: 0, + }) + ); + } + + // -- Command builder tests ----------------------------------------------- + + #[test] + fn cmd_reset_builds_correct_command() { + let cmd = cmd_reset(); + assert_eq!(cmd.opcode, 0x0C03); + assert!(cmd.parameters.is_empty()); + } + + #[test] + fn cmd_read_bd_addr_builds_correct_command() { + let cmd = cmd_read_bd_addr(); + assert_eq!(cmd.opcode, 0x1009); + assert!(cmd.parameters.is_empty()); + } + + #[test] + fn cmd_le_set_scan_parameters_builds_correct_packet() { + let cmd = cmd_le_set_scan_parameters(0x01, 0x0030, 0x0020, 0x00, 0x00); + assert_eq!(cmd.opcode, 0x200B); + assert_eq!(cmd.parameters.len(), 7); + assert_eq!(cmd.parameters[0], 0x01); // scan_type + assert_eq!(u16::from_le_bytes([cmd.parameters[1], cmd.parameters[2]]), 0x0030); // interval + assert_eq!(u16::from_le_bytes([cmd.parameters[3], cmd.parameters[4]]), 0x0020); // window + assert_eq!(cmd.parameters[5], 0x00); // own_address_type + assert_eq!(cmd.parameters[6], 0x00); // filter_policy + } + + #[test] + fn cmd_le_set_scan_enable_builds_correct_packet() { + let cmd = cmd_le_set_scan_enable(0x01, 0x01); + assert_eq!(cmd.opcode, 0x200C); + assert_eq!(cmd.parameters, vec![0x01, 0x01]); + } + + #[test] + fn cmd_le_create_connection_builds_correct_packet() { + let peer_addr: [u8; 6] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; + let cmd = cmd_le_create_connection( + 0x0060, // scan_interval + 0x0030, // scan_window + 0x00, // initiator_filter_policy + 0x00, // peer_address_type + &peer_addr, + 0x00, // own_address_type + 0x0006, // conn_interval_min + 0x000C, // conn_interval_max + 0x0000, // conn_latency + 0x00C8, // supervision_timeout + 0x0001, // min_ce_length + 0x0002, // max_ce_length + ); + assert_eq!(cmd.opcode, 0x200D); + assert_eq!(cmd.parameters.len(), 25); + // scan_interval (2) + scan_window (2) + filter_policy (1) + addr_type (1) + // + addr (6) + own_addr_type (1) + interval_min (2) + interval_max (2) + // + latency (2) + timeout (2) + min_ce (2) + max_ce (2) = 25 + assert_eq!(u16::from_le_bytes([cmd.parameters[0], cmd.parameters[1]]), 0x0060); + assert_eq!(u16::from_le_bytes([cmd.parameters[2], cmd.parameters[3]]), 0x0030); + assert_eq!(cmd.parameters[4], 0x00); // filter_policy + assert_eq!(cmd.parameters[5], 0x00); // peer_address_type + assert_eq!(&cmd.parameters[6..12], &peer_addr); + assert_eq!(cmd.parameters[12], 0x00); // own_address_type + assert_eq!(u16::from_le_bytes([cmd.parameters[13], cmd.parameters[14]]), 0x0006); + assert_eq!(u16::from_le_bytes([cmd.parameters[15], cmd.parameters[16]]), 0x000C); + assert_eq!(u16::from_le_bytes([cmd.parameters[17], cmd.parameters[18]]), 0x0000); + assert_eq!(u16::from_le_bytes([cmd.parameters[19], cmd.parameters[20]]), 0x00C8); + assert_eq!(u16::from_le_bytes([cmd.parameters[21], cmd.parameters[22]]), 0x0001); + assert_eq!(u16::from_le_bytes([cmd.parameters[23], cmd.parameters[24]]), 0x0002); + } + + #[test] + fn cmd_disconnect_builds_correct_packet() { + let cmd = cmd_disconnect(0x0023, 0x13); + assert_eq!(cmd.opcode, 0x0406); + assert_eq!(cmd.parameters.len(), 3); + assert_eq!(u16::from_le_bytes([cmd.parameters[0], cmd.parameters[1]]), 0x0023); + assert_eq!(cmd.parameters[2], 0x13); + } + + // -- Event parser tests -------------------------------------------------- + + fn make_cc_event(opcode: u16, return_params: &[u8]) -> HciEvent { + // CC event: event_code=0x0E, params=[num_packets, opcode_lo, opcode_hi, return_params...] + let mut params = vec![0x01]; // num_packets = 1 + params.push(opcode as u8); + params.push((opcode >> 8) as u8); + params.extend_from_slice(return_params); + HciEvent { + event_code: EVT_COMMAND_COMPLETE, + parameters: params, + } + } + + fn make_le_meta_event(subevent_params: &[u8]) -> HciEvent { + let mut params = subevent_params.to_vec(); + HciEvent { + event_code: EVT_LE_META, + parameters: params, + } + } + + #[test] + fn parse_read_bd_addr_extracts_address() { + let return_params = [0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; // status + addr + let event = make_cc_event(OP_READ_BD_ADDR, &return_params); + let result = parse_read_bd_addr(&event); + let parsed = result.expect("should parse"); + assert_eq!(parsed.status, 0x00); + assert_eq!(parsed.address, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + } + + #[test] + fn parse_read_bd_addr_returns_none_for_wrong_opcode() { + let return_params = [0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; + let event = make_cc_event(OP_RESET, &return_params); + assert!(parse_read_bd_addr(&event).is_none()); + } + + #[test] + fn parse_local_version_extracts_all_fields() { + // status(1) + hci_version(1) + hci_revision(2) + lmp_version(1) + manufacturer(2) + lmp_sub(2) = 9 + let return_params: [u8; 9] = [ + 0x00, // status + 0x09, // hci_version (Bluetooth 5.0) + 0x00, 0x00, // hci_revision + 0x09, // lmp_version + 0x02, 0x00, // manufacturer_name (0x0002) + 0x01, 0x00, // lmp_subversion (0x0001) + ]; + let event = make_cc_event(OP_READ_LOCAL_VERSION, &return_params); + let result = parse_local_version(&event); + let parsed = result.expect("should parse"); + assert_eq!(parsed.status, 0x00); + assert_eq!(parsed.hci_version, 0x09); + assert_eq!(parsed.hci_revision, 0x0000); + assert_eq!(parsed.lmp_version, 0x09); + assert_eq!(parsed.manufacturer_name, 0x0002); + assert_eq!(parsed.lmp_subversion, 0x0001); + } + + #[test] + fn parse_le_advertising_reports_parses_single_report() { + // subevent(1) + num_reports(1) + event_type(1) + addr_type(1) + addr(6) + data_len(1) + data(2) + rssi(1) + let subevent_params: Vec = vec![ + LE_ADVERTISING_REPORT, // subevent + 0x01, // num_reports = 1 + 0x00, // event_type = ADV_IND + 0x01, // address_type = random + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // address + 0x02, // data_len = 2 + 0xDE, 0xAD, // data + 0xC5, // rssi + ]; + let event = make_le_meta_event(&subevent_params); + let reports = parse_le_advertising_reports(&event).expect("should parse"); + assert_eq!(reports.len(), 1); + assert_eq!(reports[0].event_type, 0x00); + assert_eq!(reports[0].address_type, 0x01); + assert_eq!(reports[0].address, [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + assert_eq!(reports[0].data, vec![0xDE, 0xAD]); + assert_eq!(reports[0].rssi, 0xC5u8 as i8); + } + + #[test] + fn parse_le_advertising_reports_parses_multiple_reports() { + // Two advertising reports back-to-back + let subevent_params: Vec = vec![ + LE_ADVERTISING_REPORT, // subevent + 0x02, // num_reports = 2 + // Report 1 + 0x00, // event_type + 0x00, // address_type + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // address + 0x01, // data_len = 1 + 0xFF, // data + 0x10, // rssi + // Report 2 + 0x03, // event_type = SCAN_RSP + 0x01, // address_type = random + 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // address + 0x00, // data_len = 0 + 0x20, // rssi + ]; + let event = make_le_meta_event(&subevent_params); + let reports = parse_le_advertising_reports(&event).expect("should parse"); + assert_eq!(reports.len(), 2); + assert_eq!(reports[0].event_type, 0x00); + assert_eq!(reports[0].address, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + assert_eq!(reports[0].data, vec![0xFF]); + assert_eq!(reports[0].rssi, 0x10i8); + assert_eq!(reports[1].event_type, 0x03); + assert_eq!(reports[1].address, [0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]); + assert!(reports[1].data.is_empty()); + assert_eq!(reports[1].rssi, 0x20i8); + } + + #[test] + fn parse_le_connection_complete_extracts_all_fields() { + // Subevent(1) + status(1) + handle(2) + role(1) + addr_type(1) + addr(6) + // + interval(2) + latency(2) + timeout(2) + mca(1) = 19 + let mut subevent_params: Vec = vec![ + LE_CONNECTION_COMPLETE, // subevent + 0x00, // status = success + 0x01, 0x00, // connection_handle = 0x0001 + 0x00, // role = master + 0x00, // peer_address_type = public + ]; + subevent_params.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); // peer_address + subevent_params.extend_from_slice(&0x0024u16.to_le_bytes()); // conn_interval + subevent_params.extend_from_slice(&0x0000u16.to_le_bytes()); // conn_latency + subevent_params.extend_from_slice(&0x01F4u16.to_le_bytes()); // supervision_timeout + subevent_params.push(0x01); // master_clock_accuracy + let event = make_le_meta_event(&subevent_params); + let result = parse_le_connection_complete(&event); + let parsed = result.expect("should parse"); + assert_eq!(parsed.status, 0x00); + assert_eq!(parsed.connection_handle, 0x0001); + assert_eq!(parsed.role, 0x00); + assert_eq!(parsed.peer_address_type, 0x00); + assert_eq!(parsed.peer_address, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); + assert_eq!(parsed.conn_interval, 0x0024); + assert_eq!(parsed.conn_latency, 0x0000); + assert_eq!(parsed.supervision_timeout, 0x01F4); + assert_eq!(parsed.master_clock_accuracy, 0x01); + } + + #[test] + fn parse_le_connection_complete_returns_none_for_wrong_subevent() { + let mut subevent_params = vec![LE_ADVERTISING_REPORT, 0x01]; // wrong subevent + subevent_params.extend_from_slice(&[0u8; 16]); + let event = make_le_meta_event(&subevent_params); + assert!(parse_le_connection_complete(&event).is_none()); + } + + #[test] + fn parse_le_buffer_size_extracts_fields() { + // status(1) + le_acl_data_packet_length(2) + total_num_le_acl_data_packets(1) = 4 + let return_params: [u8; 4] = [ + 0x00, // status + 0x1B, 0x02, // le_acl_data_packet_length = 0x021B (539) + 0x05, // total_num_le_acl_data_packets = 5 + ]; + let event = make_cc_event(OP_LE_READ_BUFFER_SIZE, &return_params); + let result = parse_le_buffer_size(&event); + let parsed = result.expect("should parse"); + assert_eq!(parsed.status, 0x00); + assert_eq!(parsed.le_acl_data_packet_length, 0x021B); + assert_eq!(parsed.total_num_le_acl_data_packets, 5); + } + + #[test] + fn parse_functions_return_none_for_truncated_data() { + // Truncated Read BD_ADDR — only 3 bytes of return params (need 7) + let return_params_short = [0x00, 0xAA, 0xBB]; + let event = make_cc_event(OP_READ_BD_ADDR, &return_params_short); + assert!(parse_read_bd_addr(&event).is_none()); + + // Truncated Local Version — only 5 bytes (need 9) + let return_params_short = [0x00, 0x09, 0x00, 0x00, 0x09]; + let event = make_cc_event(OP_READ_LOCAL_VERSION, &return_params_short); + assert!(parse_local_version(&event).is_none()); + + // Truncated LE Connection Complete — too short + let subevent_params = vec![LE_CONNECTION_COMPLETE, 0x00, 0x01, 0x00]; + let event = make_le_meta_event(&subevent_params); + assert!(parse_le_connection_complete(&event).is_none()); + + // Truncated LE Buffer Size — only 2 bytes of return params (need 3) + let return_params_short = [0x00, 0x1B]; + let event = make_cc_event(OP_LE_READ_BUFFER_SIZE, &return_params_short); + assert!(parse_le_buffer_size(&event).is_none()); + + // Non-CC event for BD_ADDR parser + let event = HciEvent { + event_code: EVT_COMMAND_STATUS, + parameters: vec![0x00, 0x01, 0x09, 0x10], + }; + assert!(parse_read_bd_addr(&event).is_none()); + + // Non-LE-Meta event for advertising parser + let event = HciEvent { + event_code: EVT_COMMAND_COMPLETE, + parameters: vec![], + }; + assert!(parse_le_advertising_reports(&event).is_none()); + } +} diff --git a/local/recipes/drivers/redbear-btusb/source/src/main.rs b/local/recipes/drivers/redbear-btusb/source/src/main.rs index fdab60d7..0ad1b556 100644 --- a/local/recipes/drivers/redbear-btusb/source/src/main.rs +++ b/local/recipes/drivers/redbear-btusb/source/src/main.rs @@ -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, + pub hci_revision: Option, + pub manufacturer_name: Option, + pub init_error: Option, +} + +/// 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 { + 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 { + 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, 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, String> { probe_usb_bluetooth_adapters_in(Path::new("/scheme/usb")) } +fn hci_init_sequence(transport: &mut dyn UsbHciTransport) -> Result { + 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 = 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 = 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 = 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, + } + + 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> { + 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> { + 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) { + 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}" + ); + } } diff --git a/local/recipes/drivers/redbear-btusb/source/src/usb_transport.rs b/local/recipes/drivers/redbear-btusb/source/src/usb_transport.rs new file mode 100644 index 00000000..de281288 --- /dev/null +++ b/local/recipes/drivers/redbear-btusb/source/src/usb_transport.rs @@ -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>; + fn send_acl(&mut self, acl: &HciAcl) -> io::Result<()>; + fn recv_acl(&mut self) -> io::Result>; + fn state(&self) -> TransportState; + fn close(&mut self) -> io::Result<()>; +} + +pub struct StubTransport { + config: UsbTransportConfig, + state: TransportState, + sent_commands: Vec, + sent_acl: Vec, + pending_events: Vec, + pending_acl: Vec, +} + +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 { + let drained = self.sent_commands.clone(); + self.sent_commands.clear(); + drained + } + + pub fn drain_sent_acl(&mut self) -> Vec { + 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> { + 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> { + 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); + } +} diff --git a/local/recipes/system/redbear-btctl/source/src/main.rs b/local/recipes/system/redbear-btctl/source/src/main.rs index 5342d49d..194fad6d 100644 --- a/local/recipes/system/redbear-btctl/source/src/main.rs +++ b/local/recipes/system/redbear-btctl/source/src/main.rs @@ -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, 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); } } diff --git a/local/recipes/system/redbear-wifictl/source/src/main.rs b/local/recipes/system/redbear-wifictl/source/src/main.rs index 3c9b12d3..fe113f97 100644 --- a/local/recipes/system/redbear-wifictl/source/src/main.rs +++ b/local/recipes/system/redbear-wifictl/source/src/main.rs @@ -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 { + let Ok(value) = env::var("INIT_NOTIFY") else { + return None; + }; + let Ok(fd) = value.parse::() 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, 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)]