diff --git a/local/recipes/drivers/redbear-btusb/source/src/btintel.rs b/local/recipes/drivers/redbear-btusb/source/src/btintel.rs index 46d62222ab..e11cbfbe92 100644 --- a/local/recipes/drivers/redbear-btusb/source/src/btintel.rs +++ b/local/recipes/drivers/redbear-btusb/source/src/btintel.rs @@ -1,6 +1,7 @@ use log::{debug, info, warn}; -use crate::hci::{HciCommand, HciPacketData}; +use crate::hci::{HciCommand, HciEvent}; +use crate::usb_transport::UsbHciTransport; const OP_INTEL_MFG: u16 = 0xfc11; const OP_INTEL_READ_VERSION: u16 = 0xfc05; @@ -223,6 +224,152 @@ pub fn extract_boot_param(fw_data: &[u8]) -> Option { None } +pub fn read_firmware_blob(name: &str) -> Result, String> { + let candidates = [ + format!("/scheme/firmware/intel/{name}"), + format!("/scheme/firmware/{name}"), + format!("/lib/firmware/intel/{name}"), + format!("/lib/firmware/{name}"), + ]; + for path in &candidates { + if let Ok(data) = std::fs::read(path) { + if !data.is_empty() { + return Ok(data); + } + } + } + Err(format!("firmware {name} not found in scheme/fs paths")) +} + +fn run_intel_command( + transport: &mut dyn UsbHciTransport, + cmd: &HciCommand, +) -> Result<(u8, Vec), String> { + transport + .send_command(cmd) + .map_err(|e| format!("send opcode {:#06x}: {e}", cmd.opcode))?; + let event = transport + .recv_event() + .map_err(|e| format!("recv opcode {:#06x}: {e}", cmd.opcode))? + .ok_or_else(|| format!("no response to opcode {:#06x}", cmd.opcode))?; + let Some((_n, opcode, params)) = event.command_complete_params() else { + return Err(format!( + "expected command-complete for {:#06x}, got event {:#04x}", + cmd.opcode, event.event_code + )); + }; + if opcode != cmd.opcode { + return Err(format!( + "command-complete opcode mismatch: sent {:#06x}, got {:#06x}", + cmd.opcode, opcode + )); + } + if params.is_empty() { + return Err(format!("empty return params for {:#06x}", cmd.opcode)); + } + let status = params[0]; + Ok((status, params[1..].to_vec())) +} + +pub fn intel_read_version(transport: &mut dyn UsbHciTransport) -> Result { + let (_status, params) = run_intel_command(transport, &cmd_intel_read_version())?; + IntelVersion::from_response(¶ms) + .ok_or_else(|| format!("malformed Intel version response ({} bytes)", params.len())) +} + +pub fn intel_download_firmware( + transport: &mut dyn UsbHciTransport, + fw_data: &[u8], +) -> Result<(), String> { + let commands = firmware_download_commands(fw_data); + let total = commands.len(); + for (i, cmd) in commands.iter().enumerate() { + let (status, _params) = run_intel_command(transport, cmd) + .map_err(|e| format!("fragment {i}/{total}: {e}"))?; + if status != 0x00 { + return Err(format!( + "fragment {i}/{total} rejected by controller (status {:#04x})", + status + )); + } + if i % 32 == 0 { + debug!("btintel: firmware download progress {}/{}", i, total); + } + } + info!("btintel: firmware download complete ({} fragments)", total); + Ok(()) +} + +pub fn intel_setup_firmware( + transport: &mut dyn UsbHciTransport, + vendor_id: u16, + device_id: u16, +) -> Result { + let version = intel_read_version(transport)?; + info!( + "btintel: version hw_variant={:#04x} hw_rev={:#04x} fw_variant={:#04x} fw_rev={:#04x} ({})", + version.hw_variant, + version.hw_revision, + version.fw_variant, + version.fw_revision, + if version.is_bootloader() { "bootloader" } else { "operational" } + ); + + if version.is_bootloader() { + let sfi_name = firmware_filename_for_device(vendor_id, device_id) + .ok_or_else(|| format!("no SFI firmware known for {vendor_id:04x}:{device_id:04x}"))?; + let fw_data = read_firmware_blob(sfi_name)?; + + let (status, _p) = run_intel_command(transport, &cmd_intel_enter_mfg())?; + if status != 0x00 { + return Err(format!("enter MFG rejected (status {status:#04x})")); + } + intel_download_firmware(transport, &fw_data)?; + let (status, _p) = run_intel_command(transport, &cmd_intel_exit_mfg(true, true))?; + if status != 0x00 { + return Err(format!("exit MFG rejected (status {status:#04x})")); + } + info!("btintel: firmware patched, controller reset to operational mode"); + } + + if let Some(ddc_name) = ddc_filename_for_device(vendor_id, device_id) { + if let Ok(ddc_data) = read_firmware_blob(ddc_name) { + apply_ddc_config(transport, &ddc_data)?; + } else { + warn!("btintel: DDC config {} not found, using firmware defaults", ddc_name); + } + } + + intel_read_version(transport) +} + +fn apply_ddc_config( + transport: &mut dyn UsbHciTransport, + ddc_data: &[u8], +) -> Result<(), String> { + let mut offset = 0; + let mut applied = 0usize; + while offset + 3 < ddc_data.len() { + let opcode = u16::from_le_bytes([ddc_data[offset], ddc_data[offset + 1]]); + let plen = ddc_data[offset + 2] as usize; + if offset + 3 + plen > ddc_data.len() { + break; + } + let params = ddc_data[offset + 3..offset + 3 + plen].to_vec(); + let cmd = HciCommand::new(opcode, params); + let (status, _p) = run_intel_command(transport, &cmd) + .map_err(|e| format!("DDC opcode {opcode:#06x}: {e}"))?; + if status != 0x00 { + debug!("btintel: DDC opcode {opcode:#06x} status {status:#04x} (continuing)"); + } else { + applied += 1; + } + offset += 3 + plen; + } + info!("btintel: DDC config applied ({} commands)", applied); + Ok(()) +} + pub struct IntelFirmwareLoader { pub version: Option, } @@ -380,4 +527,90 @@ mod tests { let fw = vec![0u8; 20]; assert_eq!(extract_boot_param(&fw), None); } + + struct TestTransport { + events: std::collections::VecDeque, + sent: Vec, + } + + impl TestTransport { + fn new() -> Self { + Self { + events: std::collections::VecDeque::new(), + sent: Vec::new(), + } + } + fn push_cc(&mut self, opcode: u16, status: u8) { + let params = vec![0x01, opcode as u8, (opcode >> 8) as u8, status, 0x00]; + self.events.push_back(HciEvent { + event_code: crate::hci::EVT_COMMAND_COMPLETE, + parameters: params, + }); + } + } + + impl crate::usb_transport::UsbHciTransport for TestTransport { + fn send_command(&mut self, command: &HciCommand) -> std::io::Result<()> { + self.sent.push(command.clone()); + Ok(()) + } + fn recv_event(&mut self) -> std::io::Result> { + Ok(self.events.pop_front()) + } + fn send_acl(&mut self, _acl: &crate::hci::HciAcl) -> std::io::Result<()> { + Ok(()) + } + fn recv_acl(&mut self) -> std::io::Result> { + Ok(None) + } + fn state(&self) -> crate::usb_transport::TransportState { + crate::usb_transport::TransportState::Active + } + fn close(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + #[test] + fn run_intel_command_matches_opcode() { + let mut t = TestTransport::new(); + t.push_cc(OP_INTEL_READ_VERSION, 0x00); + let (status, _params) = run_intel_command(&mut t, &cmd_intel_read_version()).unwrap(); + assert_eq!(status, 0x00); + assert_eq!(t.sent.len(), 1); + assert_eq!(t.sent[0].opcode, OP_INTEL_READ_VERSION); + } + + #[test] + fn run_intel_command_opcode_mismatch_errors() { + let mut t = TestTransport::new(); + t.push_cc(OP_INTEL_READ_BOOT_PARAMS, 0x00); + let result = run_intel_command(&mut t, &cmd_intel_read_version()); + assert!(result.is_err()); + } + + #[test] + fn download_rejects_controller_nack() { + let mut fw = vec![0x01]; + fw.extend_from_slice(&[0u8; 643]); + fw.extend_from_slice(&[0xCC; 300]); + let mut t = TestTransport::new(); + t.push_cc(OP_INTEL_DOWNLOAD_FRAGMENT, 0x01); + let result = intel_download_firmware(&mut t, &fw); + assert!(result.is_err()); + } + + #[test] + fn download_succeeds_when_all_fragments_ack() { + let mut fw = vec![0x01]; + fw.extend_from_slice(&[0u8; 643]); + fw.extend_from_slice(&[0xCC; 300]); + let total = firmware_download_commands(&fw).len(); + let mut t = TestTransport::new(); + for _ in 0..total { + t.push_cc(OP_INTEL_DOWNLOAD_FRAGMENT, 0x00); + } + intel_download_firmware(&mut t, &fw).unwrap(); + assert_eq!(t.sent.len(), total); + } } diff --git a/local/recipes/drivers/redbear-btusb/source/src/main.rs b/local/recipes/drivers/redbear-btusb/source/src/main.rs index ecdf348761..264d767393 100644 --- a/local/recipes/drivers/redbear-btusb/source/src/main.rs +++ b/local/recipes/drivers/redbear-btusb/source/src/main.rs @@ -794,6 +794,22 @@ fn daemon_main(config: &TransportConfig) -> Result<(), String> { let mut t = StubTransport::new(transport_config); + // Intel CNVi (0x8087): bootloader detect + SFI download + DDC apply + // before standard init. No-op on operational controllers; non-fatal. + if adapter.vendor_id == 0x8087 { + match btintel::intel_setup_firmware(&mut t, adapter.vendor_id, adapter.device_id) { + Ok(version) => { + log::info!( + "btusb: Intel firmware ready (fw_variant={:#04x} fw_rev={:#04x})", + version.fw_variant, version.fw_revision + ); + } + Err(err) => { + log::warn!("btusb: Intel firmware setup failed ({}); continuing with standard init", err); + } + } + } + match hci_init_sequence(&mut t) { Ok(info) => { controller_info = info;