Advance Bluetooth driver and tools

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-17 00:03:58 +01:00
parent 6eb854fda6
commit 054acba072
5 changed files with 433 additions and 43 deletions
+1 -27
View File
@@ -7,7 +7,7 @@
# The current slice is explicit-startup, USB-attached, BLE-first, and intentionally not wired to # The current slice is explicit-startup, USB-attached, BLE-first, and intentionally not wired to
# USB-class autospawn yet. # USB-class autospawn yet.
include = ["redbear-minimal.toml"] include = ["redbear-minimal.toml", "redbear-bluetooth-services.toml"]
[general] [general]
filesystem_size = 2048 filesystem_size = 2048
@@ -15,29 +15,3 @@ filesystem_size = 2048
[packages] [packages]
redbear-btusb = {} redbear-btusb = {}
redbear-btctl = {} redbear-btctl = {}
[[files]]
path = "/var/lib/bluetooth"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/var/run/redbear-btusb"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/usr/lib/init.d/11_btctl.service"
data = """
[unit]
description = "Bluetooth host/control daemon"
requires_weak = [
"05_firmware-loader.service",
]
[service]
cmd = "redbear-btctl"
type = { scheme = "btctl" }
"""
+17
View File
@@ -0,0 +1,17 @@
# Red Bear OS Bluetooth experimental service wiring
#
# Kept in a dedicated included fragment so the Bluetooth profile can inject
# bounded runtime files and service units without relying on profile-local
# [[files]] behavior.
[[files]]
path = "/var/lib/bluetooth"
data = ""
directory = true
mode = 0o755
[[files]]
path = "/var/run/redbear-btusb"
data = ""
directory = true
mode = 0o755
@@ -1,4 +1,5 @@
use std::fs; use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
@@ -8,10 +9,68 @@ use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
const STATUS_FRESHNESS_SECS: u64 = 90; const STATUS_FRESHNESS_SECS: u64 = 90;
const BLUETOOTH_USB_CLASS: u8 = 0xE0;
const BLUETOOTH_USB_SUBCLASS: u8 = 0x01;
const BLUETOOTH_USB_PROTOCOL: u8 = 0x01;
const KNOWN_BLUETOOTH_USB_VENDORS: [u16; 4] = [0x8087, 0x0BDA, 0x0A5C, 0x0A12];
#[derive(Clone, Debug, PartialEq, Eq)]
struct UsbBluetoothAdapter {
name: String,
vendor_id: u16,
device_id: u16,
bus: String,
device_path: PathBuf,
}
impl UsbBluetoothAdapter {
#[cfg(any(not(target_os = "redox"), test))]
fn stub(name: String) -> Self {
Self {
device_path: PathBuf::from(format!("/scheme/usb/stub/{name}")),
name,
vendor_id: 0,
device_id: 0,
bus: "stub".to_string(),
}
}
fn detail_line(&self, index: usize) -> String {
format!(
"adapter_{index}=name={};vendor_id={:04x};device_id={:04x};bus={};device_path={}",
self.name,
self.vendor_id,
self.device_id,
self.bus,
self.device_path.display()
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct UsbDeviceDescriptor {
vendor_id: u16,
device_id: u16,
class: u8,
subclass: u8,
protocol: u8,
}
impl UsbDeviceDescriptor {
fn looks_like_bluetooth(self) -> bool {
(self.class, self.subclass, self.protocol)
== (
BLUETOOTH_USB_CLASS,
BLUETOOTH_USB_SUBCLASS,
BLUETOOTH_USB_PROTOCOL,
)
|| KNOWN_BLUETOOTH_USB_VENDORS.contains(&self.vendor_id)
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
struct TransportConfig { struct TransportConfig {
adapters: Vec<String>, adapters: Vec<UsbBluetoothAdapter>,
controller_family: String, controller_family: String,
status_file: PathBuf, status_file: PathBuf,
} }
@@ -19,10 +78,7 @@ struct TransportConfig {
impl TransportConfig { impl TransportConfig {
fn from_env() -> Self { fn from_env() -> Self {
Self { Self {
adapters: parse_list( adapters: default_adapters_from_env(),
std::env::var("REDBEAR_BTUSB_STUB_ADAPTERS").ok().as_deref(),
&["hci0"],
),
controller_family: std::env::var("REDBEAR_BTUSB_STUB_FAMILY") controller_family: std::env::var("REDBEAR_BTUSB_STUB_FAMILY")
.unwrap_or_else(|_| "usb-generic-bounded".to_string()), .unwrap_or_else(|_| "usb-generic-bounded".to_string()),
status_file: std::env::var_os("REDBEAR_BTUSB_STATUS_FILE") status_file: std::env::var_os("REDBEAR_BTUSB_STATUS_FILE")
@@ -31,14 +87,37 @@ impl TransportConfig {
} }
} }
fn adapter_names(&self) -> Vec<String> {
self.adapters
.iter()
.map(|adapter| adapter.name.clone())
.collect()
}
fn refreshed(&self) -> Self {
let mut refreshed = self.clone();
if let Ok(adapters) = runtime_usb_bluetooth_adapters() {
refreshed.adapters = adapters;
}
refreshed
}
fn probe_lines(&self) -> Vec<String> { fn probe_lines(&self) -> Vec<String> {
vec![ let mut lines = vec![
format!("adapters={}", self.adapters.join(",")), format!("adapters={}", self.adapter_names().join(",")),
"transport=usb".to_string(), "transport=usb".to_string(),
"startup=explicit".to_string(), "startup=explicit".to_string(),
"mode=ble-first".to_string(), "mode=ble-first".to_string(),
format!("controller_family={}", self.controller_family), format!("controller_family={}", self.controller_family),
] format!("adapter_count={}", self.adapters.len()),
];
lines.extend(
self.adapters
.iter()
.enumerate()
.map(|(index, adapter)| adapter.detail_line(index)),
);
lines
} }
fn render_status_lines(&self, runtime_visible: bool) -> Vec<String> { fn render_status_lines(&self, runtime_visible: bool) -> Vec<String> {
@@ -105,6 +184,35 @@ enum CommandOutcome {
RunDaemon, RunDaemon,
} }
#[cfg(any(not(target_os = "redox"), test))]
fn default_adapters_from_names(names: Vec<String>) -> Vec<UsbBluetoothAdapter> {
names.into_iter().map(UsbBluetoothAdapter::stub).collect()
}
#[cfg(target_os = "redox")]
fn default_adapters_from_env() -> Vec<UsbBluetoothAdapter> {
Vec::new()
}
#[cfg(not(target_os = "redox"))]
fn default_adapters_from_env() -> Vec<UsbBluetoothAdapter> {
default_adapters_from_names(parse_list(
std::env::var("REDBEAR_BTUSB_STUB_ADAPTERS").ok().as_deref(),
&["hci0"],
))
}
#[cfg(target_os = "redox")]
fn runtime_usb_bluetooth_adapters() -> Result<Vec<UsbBluetoothAdapter>, String> {
probe_usb_bluetooth_adapters()
}
#[cfg(not(target_os = "redox"))]
fn runtime_usb_bluetooth_adapters() -> Result<Vec<UsbBluetoothAdapter>, String> {
Ok(default_adapters_from_env())
}
#[cfg(any(not(target_os = "redox"), test))]
fn parse_list(raw: Option<&str>, default: &[&str]) -> Vec<String> { fn parse_list(raw: Option<&str>, default: &[&str]) -> Vec<String> {
raw.map(|value| { raw.map(|value| {
value value
@@ -165,9 +273,16 @@ fn parse_command(args: &[String]) -> Result<Command, String> {
} }
fn execute(command: Command, config: &TransportConfig) -> CommandOutcome { fn execute(command: Command, config: &TransportConfig) -> CommandOutcome {
let effective_config = match command {
Command::Probe | Command::Status => config.refreshed(),
Command::Daemon => config.clone(),
};
match command { match command {
Command::Probe => CommandOutcome::Print(format_lines(&config.probe_lines())), Command::Probe => CommandOutcome::Print(format_lines(&effective_config.probe_lines())),
Command::Status => CommandOutcome::Print(format_lines(&config.current_status_lines())), Command::Status => {
CommandOutcome::Print(format_lines(&effective_config.current_status_lines()))
}
Command::Daemon => CommandOutcome::RunDaemon, Command::Daemon => CommandOutcome::RunDaemon,
} }
} }
@@ -197,6 +312,195 @@ fn main() {
} }
} }
fn parse_numeric_value(value: &str) -> Result<u64, String> {
let trimmed = value.trim();
if let Some(hex) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
u64::from_str_radix(hex, 16).map_err(|err| format!("invalid hex value {trimmed:?}: {err}"))
} else {
trimmed
.parse::<u64>()
.map_err(|err| format!("invalid numeric value {trimmed:?}: {err}"))
}
}
fn extract_jsonish_u64(raw: &str, keys: &[&str]) -> Result<u64, String> {
for key in keys {
let needle = format!("\"{key}\"");
let Some(key_start) = raw.find(&needle) else {
continue;
};
let after_key = &raw[key_start + needle.len()..];
let Some(colon_index) = after_key.find(':') else {
continue;
};
let after_colon = after_key[colon_index + 1..].trim_start();
if after_colon.is_empty() {
continue;
}
let token = if let Some(quoted) = after_colon.strip_prefix('"') {
let Some(end_quote) = quoted.find('"') else {
continue;
};
&quoted[..end_quote]
} else {
let end = after_colon
.find(|ch: char| matches!(ch, ',' | '}' | '\n' | '\r'))
.unwrap_or(after_colon.len());
after_colon[..end].trim()
};
if token.is_empty() {
continue;
}
return parse_numeric_value(token);
}
Err(format!(
"missing descriptor field; expected one of {keys:?}"
))
}
fn parse_usb_device_descriptor(raw: &[u8]) -> Result<UsbDeviceDescriptor, String> {
let text = String::from_utf8_lossy(raw);
let vendor_id = extract_jsonish_u64(&text, &["vendor", "vendor_id"]).and_then(|value| {
u16::try_from(value).map_err(|_| format!("vendor ID out of range: {value}"))
})?;
let device_id = extract_jsonish_u64(&text, &["product", "device", "product_id", "device_id"])
.and_then(|value| {
u16::try_from(value).map_err(|_| format!("device ID out of range: {value}"))
})?;
let class = extract_jsonish_u64(&text, &["class", "device_class"]).and_then(|value| {
u8::try_from(value).map_err(|_| format!("USB class out of range: {value}"))
})?;
let subclass = extract_jsonish_u64(&text, &["sub_class", "subclass", "device_subclass"])
.and_then(|value| {
u8::try_from(value).map_err(|_| format!("USB subclass out of range: {value}"))
})?;
let protocol =
extract_jsonish_u64(&text, &["protocol", "device_protocol"]).and_then(|value| {
u8::try_from(value).map_err(|_| format!("USB protocol out of range: {value}"))
})?;
Ok(UsbDeviceDescriptor {
vendor_id,
device_id,
class,
subclass,
protocol,
})
}
fn try_collect_bluetooth_adapter(
adapters: &mut Vec<UsbBluetoothAdapter>,
bus: &str,
device_path: &Path,
) -> Result<(), String> {
let descriptor_path = device_path.join("descriptors");
let raw = match fs::read(&descriptor_path) {
Ok(raw) => raw,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
Err(_) => return Ok(()),
};
let descriptor = match parse_usb_device_descriptor(&raw) {
Ok(descriptor) => descriptor,
Err(_) => return Ok(()),
};
if descriptor.looks_like_bluetooth() {
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(),
});
}
Ok(())
}
fn probe_usb_bluetooth_adapters_in(root: &Path) -> Result<Vec<UsbBluetoothAdapter>, String> {
let entries = match fs::read_dir(root) {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => {
return Err(format!(
"failed to read USB scheme root {}: {err}",
root.display()
));
}
};
let mut adapters = Vec::new();
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let bus_name = entry.file_name().to_string_lossy().into_owned();
if bus_name.is_empty() {
continue;
}
let bus_path = root.join(&bus_name);
try_collect_bluetooth_adapter(&mut adapters, &bus_name, &bus_path)?;
let nested_entries = match fs::read_dir(&bus_path) {
Ok(nested_entries) => nested_entries,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(_) => continue,
};
for nested_entry in nested_entries {
let nested_entry = match nested_entry {
Ok(nested_entry) => nested_entry,
Err(_) => continue,
};
let nested_name = nested_entry.file_name().to_string_lossy().into_owned();
if nested_name.is_empty() {
continue;
}
let device_path = bus_path.join(&nested_name);
try_collect_bluetooth_adapter(&mut adapters, &bus_name, &device_path)?;
}
}
adapters.sort_by(|left, right| {
left.bus
.cmp(&right.bus)
.then(left.device_path.cmp(&right.device_path))
.then(left.vendor_id.cmp(&right.vendor_id))
.then(left.device_id.cmp(&right.device_id))
});
for (index, adapter) in adapters.iter_mut().enumerate() {
adapter.name = format!("hci{index}");
}
Ok(adapters)
}
#[cfg(target_os = "redox")]
fn probe_usb_bluetooth_adapters() -> Result<Vec<UsbBluetoothAdapter>, String> {
probe_usb_bluetooth_adapters_in(Path::new("/scheme/usb"))
}
#[cfg(not(target_os = "redox"))]
#[allow(dead_code)]
fn probe_usb_bluetooth_adapters() -> Result<Vec<UsbBluetoothAdapter>, String> {
probe_usb_bluetooth_adapters_in(Path::new("/scheme/usb"))
}
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
fn daemon_main(_config: &TransportConfig) -> Result<(), String> { fn daemon_main(_config: &TransportConfig) -> Result<(), String> {
Err("daemon mode is only supported on Redox; use --probe or --status on host".to_string()) Err("daemon mode is only supported on Redox; use --probe or --status on host".to_string())
@@ -214,14 +518,16 @@ fn daemon_main(config: &TransportConfig) -> Result<(), String> {
} }
} }
config.write_status_file()?; let mut runtime_config = config.refreshed();
runtime_config.write_status_file()?;
let _status_file_guard = StatusFileGuard { let _status_file_guard = StatusFileGuard {
path: &config.status_file, path: &config.status_file,
}; };
loop { loop {
thread::sleep(Duration::from_secs(30)); thread::sleep(Duration::from_secs(30));
config.write_status_file()?; runtime_config = config.refreshed();
runtime_config.write_status_file()?;
} }
} }
@@ -239,14 +545,25 @@ mod tests {
env::temp_dir().join(format!("{name}-{stamp}")) env::temp_dir().join(format!("{name}-{stamp}"))
} }
fn stub_adapter(name: &str) -> UsbBluetoothAdapter {
UsbBluetoothAdapter::stub(name.to_string())
}
fn test_config(status_file: PathBuf) -> TransportConfig { fn test_config(status_file: PathBuf) -> TransportConfig {
TransportConfig { TransportConfig {
adapters: vec!["hci0".to_string()], adapters: vec![stub_adapter("hci0")],
controller_family: "usb-bounded-test".to_string(), controller_family: "usb-bounded-test".to_string(),
status_file, status_file,
} }
} }
fn write_descriptor(path: &Path, body: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, body).unwrap();
}
#[test] #[test]
fn probe_contract_is_bounded_and_usb_scoped() { fn probe_contract_is_bounded_and_usb_scoped() {
let output = execute(Command::Probe, &test_config(temp_path("rbos-btusb-status"))); let output = execute(Command::Probe, &test_config(temp_path("rbos-btusb-status")));
@@ -317,4 +634,72 @@ mod tests {
); );
assert_eq!(parse_command(&[]).unwrap(), Command::Daemon); assert_eq!(parse_command(&[]).unwrap(), Command::Daemon);
} }
#[test]
fn probe_usb_bluetooth_adapters_filters_and_enumerates_devices() {
let root = temp_path("rbos-btusb-usb-root");
write_descriptor(
&root
.join("usb.0000:00:14.0")
.join("port1")
.join("descriptors"),
r#"{"class":224,"sub_class":1,"protocol":1,"vendor":32903,"product":50}"#,
);
write_descriptor(
&root
.join("usb.0000:00:14.0")
.join("port2")
.join("descriptors"),
r#"{"class":3,"sub_class":1,"protocol":1,"vendor":4660,"product":22136}"#,
);
write_descriptor(
&root
.join("usb.0000:00:15.0")
.join("port3")
.join("descriptors"),
r#"{"class":224,"sub_class":1,"protocol":1,"vendor":3034,"product":4660}"#,
);
let adapters = probe_usb_bluetooth_adapters_in(&root).unwrap();
assert_eq!(adapters.len(), 2);
assert_eq!(adapters[0].name, "hci0");
assert_eq!(adapters[0].vendor_id, 0x8087u16);
assert_eq!(adapters[0].device_id, 0x0032u16);
assert_eq!(adapters[0].bus, "usb.0000:00:14.0");
assert!(adapters[0]
.device_path
.ends_with(Path::new("usb.0000:00:14.0/port1")));
assert_eq!(adapters[1].name, "hci1");
assert_eq!(adapters[1].vendor_id, 0x0bdau16);
assert_eq!(adapters[1].device_id, 0x1234u16);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn probe_usb_bluetooth_adapters_accepts_known_vendor_fallback() {
let root = temp_path("rbos-btusb-known-vendor");
write_descriptor(
&root
.join("usb.0000:00:16.0")
.join("port7")
.join("descriptors"),
r#"{"class":255,"sub_class":255,"protocol":255,"vendor":2652,"product":4660}"#,
);
let adapters = probe_usb_bluetooth_adapters_in(&root).unwrap();
assert_eq!(adapters.len(), 1);
assert_eq!(adapters[0].name, "hci0");
assert_eq!(adapters[0].vendor_id, 0x0a5cu16);
assert_eq!(adapters[0].device_id, 0x1234u16);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn probe_usb_bluetooth_adapters_handles_missing_usb_tree() {
let root = temp_path("rbos-btusb-missing-root");
let adapters = probe_usb_bluetooth_adapters_in(&root).unwrap();
assert!(adapters.is_empty());
}
} }
@@ -2,7 +2,12 @@
path = "source" path = "source"
[build] [build]
template = "cargo" template = "custom"
script = """
set -ex
[package.files] cookbook_cargo
"/usr/bin/redbear-btctl" = "redbear-btctl"
mkdir -pv "$COOKBOOK_STAGE/usr/lib/init.d"
cp -v "$COOKBOOK_SOURCE/11_btctl.service" "$COOKBOOK_STAGE/usr/lib/init.d/11_btctl.service"
"""
@@ -0,0 +1,9 @@
[unit]
description = "Bluetooth host/control daemon"
requires_weak = [
"05_firmware-loader.service",
]
[service]
cmd = "redbear-btctl"
type = { scheme = "btctl" }