btusb: complete Intel BT firmware download runtime (Phase 7.1)

- btintel.rs: run_intel_command() sends a command and waits for the
  matching command-complete (opcode + status check). intel_read_version()
  parses the Intel version response. intel_download_firmware() streams
  the full SFI command sequence (CSS header + PKey + signature + payload
  fragments), rejecting on controller NACK. intel_setup_firmware()
  orchestrates bootloader detection -> enter MFG -> download -> exit MFG
  -> DDC apply -> version re-read. apply_ddc_config() replays the DDC
  command records. read_firmware_blob() probes scheme and /lib/firmware
  (flat + intel/ layouts).
- main.rs: wire intel_setup_firmware into daemon_main for Intel CNVi
  adapters (vendor 0x8087) before standard HCI init; no-op on
  operational controllers, non-fatal on failure.
- 4 new tests (download ACK/NACK, opcode match/mismatch); 164 total pass.
  Ported from Linux drivers/bluetooth/btintel.c.
This commit is contained in:
2026-07-23 06:02:03 +09:00
parent bbb7c60777
commit 2d228df602
2 changed files with 250 additions and 1 deletions
@@ -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<u32> {
None
}
pub fn read_firmware_blob(name: &str) -> Result<Vec<u8>, 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<u8>), 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<IntelVersion, String> {
let (_status, params) = run_intel_command(transport, &cmd_intel_read_version())?;
IntelVersion::from_response(&params)
.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<IntelVersion, String> {
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<IntelVersion>,
}
@@ -380,4 +527,90 @@ mod tests {
let fw = vec![0u8; 20];
assert_eq!(extract_boot_param(&fw), None);
}
struct TestTransport {
events: std::collections::VecDeque<HciEvent>,
sent: Vec<HciCommand>,
}
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<Option<HciEvent>> {
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<Option<crate::hci::HciAcl>> {
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);
}
}
@@ -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;