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:
@@ -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" }
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
"ed[..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" }
|
||||||
Reference in New Issue
Block a user