release prep: rc.1 baseline and gating updates

This commit is contained in:
2026-03-02 15:54:55 -05:00
parent 97a42c8802
commit f43b2b24b6
168 changed files with 14708 additions and 982 deletions

View File

@@ -0,0 +1,24 @@
[package]
name = "bitdo_app_core"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
[dependencies]
anyhow = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
toml = { workspace = true }
reqwest = { workspace = true }
base64 = { workspace = true }
ed25519-dalek = { workspace = true }
bitdo_proto = { path = "../bitdo_proto" }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,7 @@
name = "bitdo_proto"
version = "0.1.0"
edition = "2021"
license = "MIT"
build = "build.rs"
license = "BSD-3-Clause"
[features]
default = ["hidapi-backend"]
@@ -16,9 +15,6 @@ serde_json = { workspace = true }
hex = { workspace = true }
hidapi = { version = "2.6", optional = true }
[build-dependencies]
csv = "1.3"
[dev-dependencies]
serde_json = { workspace = true }
hex = { workspace = true }
@@ -39,10 +35,18 @@ path = "../../tests/retry_timeout.rs"
name = "pid_matrix_coverage"
path = "../../tests/pid_matrix_coverage.rs"
[[test]]
name = "command_matrix_coverage"
path = "../../tests/command_matrix_coverage.rs"
[[test]]
name = "capability_gating"
path = "../../tests/capability_gating.rs"
[[test]]
name = "candidate_readonly_gating"
path = "../../tests/candidate_readonly_gating.rs"
[[test]]
name = "profile_serialization"
path = "../../tests/profile_serialization.rs"
@@ -74,3 +78,7 @@ path = "../../tests/error_codes.rs"
[[test]]
name = "diag_probe"
path = "../../tests/diag_probe.rs"
[[test]]
name = "runtime_policy"
path = "../../tests/runtime_policy.rs"

View File

@@ -1,121 +0,0 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
let spec_dir = manifest_dir.join("../../../spec");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
let pid_csv = spec_dir.join("pid_matrix.csv");
let command_csv = spec_dir.join("command_matrix.csv");
println!("cargo:rerun-if-changed={}", pid_csv.display());
println!("cargo:rerun-if-changed={}", command_csv.display());
generate_pid_registry(&pid_csv, &out_dir.join("generated_pid_registry.rs"));
generate_command_registry(&command_csv, &out_dir.join("generated_command_registry.rs"));
}
fn generate_pid_registry(csv_path: &Path, out_path: &Path) {
let mut rdr = csv::Reader::from_path(csv_path).expect("failed to open pid_matrix.csv");
let mut out = String::new();
out.push_str("pub const PID_REGISTRY: &[crate::registry::PidRegistryRow] = &[\n");
for rec in rdr.records() {
let rec = rec.expect("invalid pid csv record");
let name = rec.get(0).expect("pid_name");
let pid: u16 = rec
.get(1)
.expect("pid_decimal")
.parse()
.expect("invalid pid decimal");
let support_level = match rec.get(5).expect("support_level") {
"full" => "crate::types::SupportLevel::Full",
"detect-only" => "crate::types::SupportLevel::DetectOnly",
other => panic!("unknown support_level {other}"),
};
let protocol_family = match rec.get(6).expect("protocol_family") {
"Standard64" => "crate::types::ProtocolFamily::Standard64",
"JpHandshake" => "crate::types::ProtocolFamily::JpHandshake",
"DInput" => "crate::types::ProtocolFamily::DInput",
"DS4Boot" => "crate::types::ProtocolFamily::DS4Boot",
"Unknown" => "crate::types::ProtocolFamily::Unknown",
other => panic!("unknown protocol_family {other}"),
};
out.push_str(&format!(
" crate::registry::PidRegistryRow {{ name: \"{name}\", pid: {pid}, support_level: {support_level}, protocol_family: {protocol_family} }},\n"
));
}
out.push_str("]\n;");
fs::write(out_path, out).expect("failed writing generated_pid_registry.rs");
}
fn generate_command_registry(csv_path: &Path, out_path: &Path) {
let mut rdr = csv::Reader::from_path(csv_path).expect("failed to open command_matrix.csv");
let mut out = String::new();
out.push_str("pub const COMMAND_REGISTRY: &[crate::registry::CommandRegistryRow] = &[\n");
for rec in rdr.records() {
let rec = rec.expect("invalid command csv record");
let id = rec.get(0).expect("command_id");
let safety_class = match rec.get(1).expect("safety_class") {
"SafeRead" => "crate::types::SafetyClass::SafeRead",
"SafeWrite" => "crate::types::SafetyClass::SafeWrite",
"UnsafeBoot" => "crate::types::SafetyClass::UnsafeBoot",
"UnsafeFirmware" => "crate::types::SafetyClass::UnsafeFirmware",
other => panic!("unknown safety_class {other}"),
};
let confidence = match rec.get(2).expect("confidence") {
"confirmed" => "crate::types::CommandConfidence::Confirmed",
"inferred" => "crate::types::CommandConfidence::Inferred",
other => panic!("unknown confidence {other}"),
};
let experimental_default = rec
.get(3)
.expect("experimental_default")
.parse::<bool>()
.expect("invalid experimental_default");
let report_id = parse_u8(rec.get(4).expect("report_id"));
let request_hex = rec.get(6).expect("request_hex");
let request = hex_to_bytes(request_hex);
let expected_response = rec.get(7).expect("expected_response");
out.push_str(&format!(
" crate::registry::CommandRegistryRow {{ id: crate::command::CommandId::{id}, safety_class: {safety_class}, confidence: {confidence}, experimental_default: {experimental_default}, report_id: {report_id}, request: &{request:?}, expected_response: \"{expected_response}\" }},\n"
));
}
out.push_str("]\n;");
fs::write(out_path, out).expect("failed writing generated_command_registry.rs");
}
fn parse_u8(value: &str) -> u8 {
if let Some(stripped) = value.strip_prefix("0x") {
u8::from_str_radix(stripped, 16).expect("invalid hex u8")
} else {
value.parse::<u8>().expect("invalid u8")
}
}
fn hex_to_bytes(hex: &str) -> Vec<u8> {
let hex = hex.trim();
if hex.len() % 2 != 0 {
panic!("hex length must be even: {hex}");
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let raw = hex.as_bytes();
for i in (0..raw.len()).step_by(2) {
let hi = (raw[i] as char)
.to_digit(16)
.unwrap_or_else(|| panic!("invalid hex: {hex}"));
let lo = (raw[i + 1] as char)
.to_digit(16)
.unwrap_or_else(|| panic!("invalid hex: {hex}"));
bytes.push(((hi << 4) | lo) as u8);
}
bytes
}

View File

@@ -20,10 +20,30 @@ pub enum CommandId {
ExitBootloader,
FirmwareChunk,
FirmwareCommit,
Jp108ReadDedicatedMappings,
Jp108WriteDedicatedMapping,
Jp108ReadFeatureFlags,
Jp108WriteFeatureFlags,
Jp108ReadVoice,
Jp108WriteVoice,
U2GetCurrentSlot,
U2ReadConfigSlot,
U2WriteConfigSlot,
U2ReadButtonMap,
U2WriteButtonMap,
U2SetMode,
Jp108EnterBootloader,
Jp108FirmwareChunk,
Jp108FirmwareCommit,
Jp108ExitBootloader,
U2EnterBootloader,
U2FirmwareChunk,
U2FirmwareCommit,
U2ExitBootloader,
}
impl CommandId {
pub const ALL: [CommandId; 17] = [
pub const ALL: [CommandId; 37] = [
CommandId::GetPid,
CommandId::GetReportRevision,
CommandId::GetMode,
@@ -41,6 +61,26 @@ impl CommandId {
CommandId::ExitBootloader,
CommandId::FirmwareChunk,
CommandId::FirmwareCommit,
CommandId::Jp108ReadDedicatedMappings,
CommandId::Jp108WriteDedicatedMapping,
CommandId::Jp108ReadFeatureFlags,
CommandId::Jp108WriteFeatureFlags,
CommandId::Jp108ReadVoice,
CommandId::Jp108WriteVoice,
CommandId::U2GetCurrentSlot,
CommandId::U2ReadConfigSlot,
CommandId::U2WriteConfigSlot,
CommandId::U2ReadButtonMap,
CommandId::U2WriteButtonMap,
CommandId::U2SetMode,
CommandId::Jp108EnterBootloader,
CommandId::Jp108FirmwareChunk,
CommandId::Jp108FirmwareCommit,
CommandId::Jp108ExitBootloader,
CommandId::U2EnterBootloader,
CommandId::U2FirmwareChunk,
CommandId::U2FirmwareCommit,
CommandId::U2ExitBootloader,
];
pub fn all() -> &'static [CommandId] {

View File

@@ -0,0 +1,82 @@
// Hardcoded command declaration table.
//
// Policy model:
// - Confirmed commands can be enabled by default.
// - Inferred safe reads can run only behind experimental/advanced mode.
// - Inferred writes and unsafe paths stay blocked until confirmed.
pub const COMMAND_REGISTRY: &[crate::registry::CommandRegistryRow] = &[
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetReportRevision, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x04;byte5=0x01", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetModeAlt, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetSuperButton, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::SetModeDInput, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 0, 81, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Idle, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Version, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte1=0x22", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::ReadProfile, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::WriteProfile, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 7, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::EnterBootloaderA, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[5, 0, 80, 1, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::EnterBootloaderB, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[0, 81, 0, 0, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::EnterBootloaderC, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[0, 80, 0, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::ExitBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 81, 1, 0, 0], expected_response: "none", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::FirmwareChunk, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::FirmwareCommit, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[], operation_group: "Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ReadDedicatedMappings, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 48, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108WriteDedicatedMapping, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 49, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ReadFeatureFlags, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 50, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108WriteFeatureFlags, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 51, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ReadVoice, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 52, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108WriteVoice, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 53, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "JP108Dedicated" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2GetCurrentSlot, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 64, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2ReadConfigSlot, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 65, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2WriteConfigSlot, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 66, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2ReadButtonMap, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 67, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2WriteButtonMap, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 68, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2SetMode, safety_class: crate::types::SafetyClass::SafeWrite, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 5, 69, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Ultimate2Core" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108EnterBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 80, 1, 0, 0], expected_response: "none", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108FirmwareChunk, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 16, 32, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108FirmwareCommit, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 17, 32, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::Jp108ExitBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 81, 1, 0, 0], expected_response: "none", applies_to: &[21001, 21002], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2EnterBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 80, 1, 0, 0], expected_response: "none", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2FirmwareChunk, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 16, 96, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2FirmwareCommit, safety_class: crate::types::SafetyClass::UnsafeFirmware, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[129, 96, 17, 96, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::U2ExitBootloader, safety_class: crate::types::SafetyClass::UnsafeBoot, confidence: crate::types::CommandConfidence::Inferred, experimental_default: true, report_id: 129, request: &[5, 0, 81, 1, 0, 0], expected_response: "none", applies_to: &[24594, 24595], operation_group: "Firmware" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12544], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12544], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12544], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12549], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12549], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12549], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[8448], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[8448], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[8448], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[8449], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[8449], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[8449], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[36890], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[36890], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[36890], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[24582], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[24582], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[24582], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[20995], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[20995], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[20995], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[20996], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[20996], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[20996], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12314], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12314], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12314], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[36904], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[36904], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[36904], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12326], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12326], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12326], operation_group: "FirmwarePreflight" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetPid, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 5, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05;byte4=0xC1", applies_to: &[12327], operation_group: "CoreDiag" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetMode, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x05", applies_to: &[12327], operation_group: "ModeProfileRead" },
crate::registry::CommandRegistryRow { id: crate::command::CommandId::GetControllerVersion, safety_class: crate::types::SafetyClass::SafeRead, confidence: crate::types::CommandConfidence::Confirmed, experimental_default: false, report_id: 129, request: &[129, 4, 33, 1, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected_response: "byte0=0x02;byte1=0x22", applies_to: &[12327], operation_group: "FirmwarePreflight" },
]
;

View File

@@ -1,5 +1,3 @@
#![cfg(feature = "hidapi-backend")]
use crate::error::{BitdoError, Result};
use crate::transport::Transport;
use crate::types::VidPid;

View File

@@ -21,10 +21,11 @@ pub use registry::{
};
pub use session::{
validate_response, CommandExecutionReport, DeviceSession, DiagCommandStatus, DiagProbeResult,
FirmwareTransferReport, IdentifyResult, ModeState, RetryPolicy, SessionConfig, TimeoutProfile,
DiagSeverity, FirmwareTransferReport, IdentifyResult, ModeState, RetryPolicy, SessionConfig,
TimeoutProfile,
};
pub use transport::{MockTransport, Transport};
pub use types::{
CommandConfidence, DeviceProfile, PidCapability, ProtocolFamily, SafetyClass, SupportEvidence,
SupportLevel, VidPid,
CommandConfidence, CommandRuntimePolicy, DeviceProfile, EvidenceConfidence, PidCapability,
ProtocolFamily, SafetyClass, SupportEvidence, SupportLevel, SupportTier, VidPid,
};

View File

@@ -0,0 +1,68 @@
// Hardcoded PID registry.
//
// Design note:
// - Every known PID from sanitized evidence/spec is declared here.
// - Declaration breadth is broad, but runtime execution remains conservative
// through session/runtime policy gates in `session.rs`.
// - PID values are unique here by policy; legacy aliases are documented in
// `spec/alias_index.md` instead of duplicated in runtime tables.
pub const PID_REGISTRY: &[crate::registry::PidRegistryRow] = &[
crate::registry::PidRegistryRow { name: "PID_None", pid: 0, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Unknown },
crate::registry::PidRegistryRow { name: "PID_IDLE", pid: 12553, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_SN30Plus", pid: 24578, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_USB_Ultimate", pid: 12544, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_USB_Ultimate2", pid: 12549, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_USB_UltimateClasses", pid: 12548, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Xcloud", pid: 8448, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Xcloud2", pid: 8449, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_ArcadeStick", pid: 36890, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Pro2", pid: 24579, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Pro2_CY", pid: 24582, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Pro2_Wired", pid: 12304, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate_PC", pid: 12305, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate2_4", pid: 12306, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate2_4RR", pid: 12307, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_UltimateBT", pid: 24583, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_UltimateBTRR", pid: 12550, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_JP", pid: 20992, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_JPUSB", pid: 20993, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_NUMPAD", pid: 20995, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_NUMPADRR", pid: 20996, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_QINGCHUN2", pid: 12554, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_QINGCHUN2RR", pid: 12316, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Xinput", pid: 12555, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Pro3", pid: 24585, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Pro3USB", pid: 24586, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Pro3DOCK", pid: 24589, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_108JP", pid: 21001, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_108JPUSB", pid: 21002, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_XBOXJP", pid: 8232, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_XBOXJPUSB", pid: 8238, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_NGCDIY", pid: 22352, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_NGCRR", pid: 36906, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Ultimate2", pid: 24594, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Ultimate2RR", pid: 24595, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_UltimateBT2", pid: 24591, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_UltimateBT2RR", pid: 24593, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_Mouse", pid: 20997, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_MouseRR", pid: 20998, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_SaturnRR", pid: 36907, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_UltimateBT2C", pid: 12314, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Lashen", pid: 12318, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_HitBox", pid: 24587, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_HitBoxRR", pid: 24588, support_level: crate::types::SupportLevel::Full, support_tier: crate::types::SupportTier::Full, protocol_family: crate::types::ProtocolFamily::DInput },
crate::registry::PidRegistryRow { name: "PID_N64BT", pid: 12313, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_N64", pid: 12292, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_N64RR", pid: 36904, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_XBOXUK", pid: 12326, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_XBOXUKUSB", pid: 12327, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_LashenX", pid: 8203, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_68JP", pid: 8250, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_68JPUSB", pid: 8265, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::CandidateReadOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
crate::registry::PidRegistryRow { name: "PID_N64JoySticks", pid: 12321, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_DoubleSuper", pid: 8254, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Cube2RR", pid: 8278, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_Cube2", pid: 8249, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::Standard64 },
crate::registry::PidRegistryRow { name: "PID_ASLGJP", pid: 8282, support_level: crate::types::SupportLevel::DetectOnly, support_tier: crate::types::SupportTier::DetectOnly, protocol_family: crate::types::ProtocolFamily::JpHandshake },
]
;

View File

@@ -1,14 +1,17 @@
use crate::command::CommandId;
use crate::types::{
CommandConfidence, DeviceProfile, PidCapability, ProtocolFamily, SafetyClass, SupportEvidence,
SupportLevel, VidPid,
CommandConfidence, CommandRuntimePolicy, DeviceProfile, EvidenceConfidence, PidCapability,
ProtocolFamily, SafetyClass, SupportEvidence, SupportLevel, SupportTier, VidPid,
};
use std::collections::HashSet;
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PidRegistryRow {
pub name: &'static str,
pub pid: u16,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
}
@@ -21,12 +24,43 @@ pub struct CommandRegistryRow {
pub report_id: u8,
pub request: &'static [u8],
pub expected_response: &'static str,
pub applies_to: &'static [u16],
pub operation_group: &'static str,
}
include!(concat!(env!("OUT_DIR"), "/generated_pid_registry.rs"));
include!(concat!(env!("OUT_DIR"), "/generated_command_registry.rs"));
// Registry data is intentionally hardcoded in source files so support coverage
// is explicit in Rust code and does not depend on build-time CSV generation.
include!("pid_registry_table.rs");
include!("command_registry_table.rs");
impl CommandRegistryRow {
/// Convert evidence confidence into a stable reporting enum.
pub fn evidence_confidence(&self) -> EvidenceConfidence {
match self.confidence {
CommandConfidence::Confirmed => EvidenceConfidence::Confirmed,
CommandConfidence::Inferred => EvidenceConfidence::Inferred,
}
}
/// Runtime policy used by the session gate checker.
///
/// Policy rationale:
/// - Confirmed paths are enabled by default.
/// - Inferred safe reads can run only when experimental mode is enabled.
/// - Inferred write/unsafe paths stay blocked until explicit confirmation.
pub fn runtime_policy(&self) -> CommandRuntimePolicy {
match (self.confidence, self.safety_class) {
(CommandConfidence::Confirmed, _) => CommandRuntimePolicy::EnabledDefault,
(CommandConfidence::Inferred, SafetyClass::SafeRead) => {
CommandRuntimePolicy::ExperimentalGate
}
(CommandConfidence::Inferred, _) => CommandRuntimePolicy::BlockedUntilConfirmed,
}
}
}
pub fn pid_registry() -> &'static [PidRegistryRow] {
ensure_unique_pid_rows();
PID_REGISTRY
}
@@ -35,27 +69,133 @@ pub fn command_registry() -> &'static [CommandRegistryRow] {
}
pub fn find_pid(pid: u16) -> Option<&'static PidRegistryRow> {
PID_REGISTRY.iter().find(|row| row.pid == pid)
pid_registry().iter().find(|row| row.pid == pid)
}
pub fn find_command(id: CommandId) -> Option<&'static CommandRegistryRow> {
COMMAND_REGISTRY.iter().find(|row| row.id == id)
}
pub fn command_applies_to_pid(row: &CommandRegistryRow, pid: u16) -> bool {
row.applies_to.is_empty() || row.applies_to.contains(&pid)
}
pub fn default_capability_for(
support_level: SupportLevel,
_protocol_family: ProtocolFamily,
pid: u16,
support_tier: SupportTier,
protocol_family: ProtocolFamily,
) -> PidCapability {
match support_level {
SupportLevel::Full => PidCapability::full(),
SupportLevel::DetectOnly => PidCapability::identify_only(),
if support_tier == SupportTier::DetectOnly {
return PidCapability::identify_only();
}
const STANDARD_CANDIDATE_READ_DIAG_PIDS: &[u16] = &[
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x3004, 0x3019, 0x3100, 0x3105, 0x2100,
0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028, 0x3026, 0x3027,
];
const JP_CANDIDATE_DIAG_PIDS: &[u16] = &[0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e];
match (support_tier, pid) {
(SupportTier::CandidateReadOnly, 0x6002)
| (SupportTier::CandidateReadOnly, 0x6003)
| (SupportTier::CandidateReadOnly, 0x3010)
| (SupportTier::CandidateReadOnly, 0x3011)
| (SupportTier::CandidateReadOnly, 0x3012)
| (SupportTier::CandidateReadOnly, 0x3013)
| (SupportTier::CandidateReadOnly, 0x3004)
| (SupportTier::CandidateReadOnly, 0x3019)
| (SupportTier::CandidateReadOnly, 0x3100)
| (SupportTier::CandidateReadOnly, 0x3105)
| (SupportTier::CandidateReadOnly, 0x2100)
| (SupportTier::CandidateReadOnly, 0x2101)
| (SupportTier::CandidateReadOnly, 0x901a)
| (SupportTier::CandidateReadOnly, 0x6006)
| (SupportTier::CandidateReadOnly, 0x5203)
| (SupportTier::CandidateReadOnly, 0x5204)
| (SupportTier::CandidateReadOnly, 0x301a)
| (SupportTier::CandidateReadOnly, 0x9028)
| (SupportTier::CandidateReadOnly, 0x3026)
| (SupportTier::CandidateReadOnly, 0x3027) => PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
(SupportTier::CandidateReadOnly, 0x5200)
| (SupportTier::CandidateReadOnly, 0x5201)
| (SupportTier::CandidateReadOnly, 0x203a)
| (SupportTier::CandidateReadOnly, 0x2049)
| (SupportTier::CandidateReadOnly, 0x2028)
| (SupportTier::CandidateReadOnly, 0x202e) => PidCapability {
supports_mode: false,
supports_profile_rw: false,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
(SupportTier::CandidateReadOnly, _) if STANDARD_CANDIDATE_READ_DIAG_PIDS.contains(&pid) => {
PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
}
}
(SupportTier::CandidateReadOnly, _) if JP_CANDIDATE_DIAG_PIDS.contains(&pid) => {
PidCapability {
supports_mode: false,
supports_profile_rw: false,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
}
}
(_, 0x5209) | (_, 0x520a) => PidCapability {
supports_mode: false,
supports_profile_rw: false,
supports_boot: true,
supports_firmware: true,
supports_jp108_dedicated_map: true,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
(_, 0x6012) | (_, 0x6013) => PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: true,
supports_firmware: true,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: true,
supports_u2_button_map: true,
},
_ => {
let mut cap = PidCapability::full();
if protocol_family == ProtocolFamily::JpHandshake {
cap.supports_mode = false;
cap.supports_profile_rw = false;
}
cap.supports_jp108_dedicated_map = false;
cap.supports_u2_slot_config = false;
cap.supports_u2_button_map = false;
cap
}
}
}
pub fn default_evidence_for(support_level: SupportLevel) -> SupportEvidence {
match support_level {
SupportLevel::Full => SupportEvidence::Confirmed,
SupportLevel::DetectOnly => SupportEvidence::Inferred,
pub fn default_evidence_for(support_tier: SupportTier) -> SupportEvidence {
match support_tier {
SupportTier::Full => SupportEvidence::Confirmed,
SupportTier::CandidateReadOnly | SupportTier::DetectOnly => SupportEvidence::Inferred,
}
}
@@ -65,18 +205,35 @@ pub fn device_profile_for(vid_pid: VidPid) -> DeviceProfile {
vid_pid,
name: row.name.to_owned(),
support_level: row.support_level,
support_tier: row.support_tier,
protocol_family: row.protocol_family,
capability: default_capability_for(row.support_level, row.protocol_family),
evidence: default_evidence_for(row.support_level),
capability: default_capability_for(row.pid, row.support_tier, row.protocol_family),
evidence: default_evidence_for(row.support_tier),
}
} else {
DeviceProfile {
vid_pid,
name: "PID_UNKNOWN".to_owned(),
support_level: SupportLevel::DetectOnly,
support_tier: SupportTier::DetectOnly,
protocol_family: ProtocolFamily::Unknown,
capability: PidCapability::identify_only(),
evidence: SupportEvidence::Untested,
}
}
}
fn ensure_unique_pid_rows() {
static CHECK: OnceLock<()> = OnceLock::new();
CHECK.get_or_init(|| {
let mut seen = HashSet::new();
for row in PID_REGISTRY {
assert!(
seen.insert(row.pid),
"duplicate pid in runtime registry: {:#06x} ({})",
row.pid,
row.name
);
}
});
}

View File

@@ -2,11 +2,13 @@ use crate::command::CommandId;
use crate::error::{BitdoError, BitdoErrorCode, Result};
use crate::frame::{CommandFrame, ResponseFrame, ResponseStatus};
use crate::profile::ProfileBlob;
use crate::registry::{device_profile_for, find_command, find_pid, CommandRegistryRow};
use crate::registry::{
command_applies_to_pid, device_profile_for, find_command, find_pid, CommandRegistryRow,
};
use crate::transport::Transport;
use crate::types::{
CommandConfidence, DeviceProfile, PidCapability, ProtocolFamily, SafetyClass, SupportEvidence,
SupportLevel, VidPid,
CommandRuntimePolicy, DeviceProfile, EvidenceConfidence, PidCapability, ProtocolFamily,
SafetyClass, SupportEvidence, SupportLevel, SupportTier, VidPid,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
@@ -83,15 +85,26 @@ pub struct CommandExecutionReport {
pub struct DiagCommandStatus {
pub command: CommandId,
pub ok: bool,
pub confidence: EvidenceConfidence,
pub is_experimental: bool,
pub severity: DiagSeverity,
pub error_code: Option<BitdoErrorCode>,
pub detail: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DiagSeverity {
Ok,
Warning,
NeedsAttention,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiagProbeResult {
pub target: VidPid,
pub profile_name: String,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
pub capability: PidCapability,
pub evidence: SupportEvidence,
@@ -104,6 +117,7 @@ pub struct IdentifyResult {
pub target: VidPid,
pub profile_name: String,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
pub capability: PidCapability,
pub evidence: SupportEvidence,
@@ -187,6 +201,7 @@ impl<T: Transport> DeviceSession<T> {
target: self.target,
profile_name: profile.name,
support_level: profile.support_level,
support_tier: profile.support_tier,
protocol_family: profile.protocol_family,
capability: profile.capability,
evidence: profile.evidence,
@@ -195,33 +210,66 @@ impl<T: Transport> DeviceSession<T> {
}
pub fn diag_probe(&mut self) -> DiagProbeResult {
let checks = [
let target_pid = self.target.pid;
let checks_to_run = [
CommandId::GetPid,
CommandId::GetReportRevision,
CommandId::GetMode,
CommandId::GetControllerVersion,
// Inferred safe reads are intentionally included in diagnostics so
// users always see signal quality, but results are labeled
// experimental and only strict safety conditions escalate.
CommandId::GetSuperButton,
CommandId::ReadProfile,
]
.iter()
.map(|cmd| match self.send_command(*cmd, None) {
Ok(_) => DiagCommandStatus {
command: *cmd,
ok: true,
error_code: None,
detail: "ok".to_owned(),
},
Err(err) => DiagCommandStatus {
command: *cmd,
ok: false,
error_code: Some(err.code()),
detail: err.to_string(),
},
.filter_map(|cmd| {
let row = find_command(*cmd)?;
if row.safety_class != SafetyClass::SafeRead {
return None;
}
if !command_applies_to_pid(row, target_pid) {
return None;
}
Some((*cmd, row.runtime_policy(), row.evidence_confidence()))
})
.collect::<Vec<_>>();
let mut checks = Vec::with_capacity(checks_to_run.len());
for (cmd, runtime_policy, confidence) in checks_to_run {
match self.send_command(cmd, None) {
Ok(_) => checks.push(DiagCommandStatus {
command: cmd,
ok: true,
confidence,
is_experimental: runtime_policy == CommandRuntimePolicy::ExperimentalGate,
severity: DiagSeverity::Ok,
error_code: None,
detail: "ok".to_owned(),
}),
Err(err) => checks.push(DiagCommandStatus {
command: cmd,
ok: false,
confidence,
is_experimental: runtime_policy == CommandRuntimePolicy::ExperimentalGate,
severity: classify_diag_failure(
cmd,
runtime_policy,
confidence,
err.code(),
self.target.pid,
),
error_code: Some(err.code()),
detail: err.to_string(),
}),
}
}
DiagProbeResult {
target: self.target,
profile_name: self.profile.name.clone(),
support_level: self.profile.support_level,
support_tier: self.profile.support_tier,
protocol_family: self.profile.protocol_family,
capability: self.profile.capability,
evidence: self.profile.evidence,
@@ -290,6 +338,116 @@ impl<T: Transport> DeviceSession<T> {
Ok(())
}
pub fn jp108_read_dedicated_mappings(&mut self) -> Result<Vec<(u8, u16)>> {
let resp = self.send_command(CommandId::Jp108ReadDedicatedMappings, None)?;
Ok(parse_indexed_u16_table(&resp.raw, 10))
}
pub fn jp108_write_dedicated_mapping(
&mut self,
index: u8,
target_hid_usage: u16,
) -> Result<()> {
let row = self.ensure_command_allowed(CommandId::Jp108WriteDedicatedMapping)?;
let mut payload = row.request.to_vec();
if payload.len() < 7 {
return Err(BitdoError::InvalidInput(
"Jp108WriteDedicatedMapping payload shorter than expected".to_owned(),
));
}
payload[4] = index;
let usage = target_hid_usage.to_le_bytes();
payload[5] = usage[0];
payload[6] = usage[1];
self.send_row(row, Some(&payload))?;
Ok(())
}
pub fn u2_get_current_slot(&mut self) -> Result<u8> {
let resp = self.send_command(CommandId::U2GetCurrentSlot, None)?;
Ok(resp.parsed_fields.get("slot").copied().unwrap_or(0) as u8)
}
pub fn u2_read_config_slot(&mut self, slot: u8) -> Result<Vec<u8>> {
let row = self.ensure_command_allowed(CommandId::U2ReadConfigSlot)?;
let mut payload = row.request.to_vec();
if payload.len() > 4 {
payload[4] = slot;
}
let resp = self.send_row(row, Some(&payload))?;
Ok(resp.raw)
}
pub fn u2_write_config_slot(&mut self, slot: u8, config_blob: &[u8]) -> Result<()> {
let row = self.ensure_command_allowed(CommandId::U2WriteConfigSlot)?;
let mut payload = row.request.to_vec();
if payload.len() < 8 {
return Err(BitdoError::InvalidInput(
"U2WriteConfigSlot payload shorter than expected".to_owned(),
));
}
payload[4] = slot;
let copy_len = config_blob.len().min(payload.len().saturating_sub(8));
if copy_len > 0 {
payload[8..8 + copy_len].copy_from_slice(&config_blob[..copy_len]);
}
self.send_row(row, Some(&payload))?;
Ok(())
}
pub fn u2_read_button_map(&mut self, slot: u8) -> Result<Vec<(u8, u16)>> {
let row = self.ensure_command_allowed(CommandId::U2ReadButtonMap)?;
let mut payload = row.request.to_vec();
if payload.len() > 4 {
payload[4] = slot;
}
let resp = self.send_row(row, Some(&payload))?;
Ok(parse_indexed_u16_table(&resp.raw, 17))
}
pub fn u2_write_button_map(&mut self, slot: u8, mappings: &[(u8, u16)]) -> Result<()> {
let row = self.ensure_command_allowed(CommandId::U2WriteButtonMap)?;
let mut payload = row.request.to_vec();
if payload.len() < 8 {
return Err(BitdoError::InvalidInput(
"U2WriteButtonMap payload shorter than expected".to_owned(),
));
}
payload[4] = slot;
for (index, usage) in mappings {
let pos = 8usize.saturating_add((*index as usize).saturating_mul(2));
if pos + 1 < payload.len() {
let bytes = usage.to_le_bytes();
payload[pos] = bytes[0];
payload[pos + 1] = bytes[1];
}
}
self.send_row(row, Some(&payload))?;
Ok(())
}
pub fn u2_set_mode(&mut self, mode: u8) -> Result<ModeState> {
let row = self.ensure_command_allowed(CommandId::U2SetMode)?;
let mut payload = row.request.to_vec();
if payload.len() < 5 {
return Err(BitdoError::InvalidInput(
"U2SetMode payload shorter than expected".to_owned(),
));
}
payload[4] = mode;
self.send_row(row, Some(&payload))?;
Ok(ModeState {
mode,
source: "U2SetMode".to_owned(),
})
}
pub fn enter_bootloader(&mut self) -> Result<()> {
self.send_command(CommandId::EnterBootloaderA, None)?;
self.send_command(CommandId::EnterBootloaderB, None)?;
@@ -529,12 +687,28 @@ impl<T: Transport> DeviceSession<T> {
fn ensure_command_allowed(&self, command: CommandId) -> Result<&'static CommandRegistryRow> {
let row = find_command(command).ok_or(BitdoError::UnknownCommand(command))?;
if row.confidence == CommandConfidence::Inferred && !self.config.experimental {
return Err(BitdoError::ExperimentalRequired { command });
// Gate 1: confidence/runtime policy.
// We intentionally keep inferred write/unsafe paths non-executable until
// they are upgraded to confirmed evidence.
match row.runtime_policy() {
CommandRuntimePolicy::EnabledDefault => {}
CommandRuntimePolicy::ExperimentalGate => {
if !self.config.experimental {
return Err(BitdoError::ExperimentalRequired { command });
}
}
CommandRuntimePolicy::BlockedUntilConfirmed => {
return Err(BitdoError::UnsupportedForPid {
command,
pid: self.target.pid,
});
}
}
// Gate 2: PID/family/capability applicability.
if !is_command_allowed_by_family(self.profile.protocol_family, command)
|| !is_command_allowed_by_capability(self.profile.capability, command)
|| !command_applies_to_pid(row, self.target.pid)
{
return Err(BitdoError::UnsupportedForPid {
command,
@@ -542,8 +716,19 @@ impl<T: Transport> DeviceSession<T> {
});
}
// Gate 3: support-tier restrictions.
if self.profile.support_tier == SupportTier::CandidateReadOnly
&& !is_command_allowed_for_candidate_pid(self.target.pid, command, row.safety_class)
{
return Err(BitdoError::UnsupportedForPid {
command,
pid: self.target.pid,
});
}
// Gate 4: explicit unsafe confirmation requirements.
if row.safety_class.is_unsafe() {
if self.profile.support_level != SupportLevel::Full {
if self.profile.support_tier != SupportTier::Full {
return Err(BitdoError::UnsupportedForPid {
command,
pid: self.target.pid,
@@ -555,7 +740,7 @@ impl<T: Transport> DeviceSession<T> {
}
if row.safety_class == SafetyClass::SafeWrite
&& self.profile.support_level == SupportLevel::DetectOnly
&& self.profile.support_tier != SupportTier::Full
{
return Err(BitdoError::UnsupportedForPid {
command,
@@ -567,6 +752,83 @@ impl<T: Transport> DeviceSession<T> {
}
}
fn classify_diag_failure(
command: CommandId,
runtime_policy: CommandRuntimePolicy,
confidence: EvidenceConfidence,
code: BitdoErrorCode,
pid: u16,
) -> DiagSeverity {
if runtime_policy != CommandRuntimePolicy::ExperimentalGate
|| confidence != EvidenceConfidence::Inferred
{
return DiagSeverity::Warning;
}
// Escalation is intentionally narrow for inferred checks:
// - identity mismatch / impossible transitions
// - command/schema applicability mismatch
// - precondition/capability mismatches implied by unsupported errors
let identity_or_transition_issue = matches!(
(command, code),
(CommandId::GetPid, BitdoErrorCode::InvalidResponse)
| (CommandId::GetPid, BitdoErrorCode::MalformedResponse)
| (CommandId::GetMode, BitdoErrorCode::InvalidResponse)
| (CommandId::GetModeAlt, BitdoErrorCode::InvalidResponse)
| (CommandId::ReadProfile, BitdoErrorCode::InvalidResponse)
| (
CommandId::GetControllerVersion,
BitdoErrorCode::InvalidResponse
)
| (CommandId::Version, BitdoErrorCode::InvalidResponse)
);
if identity_or_transition_issue {
return DiagSeverity::NeedsAttention;
}
if code == BitdoErrorCode::UnsupportedForPid
&& find_command(command)
.map(|row| command_applies_to_pid(row, pid))
.unwrap_or(false)
{
return DiagSeverity::NeedsAttention;
}
DiagSeverity::Warning
}
fn is_command_allowed_for_candidate_pid(pid: u16, command: CommandId, safety: SafetyClass) -> bool {
if safety != SafetyClass::SafeRead {
return false;
}
const BASE_DIAG_READS: &[CommandId] = &[
CommandId::GetPid,
CommandId::GetReportRevision,
CommandId::GetControllerVersion,
CommandId::Version,
CommandId::Idle,
];
const STANDARD_CANDIDATE_PIDS: &[u16] = &[
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x3004, 0x3019, 0x3100, 0x3105, 0x2100,
0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028, 0x3026, 0x3027,
];
const JP_CANDIDATE_PIDS: &[u16] = &[0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e];
if BASE_DIAG_READS.contains(&command) {
return STANDARD_CANDIDATE_PIDS.contains(&pid) || JP_CANDIDATE_PIDS.contains(&pid);
}
if STANDARD_CANDIDATE_PIDS.contains(&pid) {
return matches!(
command,
CommandId::GetMode | CommandId::GetModeAlt | CommandId::ReadProfile
);
}
false
}
fn is_command_allowed_by_capability(cap: PidCapability, command: CommandId) -> bool {
match command {
CommandId::GetPid
@@ -580,8 +842,29 @@ fn is_command_allowed_by_capability(cap: PidCapability, command: CommandId) -> b
CommandId::EnterBootloaderA
| CommandId::EnterBootloaderB
| CommandId::EnterBootloaderC
| CommandId::ExitBootloader => cap.supports_boot,
CommandId::FirmwareChunk | CommandId::FirmwareCommit => cap.supports_firmware,
| CommandId::ExitBootloader
| CommandId::Jp108EnterBootloader
| CommandId::Jp108ExitBootloader
| CommandId::U2EnterBootloader
| CommandId::U2ExitBootloader => cap.supports_boot,
CommandId::FirmwareChunk
| CommandId::FirmwareCommit
| CommandId::Jp108FirmwareChunk
| CommandId::Jp108FirmwareCommit
| CommandId::U2FirmwareChunk
| CommandId::U2FirmwareCommit => cap.supports_firmware,
CommandId::Jp108ReadDedicatedMappings
| CommandId::Jp108WriteDedicatedMapping
| CommandId::Jp108ReadFeatureFlags
| CommandId::Jp108WriteFeatureFlags
| CommandId::Jp108ReadVoice
| CommandId::Jp108WriteVoice => cap.supports_jp108_dedicated_map,
CommandId::U2GetCurrentSlot
| CommandId::U2ReadConfigSlot
| CommandId::U2WriteConfigSlot => cap.supports_u2_slot_config,
CommandId::U2ReadButtonMap | CommandId::U2WriteButtonMap | CommandId::U2SetMode => {
cap.supports_u2_button_map
}
}
}
@@ -602,6 +885,16 @@ fn is_command_allowed_by_family(family: ProtocolFamily, command: CommandId) -> b
| CommandId::WriteProfile
| CommandId::FirmwareChunk
| CommandId::FirmwareCommit
| CommandId::U2GetCurrentSlot
| CommandId::U2ReadConfigSlot
| CommandId::U2WriteConfigSlot
| CommandId::U2ReadButtonMap
| CommandId::U2WriteButtonMap
| CommandId::U2SetMode
| CommandId::U2EnterBootloader
| CommandId::U2FirmwareChunk
| CommandId::U2FirmwareCommit
| CommandId::U2ExitBootloader
),
ProtocolFamily::DS4Boot => matches!(
command,
@@ -653,6 +946,21 @@ pub fn validate_response(command: CommandId, response: &[u8]) -> ResponseStatus
ResponseStatus::Invalid
}
}
CommandId::Jp108ReadDedicatedMappings
| CommandId::Jp108ReadFeatureFlags
| CommandId::Jp108ReadVoice
| CommandId::U2ReadConfigSlot
| CommandId::U2ReadButtonMap
| CommandId::U2GetCurrentSlot => {
if response.len() < 6 {
return ResponseStatus::Malformed;
}
if response[0] == 0x02 && response[1] == 0x05 {
ResponseStatus::Ok
} else {
ResponseStatus::Invalid
}
}
CommandId::GetControllerVersion | CommandId::Version => {
if response.len() < 5 {
return ResponseStatus::Malformed;
@@ -689,6 +997,12 @@ fn minimum_response_len(command: CommandId) -> usize {
CommandId::GetPid => 24,
CommandId::GetReportRevision => 6,
CommandId::GetMode | CommandId::GetModeAlt => 6,
CommandId::U2GetCurrentSlot => 6,
CommandId::Jp108ReadDedicatedMappings
| CommandId::Jp108ReadFeatureFlags
| CommandId::Jp108ReadVoice
| CommandId::U2ReadConfigSlot
| CommandId::U2ReadButtonMap => 12,
CommandId::GetControllerVersion | CommandId::Version => 5,
_ => 2,
}
@@ -709,7 +1023,27 @@ fn parse_fields(command: CommandId, response: &[u8]) -> BTreeMap<String, u32> {
parsed.insert("version_x100".to_owned(), fw);
parsed.insert("beta".to_owned(), response[4] as u32);
}
CommandId::U2GetCurrentSlot if response.len() >= 6 => {
parsed.insert("slot".to_owned(), response[5] as u32);
}
_ => {}
}
parsed
}
fn parse_indexed_u16_table(raw: &[u8], expected_items: usize) -> Vec<(u8, u16)> {
let mut out = Vec::with_capacity(expected_items);
let offset = if raw.len() >= 8 { 8 } else { 2 };
for idx in 0..expected_items {
let pos = offset + idx * 2;
let usage = if pos + 1 < raw.len() {
u16::from_le_bytes([raw[pos], raw[pos + 1]])
} else {
0
};
out.push((idx as u8, usage));
}
out
}

View File

@@ -50,6 +50,13 @@ pub enum SupportLevel {
DetectOnly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum SupportTier {
DetectOnly,
CandidateReadOnly,
Full,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum SafetyClass {
SafeRead,
@@ -70,6 +77,24 @@ pub enum CommandConfidence {
Inferred,
}
/// Runtime execution policy for a declared command path.
///
/// This allows us to hardcode every evidenced command in the registry while
/// still keeping unsafe or low-confidence paths blocked by default.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CommandRuntimePolicy {
EnabledDefault,
ExperimentalGate,
BlockedUntilConfirmed,
}
/// Evidence confidence used by policy/reporting surfaces.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum EvidenceConfidence {
Confirmed,
Inferred,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum SupportEvidence {
Confirmed,
@@ -83,6 +108,9 @@ pub struct PidCapability {
pub supports_profile_rw: bool,
pub supports_boot: bool,
pub supports_firmware: bool,
pub supports_jp108_dedicated_map: bool,
pub supports_u2_slot_config: bool,
pub supports_u2_button_map: bool,
}
impl PidCapability {
@@ -92,6 +120,9 @@ impl PidCapability {
supports_profile_rw: true,
supports_boot: true,
supports_firmware: true,
supports_jp108_dedicated_map: true,
supports_u2_slot_config: true,
supports_u2_button_map: true,
}
}
@@ -101,6 +132,9 @@ impl PidCapability {
supports_profile_rw: false,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
}
}
}
@@ -110,6 +144,7 @@ pub struct DeviceProfile {
pub vid_pid: VidPid,
pub name: String,
pub support_level: SupportLevel,
pub support_tier: SupportTier,
pub protocol_family: ProtocolFamily,
pub capability: PidCapability,
pub evidence: SupportEvidence,

View File

@@ -0,0 +1,19 @@
[package]
name = "bitdo_tui"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
[dependencies]
anyhow = { workspace = true }
ratatui = { workspace = true }
crossterm = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
toml = { workspace = true }
bitdo_proto = { path = "../bitdo_proto" }
bitdo_app_core = { path = "../bitdo_app_core" }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }

View File

@@ -0,0 +1,69 @@
use anyhow::{anyhow, Result};
use std::path::Path;
use std::process::{Command, Stdio};
/// Open a file or directory with the user's default desktop application.
pub(crate) fn open_path_with_default_app(path: &Path) -> Result<()> {
let mut cmd = if cfg!(target_os = "macos") {
let mut c = Command::new("open");
c.arg(path);
c
} else {
let mut c = Command::new("xdg-open");
c.arg(path);
c
};
let status = cmd.status()?;
if status.success() {
Ok(())
} else {
Err(anyhow!(
"failed to open path with default app: {}",
path.to_string_lossy()
))
}
}
/// Copy text into the system clipboard using platform-appropriate commands.
pub(crate) fn copy_text_to_clipboard(text: &str) -> Result<()> {
if cfg!(target_os = "macos") {
return copy_via_command("pbcopy", &[], text);
}
if command_exists("wl-copy") {
return copy_via_command("wl-copy", &[], text);
}
if command_exists("xclip") {
return copy_via_command("xclip", &["-selection", "clipboard"], text);
}
Err(anyhow!(
"no clipboard utility found (tried pbcopy/wl-copy/xclip)"
))
}
fn command_exists(name: &str) -> bool {
Command::new("sh")
.arg("-c")
.arg(format!("command -v {name} >/dev/null 2>&1"))
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn copy_via_command(command: &str, args: &[&str], text: &str) -> Result<()> {
let mut child = Command::new(command)
.args(args)
.stdin(Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.as_mut() {
use std::io::Write as _;
stdin.write_all(text.as_bytes())?;
}
let status = child.wait()?;
if status.success() {
Ok(())
} else {
Err(anyhow!("clipboard command failed: {command}"))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
use crate::ReportSaveMode;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct PersistedSettings {
#[serde(default = "default_settings_schema_version")]
schema_version: u32,
#[serde(default)]
advanced_mode: bool,
#[serde(default)]
report_save_mode: ReportSaveMode,
}
const fn default_settings_schema_version() -> u32 {
1
}
/// Persist beginner/advanced preferences in a small TOML config file.
pub(crate) fn persist_user_settings(
path: &Path,
advanced_mode: bool,
report_save_mode: ReportSaveMode,
) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = toml::to_string_pretty(&PersistedSettings {
schema_version: default_settings_schema_version(),
advanced_mode,
report_save_mode,
})
.map_err(|err| anyhow!("failed to serialize user settings: {err}"))?;
std::fs::write(path, body)?;
Ok(())
}

View File

@@ -0,0 +1,217 @@
use crate::AppDevice;
use anyhow::{anyhow, Result};
use bitdo_app_core::FirmwareFinalReport;
use bitdo_proto::{DiagProbeResult, SupportLevel, SupportTier};
use chrono::Utc;
use serde::Serialize;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
const REPORT_MAX_COUNT: usize = 20;
const REPORT_MAX_AGE_DAYS: u64 = 30;
#[derive(Clone, Debug, Serialize)]
struct SupportReport {
schema_version: u32,
generated_at_utc: String,
operation: String,
device: Option<SupportReportDevice>,
status: String,
message: String,
diag: Option<DiagProbeResult>,
firmware: Option<FirmwareFinalReport>,
}
#[derive(Clone, Debug, Serialize)]
struct SupportReportDevice {
vid: u16,
pid: u16,
name: String,
canonical_id: String,
runtime_label: String,
serial: Option<String>,
support_level: String,
support_tier: String,
}
/// Persist a troubleshooting report as TOML.
///
/// Reports are intended for failure/support paths and are named with a timestamp plus
/// a serial-or-VID/PID token so users can share deterministic artifacts with support.
pub(crate) async fn persist_support_report(
operation: &str,
device: Option<&AppDevice>,
status: &str,
message: String,
diag: Option<&DiagProbeResult>,
firmware: Option<&FirmwareFinalReport>,
) -> Result<PathBuf> {
let now = Utc::now();
let report = SupportReport {
schema_version: 1,
generated_at_utc: now.to_rfc3339(),
operation: operation.to_owned(),
device: device.map(|d| SupportReportDevice {
vid: d.vid_pid.vid,
pid: d.vid_pid.pid,
name: d.name.clone(),
canonical_id: d.name.clone(),
runtime_label: d.support_status().as_str().to_owned(),
serial: d.serial.clone(),
support_level: match d.support_level {
SupportLevel::Full => "full".to_owned(),
SupportLevel::DetectOnly => "detect-only".to_owned(),
},
support_tier: match d.support_tier {
SupportTier::Full => "full".to_owned(),
SupportTier::CandidateReadOnly => "candidate-readonly".to_owned(),
SupportTier::DetectOnly => "detect-only".to_owned(),
},
}),
status: status.to_owned(),
message,
diag: diag.cloned(),
firmware: firmware.cloned(),
};
let report_dir = default_report_directory();
tokio::fs::create_dir_all(&report_dir).await?;
let token = report_subject_token(device);
let file_name = format!(
"{}-{}-{}.toml",
sanitize_token(operation),
now.format("%Y%m%d-%H%M%S"),
token
);
let path = report_dir.join(file_name);
let body = toml::to_string_pretty(&report)
.map_err(|err| anyhow!("failed to serialize support report: {err}"))?;
tokio::fs::write(&path, body).await?;
let _ = prune_reports_on_write().await;
Ok(path)
}
/// Startup pruning is age-based to keep stale files out of user systems.
pub(crate) async fn prune_reports_on_startup() -> Result<()> {
prune_reports_by_age().await
}
/// Write-time pruning is count-based to keep growth bounded deterministically.
async fn prune_reports_on_write() -> Result<()> {
prune_reports_by_count().await
}
pub(crate) fn report_subject_token(device: Option<&AppDevice>) -> String {
if let Some(device) = device {
if let Some(serial) = device.serial.as_deref() {
let cleaned = sanitize_token(serial);
if !cleaned.is_empty() {
return cleaned;
}
}
return format!("{:04x}{:04x}", device.vid_pid.vid, device.vid_pid.pid);
}
"unknown".to_owned()
}
fn sanitize_token(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
out.push(ch);
} else {
out.push('_');
}
}
out.trim_matches('_').to_owned()
}
fn default_report_directory() -> PathBuf {
if cfg!(target_os = "macos") {
return home_directory()
.join("Library")
.join("Application Support")
.join("OpenBitdo")
.join("reports");
}
if cfg!(target_os = "linux") {
if let Some(xdg_data_home) = std::env::var_os("XDG_DATA_HOME") {
return PathBuf::from(xdg_data_home)
.join("openbitdo")
.join("reports");
}
return home_directory()
.join(".local")
.join("share")
.join("openbitdo")
.join("reports");
}
std::env::temp_dir().join("openbitdo").join("reports")
}
async fn list_report_files() -> Result<Vec<PathBuf>> {
let report_dir = default_report_directory();
let mut out = Vec::new();
let mut entries = match tokio::fs::read_dir(&report_dir).await {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(out),
Err(err) => return Err(err.into()),
};
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("toml") {
out.push(path);
}
}
Ok(out)
}
async fn prune_reports_by_count() -> Result<()> {
let mut files = list_report_files().await?;
files.sort_by_key(|path| {
std::fs::metadata(path)
.and_then(|meta| meta.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
});
files.reverse();
for path in files.into_iter().skip(REPORT_MAX_COUNT) {
let _ = tokio::fs::remove_file(path).await;
}
Ok(())
}
async fn prune_reports_by_age() -> Result<()> {
let now = SystemTime::now();
let max_age = Duration::from_secs(REPORT_MAX_AGE_DAYS * 24 * 60 * 60);
for path in list_report_files().await? {
let Ok(meta) = std::fs::metadata(&path) else {
continue;
};
let Ok(modified) = meta.modified() else {
continue;
};
if now.duration_since(modified).unwrap_or_default() > max_age {
let _ = tokio::fs::remove_file(path).await;
}
}
Ok(())
}
fn home_directory() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}

View File

@@ -0,0 +1,370 @@
use super::*;
use crate::support_report::report_subject_token;
use bitdo_app_core::{FirmwareOutcome, OpenBitdoCoreConfig};
use bitdo_proto::SupportLevel;
#[test]
fn about_state_roundtrip_returns_home() {
let mut app = TuiApp::default();
app.refresh_devices(vec![AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "Test".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: Some("SERIAL1".to_owned()),
connected: true,
}]);
app.open_about();
assert_eq!(app.state, TuiWorkflowState::About);
app.close_overlay();
assert_eq!(app.state, TuiWorkflowState::Home);
}
#[test]
fn refresh_devices_without_any_device_enters_wait_state() {
let mut app = TuiApp::default();
app.refresh_devices(Vec::new());
assert_eq!(app.state, TuiWorkflowState::WaitForDevice);
assert!(app.selected.is_none());
}
#[test]
fn refresh_devices_autoselects_single_device() {
let mut app = TuiApp::default();
app.refresh_devices(vec![AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "One".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: None,
connected: true,
}]);
assert_eq!(app.state, TuiWorkflowState::Home);
assert_eq!(app.selected_index, 0);
assert_eq!(app.selected, Some(VidPid::new(0x2dc8, 0x6009)));
}
#[test]
fn serial_token_prefers_serial_then_vidpid() {
let with_serial = AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "S".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: Some("ABC 123".to_owned()),
connected: true,
};
assert_eq!(report_subject_token(Some(&with_serial)), "ABC_123");
let without_serial = AppDevice {
serial: None,
..with_serial
};
assert_eq!(report_subject_token(Some(&without_serial)), "2dc86009");
}
#[test]
fn launch_options_default_to_failure_only_reports() {
let opts = TuiLaunchOptions::default();
assert_eq!(opts.report_save_mode, ReportSaveMode::FailureOnly);
}
#[test]
fn blocked_panel_text_matches_support_tier() {
let mut app = TuiApp::default();
app.refresh_devices(vec![AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x2100),
name: "Candidate".to_owned(),
support_level: SupportLevel::DetectOnly,
support_tier: SupportTier::CandidateReadOnly,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability {
supports_mode: true,
supports_profile_rw: true,
supports_boot: false,
supports_firmware: false,
supports_jp108_dedicated_map: false,
supports_u2_slot_config: false,
supports_u2_button_map: false,
},
evidence: bitdo_proto::SupportEvidence::Inferred,
serial: None,
connected: true,
}]);
let selected = app.selected_device().expect("selected");
let text = blocked_action_panel_text(selected);
assert!(text.contains("blocked"));
assert!(text.contains("Status shown as Blocked"));
assert_eq!(beginner_status_label(selected), "Blocked");
}
#[test]
fn non_advanced_report_mode_skips_off_setting() {
let mut app = TuiApp {
advanced_mode: false,
..Default::default()
};
assert_eq!(app.report_save_mode, ReportSaveMode::FailureOnly);
app.cycle_report_save_mode().expect("cycle");
assert_eq!(app.report_save_mode, ReportSaveMode::Always);
app.cycle_report_save_mode().expect("cycle");
assert_eq!(app.report_save_mode, ReportSaveMode::FailureOnly);
}
#[test]
fn unknown_device_label_is_beginner_friendly() {
let device = AppDevice {
vid_pid: VidPid::new(0x2dc8, 0xabcd),
name: "PID_UNKNOWN".to_owned(),
support_level: SupportLevel::DetectOnly,
support_tier: SupportTier::DetectOnly,
protocol_family: bitdo_proto::ProtocolFamily::Unknown,
capability: bitdo_proto::PidCapability::identify_only(),
evidence: bitdo_proto::SupportEvidence::Untested,
serial: None,
connected: true,
};
let label = super::display_device_name(&device);
assert!(label.contains("Unknown 8BitDo Device"));
assert!(label.contains("2dc8:abcd"));
}
#[tokio::test]
async fn home_refresh_loads_devices() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
assert!(!app.devices.is_empty());
assert!(app.selected_device().is_some());
}
#[tokio::test]
async fn run_tui_app_no_ui_blocks_detect_only_pid() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
let result = run_tui_app(
core,
TuiLaunchOptions {
no_ui: true,
selected_vid_pid: Some(VidPid::new(0x2dc8, 0x2100)),
..Default::default()
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_tui_app_no_ui_full_support_completes() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
run_tui_app(
core,
TuiLaunchOptions {
no_ui: true,
selected_vid_pid: Some(VidPid::new(0x2dc8, 0x6009)),
..Default::default()
},
)
.await
.expect("run app");
}
#[tokio::test]
async fn tui_flow_with_manual_path_completes() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
default_chunk_size: 16,
progress_interval_ms: 1,
..Default::default()
});
let path = std::env::temp_dir().join("openbitdo-tui-flow.bin");
tokio::fs::write(&path, vec![1u8; 128])
.await
.expect("write");
let report = run_tui_flow(
core,
TuiRunRequest {
vid_pid: VidPid::new(0x2dc8, 0x6009),
firmware_path: path.clone(),
allow_unsafe: true,
brick_risk_ack: true,
experimental: true,
chunk_size: Some(32),
acknowledged_risk: true,
no_ui: true,
},
)
.await
.expect("run tui flow");
assert_eq!(report.status, FirmwareOutcome::Completed);
let _ = tokio::fs::remove_file(path).await;
}
#[tokio::test]
async fn support_report_is_toml_file() {
let device = AppDevice {
vid_pid: VidPid::new(0x2dc8, 0x6009),
name: "Test".to_owned(),
support_level: SupportLevel::Full,
support_tier: SupportTier::Full,
protocol_family: bitdo_proto::ProtocolFamily::Standard64,
capability: bitdo_proto::PidCapability::full(),
evidence: bitdo_proto::SupportEvidence::Confirmed,
serial: Some("RPT-1".to_owned()),
connected: true,
};
let report_path = persist_support_report(
"diag-probe",
Some(&device),
"ok",
"all checks passed".to_owned(),
None,
None,
)
.await
.expect("report path");
assert_eq!(
report_path.extension().and_then(|s| s.to_str()),
Some("toml")
);
let _ = tokio::fs::remove_file(report_path).await;
}
#[tokio::test]
async fn update_action_enters_jp108_wizard_for_jp108_device() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
let jp108_idx = app
.devices
.iter()
.position(|d| d.vid_pid.pid == 0x5209)
.expect("jp108 fixture");
app.select_index(jp108_idx);
app.state = TuiWorkflowState::Home;
let mut terminal = None;
let mut events = None;
let opts = TuiLaunchOptions::default();
execute_home_action(
&core,
&mut terminal,
&mut app,
&opts,
&mut events,
HomeAction::Update,
)
.await
.expect("execute");
assert_eq!(app.state, TuiWorkflowState::Jp108Mapping);
assert!(!app.jp108_mappings.is_empty());
}
#[tokio::test]
async fn update_action_enters_u2_wizard_for_ultimate2_device() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
let u2_idx = app
.devices
.iter()
.position(|d| d.vid_pid.pid == 0x6012)
.expect("u2 fixture");
app.select_index(u2_idx);
app.state = TuiWorkflowState::Home;
let mut terminal = None;
let mut events = None;
let opts = TuiLaunchOptions::default();
execute_home_action(
&core,
&mut terminal,
&mut app,
&opts,
&mut events,
HomeAction::Update,
)
.await
.expect("execute");
assert_eq!(app.state, TuiWorkflowState::U2CoreProfile);
assert!(app.u2_profile.is_some());
}
#[tokio::test]
async fn device_flow_backup_apply_sets_backup_id() {
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: true,
..Default::default()
});
let mut app = TuiApp::default();
app.refresh_devices(core.list_devices().await.expect("devices"));
let jp108_idx = app
.devices
.iter()
.position(|d| d.vid_pid.pid == 0x5209)
.expect("jp108 fixture");
app.select_index(jp108_idx);
app.begin_jp108_mapping(
core.jp108_read_dedicated_mapping(VidPid::new(0x2dc8, 0x5209))
.await
.expect("read"),
);
let mut terminal = None;
let mut events = None;
let opts = TuiLaunchOptions::default();
execute_device_flow_action(
&core,
&mut terminal,
&mut app,
&opts,
&mut events,
DeviceFlowAction::BackupApply,
)
.await
.expect("apply");
assert!(app.latest_backup.is_some());
}

View File

@@ -1,20 +0,0 @@
[package]
name = "bitdoctl"
version = "0.1.0"
edition = "2021"
license = "MIT"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
serde_json = { workspace = true }
hex = { workspace = true }
bitdo_proto = { path = "../bitdo_proto" }
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.1"
[[test]]
name = "cli_snapshot"
path = "../../tests/cli_snapshot.rs"

View File

@@ -1,518 +0,0 @@
use anyhow::{anyhow, Result};
use bitdo_proto::{
command_registry, device_profile_for, enumerate_hid_devices, BitdoErrorCode, CommandId,
DeviceSession, FirmwareTransferReport, HidTransport, MockTransport, ProfileBlob, RetryPolicy,
SessionConfig, TimeoutProfile, Transport, VidPid,
};
use clap::{Parser, Subcommand};
use serde_json::json;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(name = "bitdoctl")]
#[command(about = "OpenBitdo clean-room protocol CLI")]
struct Cli {
#[arg(long)]
vid: Option<String>,
#[arg(long)]
pid: Option<String>,
#[arg(long)]
json: bool,
#[arg(long = "unsafe")]
allow_unsafe: bool,
#[arg(long = "i-understand-brick-risk")]
brick_risk_ack: bool,
#[arg(long)]
experimental: bool,
#[arg(long)]
mock: bool,
#[arg(long, default_value_t = 3)]
max_attempts: u8,
#[arg(long, default_value_t = 10)]
backoff_ms: u64,
#[arg(long, default_value_t = 200)]
probe_timeout_ms: u64,
#[arg(long, default_value_t = 400)]
io_timeout_ms: u64,
#[arg(long, default_value_t = 1200)]
firmware_timeout_ms: u64,
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
List,
Identify,
Diag {
#[command(subcommand)]
command: DiagCommand,
},
Profile {
#[command(subcommand)]
command: ProfileCommand,
},
Mode {
#[command(subcommand)]
command: ModeCommand,
},
Boot {
#[command(subcommand)]
command: BootCommand,
},
Fw {
#[command(subcommand)]
command: FwCommand,
},
}
#[derive(Debug, Subcommand)]
enum DiagCommand {
Probe,
}
#[derive(Debug, Subcommand)]
enum ProfileCommand {
Dump {
#[arg(long)]
slot: u8,
},
Apply {
#[arg(long)]
slot: u8,
#[arg(long)]
file: PathBuf,
},
}
#[derive(Debug, Subcommand)]
enum ModeCommand {
Get,
Set {
#[arg(long)]
mode: u8,
},
}
#[derive(Debug, Subcommand)]
enum BootCommand {
Enter,
Exit,
}
#[derive(Debug, Subcommand)]
enum FwCommand {
Write {
#[arg(long)]
file: PathBuf,
#[arg(long, default_value_t = 56)]
chunk_size: usize,
#[arg(long)]
dry_run: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
if let Err(err) = run(cli) {
eprintln!("error: {err}");
return Err(err);
}
Ok(())
}
fn run(cli: Cli) -> Result<()> {
match &cli.command {
Commands::List => handle_list(&cli),
Commands::Identify
| Commands::Diag { .. }
| Commands::Profile { .. }
| Commands::Mode { .. }
| Commands::Boot { .. }
| Commands::Fw { .. } => {
let target = resolve_target(&cli)?;
let transport: Box<dyn Transport> = if cli.mock {
Box::new(mock_transport_for(&cli.command, target)?)
} else {
Box::new(HidTransport::new())
};
let config = SessionConfig {
retry_policy: RetryPolicy {
max_attempts: cli.max_attempts,
backoff_ms: cli.backoff_ms,
},
timeout_profile: TimeoutProfile {
probe_ms: cli.probe_timeout_ms,
io_ms: cli.io_timeout_ms,
firmware_ms: cli.firmware_timeout_ms,
},
allow_unsafe: cli.allow_unsafe,
brick_risk_ack: cli.brick_risk_ack,
experimental: cli.experimental,
trace_enabled: true,
};
let mut session = DeviceSession::new(transport, target, config)?;
match &cli.command {
Commands::Identify => {
let info = session.identify()?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&info)?);
} else {
println!(
"target={} profile={} support={:?} family={:?} evidence={:?} capability={:?} detected_pid={}",
info.target,
info.profile_name,
info.support_level,
info.protocol_family,
info.evidence,
info.capability,
info.detected_pid
.map(|v| format!("{v:#06x}"))
.unwrap_or_else(|| "none".to_owned())
);
}
}
Commands::Diag { command } => match command {
DiagCommand::Probe => {
let diag = session.diag_probe();
if cli.json {
println!("{}", serde_json::to_string_pretty(&diag)?);
} else {
println!(
"diag target={} profile={} family={:?}",
diag.target, diag.profile_name, diag.protocol_family
);
for check in diag.command_checks {
println!(
" {:?}: ok={} code={}",
check.command,
check.ok,
check
.error_code
.map(|c| format!("{c:?}"))
.unwrap_or_else(|| "none".to_owned())
);
}
}
}
},
Commands::Mode { command } => match command {
ModeCommand::Get => {
let mode = session.get_mode()?;
print_mode(mode.mode, &mode.source, cli.json);
}
ModeCommand::Set { mode } => {
let mode_state = session.set_mode(*mode)?;
print_mode(mode_state.mode, &mode_state.source, cli.json);
}
},
Commands::Profile { command } => match command {
ProfileCommand::Dump { slot } => {
let profile = session.read_profile(*slot)?;
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"slot": profile.slot,
"payload_hex": hex::encode(&profile.payload),
}))?
);
} else {
println!(
"slot={} payload_hex={}",
profile.slot,
hex::encode(&profile.payload)
);
}
}
ProfileCommand::Apply { slot, file } => {
let bytes = fs::read(file)?;
let parsed = ProfileBlob::from_bytes(&bytes)?;
let blob = ProfileBlob {
slot: *slot,
payload: parsed.payload,
};
session.write_profile(*slot, &blob)?;
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"applied": true,
"slot": slot,
}))?
);
} else {
println!("applied profile to slot={slot}");
}
}
},
Commands::Boot { command } => {
match command {
BootCommand::Enter => session.enter_bootloader()?,
BootCommand::Exit => session.exit_bootloader()?,
}
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"ok": true,
"command": format!("{:?}", command),
}))?
);
} else {
println!("{:?} completed", command);
}
}
Commands::Fw { command } => match command {
FwCommand::Write {
file,
chunk_size,
dry_run,
} => {
let image = fs::read(file)?;
let report = session.firmware_transfer(&image, *chunk_size, *dry_run)?;
print_fw_report(report, cli.json)?;
}
},
Commands::List => unreachable!(),
}
session.close()?;
Ok(())
}
}
}
fn handle_list(cli: &Cli) -> Result<()> {
if cli.mock {
let profile = device_profile_for(VidPid::new(0x2dc8, 0x6009));
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&vec![json!({
"vid": "0x2dc8",
"pid": "0x6009",
"product": "Mock 8BitDo Device",
"support_level": format!("{:?}", profile.support_level),
"protocol_family": format!("{:?}", profile.protocol_family),
"capability": profile.capability,
"evidence": format!("{:?}", profile.evidence),
})])?
);
} else {
println!("2dc8:6009 Mock 8BitDo Device");
}
return Ok(());
}
let devices = enumerate_hid_devices()?;
let filtered: Vec<_> = devices
.into_iter()
.filter(|d| d.vid_pid.vid == 0x2dc8)
.collect();
if cli.json {
let out: Vec<_> = filtered
.iter()
.map(|d| {
let profile = device_profile_for(d.vid_pid);
json!({
"vid": format!("{:#06x}", d.vid_pid.vid),
"pid": format!("{:#06x}", d.vid_pid.pid),
"product": d.product,
"manufacturer": d.manufacturer,
"serial": d.serial,
"path": d.path,
"support_level": format!("{:?}", profile.support_level),
"protocol_family": format!("{:?}", profile.protocol_family),
"capability": profile.capability,
"evidence": format!("{:?}", profile.evidence),
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&out)?);
} else {
for d in &filtered {
println!(
"{} {}",
d.vid_pid,
d.product.as_deref().unwrap_or("(unknown product)")
);
}
}
Ok(())
}
fn resolve_target(cli: &Cli) -> Result<VidPid> {
let vid = cli
.vid
.as_deref()
.map(parse_u16)
.transpose()?
.unwrap_or(0x2dc8);
let pid_str = cli
.pid
.as_deref()
.ok_or_else(|| anyhow!("--pid is required for this command"))?;
let pid = parse_u16(pid_str)?;
Ok(VidPid::new(vid, pid))
}
fn parse_u16(input: &str) -> Result<u16> {
if let Some(hex) = input
.strip_prefix("0x")
.or_else(|| input.strip_prefix("0X"))
{
return Ok(u16::from_str_radix(hex, 16)?);
}
Ok(input.parse::<u16>()?)
}
fn mock_transport_for(command: &Commands, target: VidPid) -> Result<MockTransport> {
let mut t = MockTransport::default();
match command {
Commands::Identify => {
t.push_read_data(build_pid_response(target.pid));
}
Commands::Diag { command } => match command {
DiagCommand::Probe => {
t.push_read_data(build_pid_response(target.pid));
t.push_read_data(build_rr_response());
t.push_read_data(build_mode_response(2));
t.push_read_data(build_version_response());
}
},
Commands::Mode { command } => match command {
ModeCommand::Get => t.push_read_data(build_mode_response(2)),
ModeCommand::Set { mode } => {
t.push_read_data(build_ack_response());
t.push_read_data(build_mode_response(*mode));
}
},
Commands::Profile { command } => match command {
ProfileCommand::Dump { slot } => {
let mut raw = vec![0x02, 0x06, 0x00, *slot];
raw.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
t.push_read_data(raw);
}
ProfileCommand::Apply { .. } => {
t.push_read_data(build_ack_response());
}
},
Commands::Boot { .. } => {}
Commands::Fw { command } => {
let chunks = match command {
FwCommand::Write {
file,
chunk_size,
dry_run,
} => {
if *dry_run {
0
} else {
let sz = fs::metadata(file).map(|m| m.len() as usize).unwrap_or(0);
sz.div_ceil(*chunk_size) + 1
}
}
};
for _ in 0..chunks {
t.push_read_data(build_ack_response());
}
}
Commands::List => {}
}
if matches!(command, Commands::Profile { .. } | Commands::Fw { .. })
&& !command_registry()
.iter()
.any(|c| c.id == CommandId::ReadProfile)
{
return Err(anyhow!("command registry is empty"));
}
Ok(t)
}
fn build_ack_response() -> Vec<u8> {
vec![0x02, 0x01, 0x00, 0x00]
}
fn build_mode_response(mode: u8) -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x05;
out[5] = mode;
out
}
fn build_rr_response() -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x04;
out[5] = 0x01;
out
}
fn build_version_response() -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x22;
out[2] = 0x2A;
out[3] = 0x00;
out[4] = 0x01;
out
}
fn build_pid_response(pid: u16) -> Vec<u8> {
let mut out = vec![0u8; 64];
out[0] = 0x02;
out[1] = 0x05;
out[4] = 0xC1;
let [lo, hi] = pid.to_le_bytes();
out[22] = lo;
out[23] = hi;
out
}
fn print_mode(mode: u8, source: &str, as_json: bool) {
if as_json {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"mode": mode,
"source": source,
}))
.expect("json serialization")
);
} else {
println!("mode={} source={}", mode, source);
}
}
fn print_fw_report(report: FirmwareTransferReport, as_json: bool) -> Result<()> {
if as_json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
println!(
"bytes_total={} chunk_size={} chunks_sent={} dry_run={}",
report.bytes_total, report.chunk_size, report.chunks_sent, report.dry_run
);
}
Ok(())
}
#[allow(dead_code)]
fn print_error_code(code: BitdoErrorCode, as_json: bool) {
if as_json {
println!(
"{}",
serde_json::to_string_pretty(&json!({ "error_code": format!("{:?}", code) }))
.expect("json serialization")
);
} else {
println!("error_code={:?}", code);
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "openbitdo"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
build = "build.rs"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
bitdo_app_core = { path = "../bitdo_app_core" }
bitdo_tui = { path = "../bitdo_tui" }
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.1"

View File

@@ -0,0 +1,42 @@
use std::env;
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
emit("OPENBITDO_APP_VERSION", env::var("CARGO_PKG_VERSION").ok());
emit("OPENBITDO_TARGET_TRIPLE", env::var("TARGET").ok());
emit(
"OPENBITDO_GIT_COMMIT_FULL",
run_cmd("git", &["rev-parse", "HEAD"]),
);
emit(
"OPENBITDO_GIT_COMMIT_SHORT",
run_cmd("git", &["rev-parse", "--short=12", "HEAD"]),
);
emit(
"OPENBITDO_BUILD_DATE_UTC",
run_cmd("date", &["-u", "+%Y-%m-%dT%H:%M:%SZ"]),
);
}
fn emit(key: &str, value: Option<String>) {
let normalized = value
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
.unwrap_or("unknown");
println!("cargo:rustc-env={key}={normalized}");
}
fn run_cmd(program: &str, args: &[&str]) -> Option<String> {
let output = Command::new(program).args(args).output().ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|v| v.trim().to_owned())
.filter(|v| !v.is_empty())
}

View File

@@ -0,0 +1,218 @@
use bitdo_app_core::{signing_key_fingerprint_active_sha256, signing_key_fingerprint_next_sha256};
use bitdo_tui::ReportSaveMode;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BuildInfo {
pub app_version: String,
pub git_commit_short: String,
pub git_commit_full: String,
pub build_date_utc: String,
pub target_triple: String,
pub runtime_platform: String,
pub signing_key_fingerprint_short: String,
pub signing_key_fingerprint_full: String,
pub signing_key_next_fingerprint_short: String,
}
impl BuildInfo {
pub fn current() -> Self {
Self::from_raw(
option_env!("OPENBITDO_APP_VERSION"),
option_env!("OPENBITDO_GIT_COMMIT_SHORT"),
option_env!("OPENBITDO_GIT_COMMIT_FULL"),
option_env!("OPENBITDO_BUILD_DATE_UTC"),
option_env!("OPENBITDO_TARGET_TRIPLE"),
)
}
pub fn to_tui_info(&self) -> bitdo_tui::BuildInfo {
bitdo_tui::BuildInfo {
app_version: self.app_version.clone(),
git_commit_short: self.git_commit_short.clone(),
git_commit_full: self.git_commit_full.clone(),
build_date_utc: self.build_date_utc.clone(),
target_triple: self.target_triple.clone(),
runtime_platform: self.runtime_platform.clone(),
signing_key_fingerprint_short: self.signing_key_fingerprint_short.clone(),
signing_key_fingerprint_full: self.signing_key_fingerprint_full.clone(),
signing_key_next_fingerprint_short: self.signing_key_next_fingerprint_short.clone(),
}
}
fn from_raw(
app_version: Option<&'static str>,
git_commit_short: Option<&'static str>,
git_commit_full: Option<&'static str>,
build_date_utc: Option<&'static str>,
target_triple: Option<&'static str>,
) -> Self {
Self {
app_version: normalize(app_version),
git_commit_short: normalize(git_commit_short),
git_commit_full: normalize(git_commit_full),
build_date_utc: normalize(build_date_utc),
target_triple: normalize(target_triple),
runtime_platform: format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH),
signing_key_fingerprint_short: short_fingerprint(
&signing_key_fingerprint_active_sha256(),
),
signing_key_fingerprint_full: signing_key_fingerprint_active_sha256(),
signing_key_next_fingerprint_short: short_fingerprint(
&signing_key_fingerprint_next_sha256(),
),
}
}
}
fn normalize(value: Option<&str>) -> String {
value
.map(str::trim)
.filter(|v| !v.is_empty())
.unwrap_or("unknown")
.to_owned()
}
fn short_fingerprint(full: &str) -> String {
if full == "unknown" {
return "unknown".to_owned();
}
full.chars().take(16).collect()
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct UserSettings {
#[serde(default = "default_settings_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub advanced_mode: bool,
#[serde(default)]
pub report_save_mode: ReportSaveMode,
}
impl Default for UserSettings {
fn default() -> Self {
Self {
schema_version: default_settings_schema_version(),
advanced_mode: false,
report_save_mode: ReportSaveMode::FailureOnly,
}
}
}
const fn default_settings_schema_version() -> u32 {
1
}
pub fn user_settings_path() -> PathBuf {
if cfg!(target_os = "macos") {
return home_directory()
.join("Library")
.join("Application Support")
.join("OpenBitdo")
.join("config.toml");
}
if cfg!(target_os = "linux") {
if let Some(xdg_config_home) = std::env::var_os("XDG_CONFIG_HOME") {
return PathBuf::from(xdg_config_home)
.join("openbitdo")
.join("config.toml");
}
return home_directory()
.join(".config")
.join("openbitdo")
.join("config.toml");
}
std::env::temp_dir().join("openbitdo").join("config.toml")
}
pub fn load_user_settings(path: &Path) -> UserSettings {
let Ok(raw) = std::fs::read_to_string(path) else {
return UserSettings::default();
};
let mut settings: UserSettings = toml::from_str(&raw).unwrap_or_default();
if !settings.advanced_mode && settings.report_save_mode == ReportSaveMode::Off {
settings.report_save_mode = ReportSaveMode::FailureOnly;
}
settings
}
pub fn save_user_settings(path: &Path, settings: &UserSettings) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = toml::to_string_pretty(settings)?;
std::fs::write(path, body)?;
Ok(())
}
fn home_directory() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn build_info_falls_back_to_unknown_when_missing() {
let info = BuildInfo::from_raw(None, None, None, None, None);
assert_eq!(info.app_version, "unknown");
assert_eq!(info.git_commit_short, "unknown");
assert_eq!(info.git_commit_full, "unknown");
assert_eq!(info.build_date_utc, "unknown");
assert_eq!(info.target_triple, "unknown");
}
#[test]
fn runtime_platform_has_expected_separator() {
let info = BuildInfo::from_raw(None, None, None, None, None);
assert!(info.runtime_platform.contains('/'));
}
#[test]
fn normalize_trims_and_preserves_values() {
let info = BuildInfo::from_raw(
Some(" 0.1.0 "),
Some(" abc123 "),
Some(" abc123def456 "),
Some(" 2026-01-01T00:00:00Z "),
Some(" x86_64-unknown-linux-gnu "),
);
assert_eq!(info.app_version, "0.1.0");
assert_eq!(info.git_commit_short, "abc123");
assert_eq!(info.git_commit_full, "abc123def456");
assert_eq!(info.build_date_utc, "2026-01-01T00:00:00Z");
assert_eq!(info.target_triple, "x86_64-unknown-linux-gnu");
}
#[test]
fn settings_roundtrip_toml() {
let tmp =
std::env::temp_dir().join(format!("openbitdo-settings-{}.toml", std::process::id()));
let settings = UserSettings {
schema_version: 1,
advanced_mode: true,
report_save_mode: ReportSaveMode::Always,
};
save_user_settings(&tmp, &settings).expect("save settings");
let loaded = load_user_settings(&tmp);
assert_eq!(loaded, settings);
let _ = std::fs::remove_file(tmp);
}
#[test]
fn missing_settings_uses_defaults() {
let path = PathBuf::from("/tmp/openbitdo-nonexistent-settings.toml");
let loaded = load_user_settings(&path);
assert!(!loaded.advanced_mode);
assert_eq!(loaded.report_save_mode, ReportSaveMode::FailureOnly);
}
}

View File

@@ -0,0 +1,58 @@
use anyhow::Result;
use bitdo_app_core::{OpenBitdoCore, OpenBitdoCoreConfig};
use bitdo_tui::{run_tui_app, TuiLaunchOptions};
use clap::Parser;
use openbitdo::{load_user_settings, user_settings_path, BuildInfo};
#[derive(Debug, Parser)]
#[command(name = "openbitdo")]
#[command(about = "OpenBitdo beginner-first launcher")]
struct Cli {
#[arg(long, help = "Use mock transport/devices")]
mock: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
let settings_path = user_settings_path();
let settings = load_user_settings(&settings_path);
let core = OpenBitdoCore::new(OpenBitdoCoreConfig {
mock_mode: cli.mock,
advanced_mode: settings.advanced_mode,
progress_interval_ms: 5,
..Default::default()
});
run_tui_app(
core,
TuiLaunchOptions {
build_info: BuildInfo::current().to_tui_info(),
advanced_mode: settings.advanced_mode,
report_save_mode: settings.report_save_mode,
settings_path: Some(settings_path),
..Default::default()
},
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::error::ErrorKind;
#[test]
fn cli_supports_mock_only() {
let cli = Cli::parse_from(["openbitdo", "--mock"]);
assert!(cli.mock);
}
#[test]
fn cli_rejects_cmd_subcommand() {
let err = Cli::try_parse_from(["openbitdo", "cmd"]).expect_err("must reject cmd");
assert_eq!(err.kind(), ErrorKind::UnknownArgument);
}
}

View File

@@ -0,0 +1,13 @@
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
#[test]
fn help_mentions_beginner_flow() {
let mut cmd = cargo_bin_cmd!("openbitdo");
cmd.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("beginner-first"))
.stdout(predicate::str::contains("--mock"))
.stdout(predicate::str::contains("cmd").not());
}