mirror of
https://github.com/bybrooklyn/openbitdo.git
synced 2026-03-19 04:12:56 -04:00
release prep: rc.1 baseline and gating updates
This commit is contained in:
2383
sdk/Cargo.lock
generated
2383
sdk/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,33 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/bitdo_proto",
|
||||
"crates/bitdoctl",
|
||||
"crates/bitdo_app_core",
|
||||
"crates/bitdo_tui",
|
||||
"crates/openbitdo",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
license = "MIT"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
thiserror = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
base64 = "0.22"
|
||||
ed25519-dalek = { version = "2.1", default-features = false, features = ["std"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
hex = "0.4"
|
||||
tokio = { version = "1.48", features = ["rt-multi-thread", "macros", "sync", "time", "fs", "net", "io-util"] }
|
||||
uuid = { version = "1.18", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["clock", "serde"] }
|
||||
sha2 = "0.10"
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.29"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||
|
||||
102
sdk/README.md
102
sdk/README.md
@@ -1,6 +1,10 @@
|
||||
# OpenBitdo SDK
|
||||
|
||||
`bitdo_proto` and `bitdoctl` provide the clean-room protocol core and CLI.
|
||||
OpenBitdo SDK includes:
|
||||
- `bitdo_proto`: protocol/transport/session library
|
||||
- `bitdo_app_core`: shared firmware-first workflow and policy layer
|
||||
- `bitdo_tui`: Ratatui/Crossterm terminal app
|
||||
- `openbitdo`: beginner-first launcher (`openbitdo` starts guided TUI)
|
||||
|
||||
## Build
|
||||
```bash
|
||||
@@ -22,9 +26,97 @@ cargo test --workspace --all-targets
|
||||
./scripts/run_hardware_smoke.sh
|
||||
```
|
||||
|
||||
## CLI examples
|
||||
## TUI app examples (`openbitdo`)
|
||||
```bash
|
||||
cargo run -p bitdoctl -- --mock list
|
||||
cargo run -p bitdoctl -- --mock --json --pid 24585 identify
|
||||
cargo run -p bitdoctl -- --mock --json --pid 24585 diag probe
|
||||
cargo run -p openbitdo -- --mock
|
||||
```
|
||||
|
||||
## Beginner-first behavior
|
||||
- launch with no subcommands
|
||||
- if no device is connected, OpenBitdo starts in a waiting screen with `Refresh`, `Help`, and `Quit`
|
||||
- if one device is connected, it is auto-selected and ready for action
|
||||
- choose `Recommended Update` or `Diagnose` from large clickable actions
|
||||
- for JP108 devices (`0x5209`/`0x520a`), `Recommended Update` enters a dedicated-button wizard:
|
||||
- edit `A/B/K1-K8`
|
||||
- backup + apply
|
||||
- guided button test
|
||||
- for Ultimate2 devices (`0x6012`/`0x6013`), `Recommended Update` enters a core-profile wizard:
|
||||
- choose slot (`Slot1/2/3`)
|
||||
- set mode
|
||||
- edit RC mapping slots (`A/B/K1-K8`) with known controller-button targets
|
||||
- view/edit L2/R2 analog values when capability supports writes
|
||||
- backup + apply
|
||||
- guided button test
|
||||
- firmware path defaults to verified recommended download; local file fallback is prompted if unavailable
|
||||
- update transfer requires one plain-language `I Understand` confirmation
|
||||
- detect-only PIDs stay read/diagnostic-only with a clear block reason
|
||||
- mouse support:
|
||||
- left click for primary actions
|
||||
- right click on device rows for context menu actions
|
||||
- scroll wheel to navigate device list/detail panes
|
||||
- support reports are TOML only
|
||||
- beginner mode: `failure_only` (default) or `always`
|
||||
- advanced mode: `failure_only`, `always`, or `off` (with warning)
|
||||
- advanced mode is toggled from About (`t` or click) and persisted to OS config TOML
|
||||
- advanced report hotkeys after a failure report exists:
|
||||
- `c` copy report path
|
||||
- `o` open report file
|
||||
- `f` open report folder
|
||||
- open About from home (`a` key or click `About`) to view:
|
||||
- app version
|
||||
- git commit short and full hash
|
||||
- build date (UTC)
|
||||
- compile target triple
|
||||
- runtime OS/arch
|
||||
- firmware signing-key fingerprint (short with full-view toggle, plus next-key short)
|
||||
|
||||
## Packaging
|
||||
```bash
|
||||
./scripts/package-linux.sh v0.0.1-rc.1 x86_64
|
||||
./scripts/package-linux.sh v0.0.1-rc.1 aarch64
|
||||
./scripts/package-macos.sh v0.0.1-rc.1 arm64 aarch64-apple-darwin
|
||||
```
|
||||
|
||||
Packaging outputs use:
|
||||
- `openbitdo-<version>-<os>-<arch>.tar.gz`
|
||||
- `openbitdo-<version>-<os>-<arch>` standalone binary
|
||||
- `.sha256` checksum file for each artifact
|
||||
- macOS arm64 additionally emits `.pkg` (unsigned/ad-hoc for RC)
|
||||
|
||||
## Release Workflow
|
||||
- CI checks remain in `.github/workflows/ci.yml`.
|
||||
- Tag-based release workflow is in `.github/workflows/release.yml`.
|
||||
- Release tags must originate from `main`.
|
||||
- `v0.0.1-rc.1` style tags publish GitHub pre-releases.
|
||||
- Release notes are sourced from `/Users/brooklyn/data/8bitdo/cleanroom/CHANGELOG.md`.
|
||||
- Package-manager publish runs only after release assets are published.
|
||||
|
||||
## Public RC Gate
|
||||
- No open GitHub issues with label `release-blocker`.
|
||||
- Scope-completeness gate:
|
||||
- JP108 RC scope is dedicated mapping only (`A/B/K1-K8`).
|
||||
- Ultimate2 RC scope is expanded mapping for required fields only.
|
||||
- Clean-tree requirement from `/Users/brooklyn/data/8bitdo/cleanroom/RC_CHECKLIST.md` must be satisfied before tagging.
|
||||
|
||||
## Distribution Prep
|
||||
- Homebrew formula scaffold: `/Users/brooklyn/data/8bitdo/cleanroom/packaging/homebrew/Formula/openbitdo.rb`
|
||||
- Homebrew tap sync script (disabled by default): `/Users/brooklyn/data/8bitdo/cleanroom/packaging/homebrew/sync_tap.sh`
|
||||
- Planned tap repository: `bybrooklyn/homebrew-openbitdo`
|
||||
- AUR package sources:
|
||||
- `/Users/brooklyn/data/8bitdo/cleanroom/packaging/aur/openbitdo`
|
||||
- `/Users/brooklyn/data/8bitdo/cleanroom/packaging/aur/openbitdo-bin`
|
||||
- Release metadata renderer:
|
||||
- `/Users/brooklyn/data/8bitdo/cleanroom/packaging/scripts/render_release_metadata.sh`
|
||||
- AUR publish workflow:
|
||||
- `/Users/brooklyn/data/8bitdo/cleanroom/.github/workflows/aur-publish.yml`
|
||||
- gated by `AUR_PUBLISH_ENABLED=1`
|
||||
- Homebrew publish path:
|
||||
- `release.yml` renders checksum-pinned formula and runs `sync_tap.sh`
|
||||
- gated by `HOMEBREW_PUBLISH_ENABLED=1`
|
||||
|
||||
## Hardware CI gates
|
||||
- required:
|
||||
- `hardware-ultimate2`
|
||||
- `hardware-108jp`
|
||||
- gated:
|
||||
- `hardware-jphandshake` (enabled only when `BITDO_ENABLE_JP_HARDWARE=1`)
|
||||
|
||||
24
sdk/crates/bitdo_app_core/Cargo.toml
Normal file
24
sdk/crates/bitdo_app_core/Cargo.toml
Normal 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"] }
|
||||
2060
sdk/crates/bitdo_app_core/src/lib.rs
Normal file
2060
sdk/crates/bitdo_app_core/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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] {
|
||||
|
||||
82
sdk/crates/bitdo_proto/src/command_registry_table.rs
Normal file
82
sdk/crates/bitdo_proto/src/command_registry_table.rs
Normal 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" },
|
||||
]
|
||||
;
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(feature = "hidapi-backend")]
|
||||
|
||||
use crate::error::{BitdoError, Result};
|
||||
use crate::transport::Transport;
|
||||
use crate::types::VidPid;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
68
sdk/crates/bitdo_proto/src/pid_registry_table.rs
Normal file
68
sdk/crates/bitdo_proto/src/pid_registry_table.rs
Normal 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 },
|
||||
]
|
||||
;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
19
sdk/crates/bitdo_tui/Cargo.toml
Normal file
19
sdk/crates/bitdo_tui/Cargo.toml
Normal 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"] }
|
||||
69
sdk/crates/bitdo_tui/src/desktop_io.rs
Normal file
69
sdk/crates/bitdo_tui/src/desktop_io.rs
Normal 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}"))
|
||||
}
|
||||
}
|
||||
3151
sdk/crates/bitdo_tui/src/lib.rs
Normal file
3151
sdk/crates/bitdo_tui/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
38
sdk/crates/bitdo_tui/src/settings.rs
Normal file
38
sdk/crates/bitdo_tui/src/settings.rs
Normal 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(())
|
||||
}
|
||||
217
sdk/crates/bitdo_tui/src/support_report.rs
Normal file
217
sdk/crates/bitdo_tui/src/support_report.rs
Normal 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)
|
||||
}
|
||||
370
sdk/crates/bitdo_tui/src/tests.rs
Normal file
370
sdk/crates/bitdo_tui/src/tests.rs
Normal 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());
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
21
sdk/crates/openbitdo/Cargo.toml
Normal file
21
sdk/crates/openbitdo/Cargo.toml
Normal 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"
|
||||
42
sdk/crates/openbitdo/build.rs
Normal file
42
sdk/crates/openbitdo/build.rs
Normal 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())
|
||||
}
|
||||
218
sdk/crates/openbitdo/src/lib.rs
Normal file
218
sdk/crates/openbitdo/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
58
sdk/crates/openbitdo/src/main.rs
Normal file
58
sdk/crates/openbitdo/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
sdk/crates/openbitdo/tests/cli_smoke.rs
Normal file
13
sdk/crates/openbitdo/tests/cli_smoke.rs
Normal 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());
|
||||
}
|
||||
@@ -5,9 +5,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
forbidden_pattern='decompiled(_dll|_autoupdate)?/|bundle_extract/|extracted(_net)?/|session-ses_35e4|8BitDo_Ultimate_Software_V2\.decompiled\.cs'
|
||||
scan_paths=(crates tests .github)
|
||||
scan_paths=(crates tests scripts ../.github)
|
||||
|
||||
if rg -n --hidden -g '!target/**' "$forbidden_pattern" "${scan_paths[@]}"; then
|
||||
if rg -n --hidden -g '!target/**' -g '!scripts/cleanroom_guard.sh' "$forbidden_pattern" "${scan_paths[@]}"; then
|
||||
echo "cleanroom guard failed: forbidden dirty-room reference detected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
68
sdk/scripts/package-linux.sh
Executable file
68
sdk/scripts/package-linux.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REPO_ROOT="$(cd "$ROOT/.." && pwd)"
|
||||
VERSION="${1:-v0.0.1-rc.1}"
|
||||
ARCH_LABEL="${2:-$(uname -m)}"
|
||||
TARGET_TRIPLE="${3:-}"
|
||||
|
||||
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||
echo "package-linux.sh must run on Linux" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$ARCH_LABEL" in
|
||||
x86_64|aarch64) ;;
|
||||
arm64) ARCH_LABEL="aarch64" ;;
|
||||
*)
|
||||
echo "unsupported linux arch label: $ARCH_LABEL" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
DIST_ROOT="$ROOT/dist"
|
||||
PKG_NAME="openbitdo-${VERSION}-linux-${ARCH_LABEL}"
|
||||
PKG_DIR="$DIST_ROOT/$PKG_NAME"
|
||||
BIN_ASSET="$DIST_ROOT/${PKG_NAME}"
|
||||
|
||||
checksum_file() {
|
||||
local path="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$path" > "${path}.sha256"
|
||||
elif command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$path" > "${path}.sha256"
|
||||
else
|
||||
echo "warning: no checksum tool found for $path" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
build_binary() {
|
||||
cd "$ROOT"
|
||||
if [[ -n "$TARGET_TRIPLE" ]]; then
|
||||
cargo build --release -p openbitdo --target "$TARGET_TRIPLE"
|
||||
echo "$ROOT/target/$TARGET_TRIPLE/release/openbitdo"
|
||||
else
|
||||
cargo build --release -p openbitdo
|
||||
echo "$ROOT/target/release/openbitdo"
|
||||
fi
|
||||
}
|
||||
|
||||
BIN_PATH="$(build_binary)"
|
||||
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR/bin" "$DIST_ROOT"
|
||||
|
||||
cp "$BIN_PATH" "$PKG_DIR/bin/openbitdo"
|
||||
cp "$BIN_PATH" "$BIN_ASSET"
|
||||
cp "$REPO_ROOT/README.md" "$PKG_DIR/README.md"
|
||||
cp "$ROOT/README.md" "$PKG_DIR/SDK_README.md"
|
||||
cp "$REPO_ROOT/LICENSE" "$PKG_DIR/LICENSE"
|
||||
|
||||
tar -C "$DIST_ROOT" -czf "$DIST_ROOT/${PKG_NAME}.tar.gz" "$PKG_NAME"
|
||||
|
||||
checksum_file "$DIST_ROOT/${PKG_NAME}.tar.gz"
|
||||
checksum_file "$BIN_ASSET"
|
||||
|
||||
echo "created package: $DIST_ROOT/${PKG_NAME}.tar.gz"
|
||||
echo "created standalone binary: $BIN_ASSET"
|
||||
77
sdk/scripts/package-macos.sh
Executable file
77
sdk/scripts/package-macos.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REPO_ROOT="$(cd "$ROOT/.." && pwd)"
|
||||
VERSION="${1:-v0.0.1-rc.1}"
|
||||
ARCH_LABEL="${2:-arm64}"
|
||||
TARGET_TRIPLE="${3:-aarch64-apple-darwin}"
|
||||
INSTALL_PREFIX="${4:-/opt/homebrew/bin}"
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "package-macos.sh must run on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ARCH_LABEL" != "arm64" ]]; then
|
||||
echo "unsupported macOS arch label: $ARCH_LABEL (expected arm64)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIST_ROOT="$ROOT/dist"
|
||||
PKG_NAME="openbitdo-${VERSION}-macos-${ARCH_LABEL}"
|
||||
PKG_DIR="$DIST_ROOT/$PKG_NAME"
|
||||
BIN_ASSET="$DIST_ROOT/${PKG_NAME}"
|
||||
PKG_ASSET="$DIST_ROOT/${PKG_NAME}.pkg"
|
||||
PKGROOT="$DIST_ROOT/${PKG_NAME}-pkgroot"
|
||||
|
||||
checksum_file() {
|
||||
local path="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$path" > "${path}.sha256"
|
||||
elif command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$path" > "${path}.sha256"
|
||||
else
|
||||
echo "warning: no checksum tool found for $path" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
build_binary() {
|
||||
cd "$ROOT"
|
||||
cargo build --release -p openbitdo --target "$TARGET_TRIPLE"
|
||||
echo "$ROOT/target/$TARGET_TRIPLE/release/openbitdo"
|
||||
}
|
||||
|
||||
BIN_PATH="$(build_binary)"
|
||||
VERSION_STRIPPED="${VERSION#v}"
|
||||
|
||||
rm -rf "$PKG_DIR" "$PKGROOT" "$PKG_ASSET"
|
||||
mkdir -p "$PKG_DIR/bin" "$DIST_ROOT"
|
||||
|
||||
cp "$BIN_PATH" "$PKG_DIR/bin/openbitdo"
|
||||
cp "$BIN_PATH" "$BIN_ASSET"
|
||||
cp "$REPO_ROOT/README.md" "$PKG_DIR/README.md"
|
||||
cp "$ROOT/README.md" "$PKG_DIR/SDK_README.md"
|
||||
cp "$REPO_ROOT/LICENSE" "$PKG_DIR/LICENSE"
|
||||
|
||||
tar -C "$DIST_ROOT" -czf "$DIST_ROOT/${PKG_NAME}.tar.gz" "$PKG_NAME"
|
||||
|
||||
mkdir -p "$PKGROOT${INSTALL_PREFIX}"
|
||||
cp "$BIN_PATH" "$PKGROOT${INSTALL_PREFIX}/openbitdo"
|
||||
chmod 755 "$PKGROOT${INSTALL_PREFIX}/openbitdo"
|
||||
|
||||
pkgbuild \
|
||||
--root "$PKGROOT" \
|
||||
--identifier "io.openbitdo.cli" \
|
||||
--version "$VERSION_STRIPPED" \
|
||||
"$PKG_ASSET"
|
||||
|
||||
rm -rf "$PKGROOT"
|
||||
|
||||
checksum_file "$DIST_ROOT/${PKG_NAME}.tar.gz"
|
||||
checksum_file "$BIN_ASSET"
|
||||
checksum_file "$PKG_ASSET"
|
||||
|
||||
echo "created package: $DIST_ROOT/${PKG_NAME}.tar.gz"
|
||||
echo "created standalone binary: $BIN_ASSET"
|
||||
echo "created installer pkg: $PKG_ASSET"
|
||||
@@ -3,37 +3,234 @@ set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
LAB_CONFIG="${ROOT}/../harness/lab/device_lab.yaml"
|
||||
|
||||
if [[ ! -f "$LAB_CONFIG" ]]; then
|
||||
echo "missing lab config: $LAB_CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPORT_DIR="${ROOT}/../harness/reports"
|
||||
mkdir -p "$REPORT_DIR"
|
||||
TS="$(date +%Y%m%d-%H%M%S)"
|
||||
REPORT_PATH="${1:-$REPORT_DIR/hardware_smoke_${TS}.json}"
|
||||
|
||||
LIST_JSON="$(cargo run -q -p bitdoctl -- --json list 2>/dev/null || echo '[]')"
|
||||
SUITE="${BITDO_REQUIRED_SUITE:-family}"
|
||||
REQUIRED_FAMILIES="${BITDO_REQUIRED_FAMILIES:-Standard64,DInput}"
|
||||
|
||||
TEST_OUTPUT_FILE="$(mktemp)"
|
||||
PARSE_OUTPUT="$(mktemp)"
|
||||
set +e
|
||||
BITDO_HARDWARE=1 cargo test --workspace --test hardware_smoke -- --ignored >"$TEST_OUTPUT_FILE" 2>&1
|
||||
TEST_STATUS=$?
|
||||
python3 - <<'PY' "$LAB_CONFIG" "$SUITE" "$REQUIRED_FAMILIES" >"$PARSE_OUTPUT"
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
config_path = pathlib.Path(sys.argv[1])
|
||||
suite = sys.argv[2].strip()
|
||||
required_families = [item.strip() for item in sys.argv[3].split(",") if item.strip()]
|
||||
lines = config_path.read_text().splitlines()
|
||||
|
||||
devices = []
|
||||
current = None
|
||||
in_devices = False
|
||||
|
||||
def parse_scalar(text: str):
|
||||
value = text.split("#", 1)[0].strip()
|
||||
if not value:
|
||||
return value
|
||||
if value.startswith(("0x", "0X")):
|
||||
return int(value, 16)
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("devices:"):
|
||||
in_devices = True
|
||||
continue
|
||||
if not in_devices:
|
||||
continue
|
||||
if stripped.startswith("policies:"):
|
||||
if current:
|
||||
devices.append(current)
|
||||
current = None
|
||||
break
|
||||
|
||||
if re.match(r"^\s*-\s+", line):
|
||||
if current:
|
||||
devices.append(current)
|
||||
current = {}
|
||||
continue
|
||||
|
||||
if current is None:
|
||||
continue
|
||||
|
||||
field_match = re.match(r"^\s*([A-Za-z0-9_]+)\s*:\s*(.+)$", line)
|
||||
if not field_match:
|
||||
continue
|
||||
|
||||
key = field_match.group(1)
|
||||
value = parse_scalar(field_match.group(2))
|
||||
current[key] = value
|
||||
|
||||
if current:
|
||||
devices.append(current)
|
||||
|
||||
if not devices:
|
||||
sys.stderr.write(f"no devices found in {config_path}\n")
|
||||
sys.exit(1)
|
||||
|
||||
family_to_pid = {}
|
||||
fixture_to_pid = {}
|
||||
for device in devices:
|
||||
family = device.get("protocol_family")
|
||||
pid = device.get("pid")
|
||||
fixture_id = device.get("fixture_id")
|
||||
if isinstance(family, str) and isinstance(pid, int) and family not in family_to_pid:
|
||||
family_to_pid[family] = pid
|
||||
if isinstance(fixture_id, str) and isinstance(pid, int) and fixture_id not in fixture_to_pid:
|
||||
fixture_to_pid[fixture_id] = pid
|
||||
|
||||
if suite == "family":
|
||||
missing = [fam for fam in required_families if fam not in family_to_pid]
|
||||
if missing:
|
||||
available = ", ".join(sorted(family_to_pid.keys())) if family_to_pid else "none"
|
||||
sys.stderr.write(
|
||||
f"missing required family fixtures in {config_path}: {', '.join(missing)}; available: {available}\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
for fam in required_families:
|
||||
print(f"FAMILY:{fam}={family_to_pid[fam]:#06x}")
|
||||
elif suite == "ultimate2":
|
||||
if "ultimate2" not in fixture_to_pid:
|
||||
available = ", ".join(sorted(fixture_to_pid.keys())) if fixture_to_pid else "none"
|
||||
sys.stderr.write(
|
||||
f"missing fixture_id=ultimate2 in {config_path}; available fixture_ids: {available}\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
print(f"FIXTURE:ultimate2={fixture_to_pid['ultimate2']:#06x}")
|
||||
elif suite == "108jp":
|
||||
if "108jp" not in fixture_to_pid:
|
||||
available = ", ".join(sorted(fixture_to_pid.keys())) if fixture_to_pid else "none"
|
||||
sys.stderr.write(
|
||||
f"missing fixture_id=108jp in {config_path}; available fixture_ids: {available}\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
print(f"FIXTURE:108jp={fixture_to_pid['108jp']:#06x}")
|
||||
else:
|
||||
sys.stderr.write(f"unsupported BITDO_REQUIRED_SUITE value: {suite}\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
PARSE_STATUS=$?
|
||||
set -e
|
||||
|
||||
python3 - <<'PY' "$REPORT_PATH" "$TEST_STATUS" "$TEST_OUTPUT_FILE" "$LIST_JSON"
|
||||
if [[ $PARSE_STATUS -ne 0 ]]; then
|
||||
rm -f "$PARSE_OUTPUT"
|
||||
exit $PARSE_STATUS
|
||||
fi
|
||||
|
||||
while IFS='=' read -r key pid_hex; do
|
||||
[[ -z "$key" ]] && continue
|
||||
if [[ "$key" == FAMILY:* ]]; then
|
||||
family="${key#FAMILY:}"
|
||||
case "$family" in
|
||||
DInput) export BITDO_EXPECT_DINPUT_PID="$pid_hex" ;;
|
||||
Standard64) export BITDO_EXPECT_STANDARD64_PID="$pid_hex" ;;
|
||||
JpHandshake) export BITDO_EXPECT_JPHANDSHAKE_PID="$pid_hex" ;;
|
||||
*)
|
||||
echo "unsupported family in parsed lab config: $family" >&2
|
||||
rm -f "$PARSE_OUTPUT"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
elif [[ "$key" == FIXTURE:* ]]; then
|
||||
fixture="${key#FIXTURE:}"
|
||||
case "$fixture" in
|
||||
ultimate2) export BITDO_EXPECT_ULTIMATE2_PID="$pid_hex" ;;
|
||||
108jp) export BITDO_EXPECT_108JP_PID="$pid_hex" ;;
|
||||
*)
|
||||
echo "unsupported fixture in parsed lab config: $fixture" >&2
|
||||
rm -f "$PARSE_OUTPUT"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done <"$PARSE_OUTPUT"
|
||||
rm -f "$PARSE_OUTPUT"
|
||||
|
||||
TEST_OUTPUT_FILE="$(mktemp)"
|
||||
TEST_STATUS=0
|
||||
|
||||
run_test() {
|
||||
local test_name="$1"
|
||||
set +e
|
||||
BITDO_HARDWARE=1 cargo test --workspace --test hardware_smoke -- --ignored --exact "$test_name" >>"$TEST_OUTPUT_FILE" 2>&1
|
||||
local status=$?
|
||||
set -e
|
||||
if [[ $status -ne 0 ]]; then
|
||||
TEST_STATUS=$status
|
||||
fi
|
||||
}
|
||||
|
||||
run_test "hardware_smoke_detect_devices"
|
||||
|
||||
case "$SUITE" in
|
||||
family)
|
||||
IFS=',' read -r -a FAMILY_LIST <<<"$REQUIRED_FAMILIES"
|
||||
for family in "${FAMILY_LIST[@]}"; do
|
||||
case "$family" in
|
||||
DInput) run_test "hardware_smoke_dinput_family" ;;
|
||||
Standard64) run_test "hardware_smoke_standard64_family" ;;
|
||||
JpHandshake) run_test "hardware_smoke_jphandshake_family" ;;
|
||||
*)
|
||||
echo "unsupported required family for tests: $family" >>"$TEST_OUTPUT_FILE"
|
||||
TEST_STATUS=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
ultimate2)
|
||||
run_test "hardware_smoke_ultimate2_core_ops"
|
||||
;;
|
||||
108jp)
|
||||
run_test "hardware_smoke_108jp_dedicated_ops"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported suite: $SUITE" >>"$TEST_OUTPUT_FILE"
|
||||
TEST_STATUS=1
|
||||
;;
|
||||
esac
|
||||
|
||||
python3 - <<'PY' "$REPORT_PATH" "$TEST_STATUS" "$TEST_OUTPUT_FILE" "$SUITE" "$REQUIRED_FAMILIES" "${BITDO_EXPECT_STANDARD64_PID:-}" "${BITDO_EXPECT_DINPUT_PID:-}" "${BITDO_EXPECT_JPHANDSHAKE_PID:-}" "${BITDO_EXPECT_ULTIMATE2_PID:-}" "${BITDO_EXPECT_108JP_PID:-}"
|
||||
import json, sys, pathlib, datetime
|
||||
report_path = pathlib.Path(sys.argv[1])
|
||||
test_status = int(sys.argv[2])
|
||||
output_file = pathlib.Path(sys.argv[3])
|
||||
list_json_raw = sys.argv[4]
|
||||
|
||||
try:
|
||||
devices = json.loads(list_json_raw)
|
||||
except Exception:
|
||||
devices = []
|
||||
suite = sys.argv[4]
|
||||
required_families = [x for x in sys.argv[5].split(",") if x]
|
||||
expected_standard64 = sys.argv[6]
|
||||
expected_dinput = sys.argv[7]
|
||||
expected_jphandshake = sys.argv[8]
|
||||
expected_ultimate2 = sys.argv[9]
|
||||
expected_108jp = sys.argv[10]
|
||||
|
||||
report = {
|
||||
"timestamp_utc": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"suite": suite,
|
||||
"test_status": test_status,
|
||||
"tests_passed": test_status == 0,
|
||||
"devices": devices,
|
||||
"required_families": required_families,
|
||||
"required_family_fixtures": {
|
||||
"Standard64": expected_standard64,
|
||||
"DInput": expected_dinput,
|
||||
"JpHandshake": expected_jphandshake,
|
||||
},
|
||||
"required_device_fixtures": {
|
||||
"ultimate2": expected_ultimate2,
|
||||
"108jp": expected_108jp,
|
||||
},
|
||||
"raw_test_output": output_file.read_text(errors="replace"),
|
||||
}
|
||||
|
||||
@@ -43,3 +240,4 @@ PY
|
||||
|
||||
rm -f "$TEST_OUTPUT_FILE"
|
||||
echo "hardware smoke report written: $REPORT_PATH"
|
||||
exit "$TEST_STATUS"
|
||||
|
||||
26
sdk/tests/alias_index_integrity.rs
Normal file
26
sdk/tests/alias_index_integrity.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn alias_index_matches_unique_registry_policy() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let alias_path = manifest.join("../../../spec/alias_index.md");
|
||||
let body = fs::read_to_string(alias_path).expect("read alias_index.md");
|
||||
|
||||
assert!(body.contains("PID_Pro2_OLD"));
|
||||
assert!(body.contains("PID_Pro2"));
|
||||
assert!(body.contains("0x6003"));
|
||||
assert!(body.contains("PID_ASLGMouse"));
|
||||
assert!(body.contains("PID_Mouse"));
|
||||
assert!(body.contains("0x5205"));
|
||||
|
||||
let names = pid_registry()
|
||||
.iter()
|
||||
.map(|row| row.name)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(names.contains(&"PID_Pro2"));
|
||||
assert!(names.contains(&"PID_Mouse"));
|
||||
assert!(!names.contains(&"PID_Pro2_OLD"));
|
||||
assert!(!names.contains(&"PID_ASLGMouse"));
|
||||
}
|
||||
136
sdk/tests/candidate_readonly_gating.rs
Normal file
136
sdk/tests/candidate_readonly_gating.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use bitdo_proto::{
|
||||
device_profile_for, BitdoError, DeviceSession, MockTransport, SessionConfig, SupportLevel,
|
||||
SupportTier, VidPid,
|
||||
};
|
||||
|
||||
const CANDIDATE_READONLY_PIDS: &[u16] = &[
|
||||
0x6002, 0x6003, 0x3010, 0x3011, 0x3012, 0x3013, 0x5200, 0x5201, 0x203a, 0x2049, 0x2028, 0x202e,
|
||||
0x3004, 0x3019, 0x3100, 0x3105, 0x2100, 0x2101, 0x901a, 0x6006, 0x5203, 0x5204, 0x301a, 0x9028,
|
||||
0x3026, 0x3027,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn candidate_targets_are_candidate_readonly() {
|
||||
for pid in CANDIDATE_READONLY_PIDS {
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, *pid));
|
||||
assert_eq!(
|
||||
profile.support_tier,
|
||||
SupportTier::CandidateReadOnly,
|
||||
"expected candidate-readonly for pid={pid:#06x}"
|
||||
);
|
||||
assert_eq!(
|
||||
profile.support_level,
|
||||
SupportLevel::DetectOnly,
|
||||
"support_level remains detect-only until full promotion"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_standard_pid_allows_diag_read_but_blocks_write_and_unsafe() {
|
||||
let pid = 0x6002;
|
||||
let mut transport = MockTransport::default();
|
||||
// get_mode issues up to 3 reads; allow timeout outcome to prove it was permitted by policy.
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mode_err = session
|
||||
.get_mode()
|
||||
.expect_err("candidate get_mode should execute and fail only at transport/response stage");
|
||||
assert!(matches!(
|
||||
mode_err,
|
||||
BitdoError::Timeout | BitdoError::MalformedResponse { .. }
|
||||
));
|
||||
|
||||
let write_err = session
|
||||
.set_mode(1)
|
||||
.expect_err("candidate safe-write must be blocked");
|
||||
assert!(matches!(write_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let unsafe_err = session
|
||||
.enter_bootloader()
|
||||
.expect_err("candidate unsafe command must be blocked");
|
||||
assert!(matches!(unsafe_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_jp_pid_remains_diag_only() {
|
||||
let pid = 0x5200;
|
||||
let mut transport = MockTransport::default();
|
||||
transport.push_read_data({
|
||||
let mut response = vec![0u8; 64];
|
||||
response[0] = 0x02;
|
||||
response[1] = 0x05;
|
||||
response[4] = 0xC1;
|
||||
response
|
||||
});
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let identify = session.identify().expect("identify allowed");
|
||||
assert_eq!(identify.target.pid, pid);
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(profile.support_tier, SupportTier::CandidateReadOnly);
|
||||
|
||||
let mode_err = session
|
||||
.get_mode()
|
||||
.expect_err("jp candidate should not expose mode read path");
|
||||
assert!(matches!(mode_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave2_candidate_standard_pid_allows_safe_reads_only() {
|
||||
let pid = 0x3100;
|
||||
let mut transport = MockTransport::default();
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
transport.push_read_timeout();
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mode_err = session.get_mode().expect_err(
|
||||
"wave2 candidate get_mode should be permitted and fail at transport/response stage",
|
||||
);
|
||||
assert!(matches!(
|
||||
mode_err,
|
||||
BitdoError::Timeout | BitdoError::MalformedResponse { .. }
|
||||
));
|
||||
|
||||
let write_err = session
|
||||
.set_mode(1)
|
||||
.expect_err("wave2 candidate safe-write must be blocked");
|
||||
assert!(matches!(write_err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use assert_cmd::cargo::cargo_bin_cmd;
|
||||
use predicates::prelude::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn list_mock_text_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("2dc8:6009"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identify_mock_json_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "--json", "--pid", "24585", "identify"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"capability\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_get_mock_json_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "--json", "--pid", "24585", "mode", "get"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"mode\": 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diag_probe_mock_json_snapshot() {
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args(["--mock", "--json", "--pid", "24585", "diag", "probe"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"command_checks\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_dry_run_snapshot() {
|
||||
let tmp = std::env::temp_dir().join("bitdoctl-fw-test.bin");
|
||||
fs::write(&tmp, vec![0xAA; 128]).expect("write temp fw");
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("bitdoctl");
|
||||
cmd.args([
|
||||
"--mock",
|
||||
"--json",
|
||||
"--pid",
|
||||
"24585",
|
||||
"--unsafe",
|
||||
"--i-understand-brick-risk",
|
||||
"--experimental",
|
||||
"fw",
|
||||
"write",
|
||||
"--file",
|
||||
tmp.to_str().expect("path"),
|
||||
"--dry-run",
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"dry_run\": true"));
|
||||
|
||||
let _ = fs::remove_file(tmp);
|
||||
}
|
||||
59
sdk/tests/command_matrix_coverage.rs
Normal file
59
sdk/tests/command_matrix_coverage.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use bitdo_proto::{command_registry, CommandRuntimePolicy};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn command_registry_matches_spec_rows_and_runtime_policy() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let csv_path = manifest.join("../../../spec/command_matrix.csv");
|
||||
let content = fs::read_to_string(csv_path).expect("read command_matrix.csv");
|
||||
|
||||
let mut lines = content.lines();
|
||||
let header = lines.next().expect("command matrix header");
|
||||
let columns = header.split(',').collect::<Vec<_>>();
|
||||
let idx_command = col_index(&columns, "command_id");
|
||||
let idx_safety = col_index(&columns, "safety_class");
|
||||
let idx_confidence = col_index(&columns, "confidence");
|
||||
|
||||
let spec_rows = content
|
||||
.lines()
|
||||
.skip(1)
|
||||
.filter(|row| !row.trim().is_empty() && !row.starts_with("command_id,"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
spec_rows.len(),
|
||||
command_registry().len(),
|
||||
"command registry size mismatch vs command_matrix.csv"
|
||||
);
|
||||
|
||||
for row in spec_rows {
|
||||
let fields = row.split(',').collect::<Vec<_>>();
|
||||
let command_name = fields[idx_command];
|
||||
let safety = fields[idx_safety];
|
||||
let confidence = fields[idx_confidence];
|
||||
let reg = command_registry()
|
||||
.iter()
|
||||
.find(|entry| format!("{:?}", entry.id) == command_name)
|
||||
.unwrap_or_else(|| panic!("missing command in registry: {command_name}"));
|
||||
|
||||
let expected_policy = match (confidence, safety) {
|
||||
("confirmed", _) => CommandRuntimePolicy::EnabledDefault,
|
||||
("inferred", "SafeRead") => CommandRuntimePolicy::ExperimentalGate,
|
||||
("inferred", _) => CommandRuntimePolicy::BlockedUntilConfirmed,
|
||||
other => panic!("unknown confidence/safety tuple: {other:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reg.runtime_policy(),
|
||||
expected_policy,
|
||||
"runtime policy mismatch for command={command_name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn col_index(columns: &[&str], name: &str) -> usize {
|
||||
columns
|
||||
.iter()
|
||||
.position(|c| *c == name)
|
||||
.unwrap_or_else(|| panic!("missing column: {name}"))
|
||||
}
|
||||
@@ -32,14 +32,27 @@ fn diag_probe_returns_command_checks() {
|
||||
ver[4] = 1;
|
||||
transport.push_read_data(ver);
|
||||
|
||||
let mut super_button = vec![0u8; 64];
|
||||
super_button[0] = 0x02;
|
||||
super_button[1] = 0x05;
|
||||
transport.push_read_data(super_button);
|
||||
|
||||
let mut profile = vec![0u8; 64];
|
||||
profile[0] = 0x02;
|
||||
profile[1] = 0x05;
|
||||
transport.push_read_data(profile);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
transport,
|
||||
VidPid::new(0x2dc8, 24585),
|
||||
SessionConfig::default(),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("session init");
|
||||
|
||||
let diag = session.diag_probe();
|
||||
assert_eq!(diag.command_checks.len(), 4);
|
||||
assert_eq!(diag.command_checks.len(), 6);
|
||||
assert!(diag.command_checks.iter().all(|c| c.ok));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
|
||||
|
||||
#[test]
|
||||
fn firmware_transfer_chunks_and_commit() {
|
||||
fn inferred_firmware_transfer_is_blocked_until_confirmed() {
|
||||
let mut transport = MockTransport::default();
|
||||
for _ in 0..4 {
|
||||
transport.push_read_data(vec![0x02, 0x10, 0x00, 0x00]);
|
||||
@@ -20,11 +20,11 @@ fn firmware_transfer_chunks_and_commit() {
|
||||
.expect("session init");
|
||||
|
||||
let image = vec![0xAB; 120];
|
||||
let report = session
|
||||
let err = session
|
||||
.firmware_transfer(&image, 50, false)
|
||||
.expect("fw transfer");
|
||||
assert_eq!(report.chunks_sent, 3);
|
||||
.expect_err("inferred firmware chunk/commit must remain blocked");
|
||||
assert!(matches!(err, BitdoError::UnsupportedForPid { .. }));
|
||||
|
||||
let transport = session.into_transport();
|
||||
assert_eq!(transport.writes().len(), 4);
|
||||
assert_eq!(transport.writes().len(), 0);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use bitdo_proto::{command_registry, CommandFrame, CommandId, Report64};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn frame_encode_decode_roundtrip_for_all_commands() {
|
||||
assert_eq!(command_registry().len(), CommandId::all().len());
|
||||
let unique = command_registry()
|
||||
.iter()
|
||||
.map(|row| row.id)
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(unique.len(), CommandId::all().len());
|
||||
assert!(command_registry().len() >= unique.len());
|
||||
|
||||
for row in command_registry() {
|
||||
let frame = CommandFrame {
|
||||
|
||||
@@ -1,23 +1,91 @@
|
||||
use bitdo_proto::{device_profile_for, enumerate_hid_devices, ProtocolFamily, VidPid};
|
||||
use bitdo_proto::{
|
||||
device_profile_for, enumerate_hid_devices, DeviceSession, HidTransport, ProtocolFamily,
|
||||
SessionConfig, VidPid,
|
||||
};
|
||||
|
||||
fn hardware_enabled() -> bool {
|
||||
std::env::var("BITDO_HARDWARE").ok().as_deref() == Some("1")
|
||||
}
|
||||
|
||||
fn expected_pid(env_key: &str) -> Option<u16> {
|
||||
std::env::var(env_key).ok().and_then(|v| {
|
||||
let trimmed = v.trim();
|
||||
if let Some(hex) = trimmed
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| trimmed.strip_prefix("0X"))
|
||||
{
|
||||
u16::from_str_radix(hex, 16).ok()
|
||||
} else {
|
||||
trimmed.parse::<u16>().ok()
|
||||
}
|
||||
fn parse_pid(input: &str) -> Option<u16> {
|
||||
let trimmed = input.trim();
|
||||
if let Some(hex) = trimmed
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| trimmed.strip_prefix("0X"))
|
||||
{
|
||||
u16::from_str_radix(hex, 16).ok()
|
||||
} else {
|
||||
trimmed.parse::<u16>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_pid(env_key: &str, family: &str) -> u16 {
|
||||
let raw = std::env::var(env_key)
|
||||
.unwrap_or_else(|_| panic!("missing required {env_key} for {family} family hardware gate"));
|
||||
parse_pid(&raw).unwrap_or_else(|| {
|
||||
panic!("invalid {env_key} value '{raw}' for {family} family hardware gate")
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_8bitdo_pids() -> Vec<u16> {
|
||||
enumerate_hid_devices()
|
||||
.expect("enumeration")
|
||||
.into_iter()
|
||||
.filter(|d| d.vid_pid.vid == 0x2dc8)
|
||||
.map(|d| d.vid_pid.pid)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn assert_family_fixture(env_key: &str, family: &str, expected_family: ProtocolFamily) {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pid = expected_pid(env_key, family);
|
||||
let attached_pids = attached_8bitdo_pids();
|
||||
assert!(
|
||||
attached_pids.contains(&pid),
|
||||
"missing fixture for {family}: expected attached pid={pid:#06x}, attached={:?}",
|
||||
attached_pids
|
||||
.iter()
|
||||
.map(|value| format!("{value:#06x}"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family, expected_family,
|
||||
"expected {family} family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_named_fixture(env_key: &str, name: &str, expected_family: ProtocolFamily) -> u16 {
|
||||
if !hardware_enabled() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let pid = expected_pid(env_key, name);
|
||||
let attached_pids = attached_8bitdo_pids();
|
||||
assert!(
|
||||
attached_pids.contains(&pid),
|
||||
"missing fixture for {name}: expected attached pid={pid:#06x}, attached={:?}",
|
||||
attached_pids
|
||||
.iter()
|
||||
.map(|value| format!("{value:#06x}"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family, expected_family,
|
||||
"expected {name} family {:?} for pid={pid:#06x}, got {:?}",
|
||||
expected_family, profile.protocol_family
|
||||
);
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_HARDWARE=1"]
|
||||
fn hardware_smoke_detect_devices() {
|
||||
@@ -36,61 +104,140 @@ fn hardware_smoke_detect_devices() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_DINPUT_PID"]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_DINPUT_PID"]
|
||||
fn hardware_smoke_dinput_family() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(pid) = expected_pid("BITDO_EXPECT_DINPUT_PID") else {
|
||||
eprintln!("BITDO_EXPECT_DINPUT_PID not set, skipping DInput family check");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family,
|
||||
ProtocolFamily::DInput,
|
||||
"expected DInput family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
assert_family_fixture("BITDO_EXPECT_DINPUT_PID", "DInput", ProtocolFamily::DInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_STANDARD64_PID"]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_STANDARD64_PID"]
|
||||
fn hardware_smoke_standard64_family() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(pid) = expected_pid("BITDO_EXPECT_STANDARD64_PID") else {
|
||||
eprintln!("BITDO_EXPECT_STANDARD64_PID not set, skipping Standard64 family check");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family,
|
||||
assert_family_fixture(
|
||||
"BITDO_EXPECT_STANDARD64_PID",
|
||||
"Standard64",
|
||||
ProtocolFamily::Standard64,
|
||||
"expected Standard64 family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "optional family check; set BITDO_EXPECT_JPHANDSHAKE_PID"]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_JPHANDSHAKE_PID"]
|
||||
fn hardware_smoke_jphandshake_family() {
|
||||
assert_family_fixture(
|
||||
"BITDO_EXPECT_JPHANDSHAKE_PID",
|
||||
"JpHandshake",
|
||||
ProtocolFamily::JpHandshake,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_ULTIMATE2_PID"]
|
||||
fn hardware_smoke_ultimate2_core_ops() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
let Some(pid) = expected_pid("BITDO_EXPECT_JPHANDSHAKE_PID") else {
|
||||
eprintln!("BITDO_EXPECT_JPHANDSHAKE_PID not set, skipping JpHandshake family check");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert_eq!(
|
||||
profile.protocol_family,
|
||||
ProtocolFamily::JpHandshake,
|
||||
"expected JpHandshake family for pid={pid:#06x}, got {:?}",
|
||||
profile.protocol_family
|
||||
let pid = assert_named_fixture(
|
||||
"BITDO_EXPECT_ULTIMATE2_PID",
|
||||
"Ultimate2",
|
||||
ProtocolFamily::DInput,
|
||||
);
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert!(profile.capability.supports_u2_slot_config);
|
||||
assert!(profile.capability.supports_u2_button_map);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
HidTransport::new(),
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mode_before = session.get_mode().expect("read mode").mode;
|
||||
session
|
||||
.u2_set_mode(mode_before)
|
||||
.expect("mode read/write/readback");
|
||||
let mode_after = session.get_mode().expect("read mode after write").mode;
|
||||
assert_eq!(mode_after, mode_before);
|
||||
|
||||
let slot = session.u2_get_current_slot().expect("read current slot");
|
||||
let config_before = session.u2_read_config_slot(slot).expect("read config slot");
|
||||
session
|
||||
.u2_write_config_slot(slot, &config_before)
|
||||
.expect("write config slot");
|
||||
let config_after = session
|
||||
.u2_read_config_slot(slot)
|
||||
.expect("read config readback");
|
||||
assert!(!config_after.is_empty());
|
||||
|
||||
let map_before = session.u2_read_button_map(slot).expect("read button map");
|
||||
session
|
||||
.u2_write_button_map(slot, &map_before)
|
||||
.expect("write button map");
|
||||
let map_after = session
|
||||
.u2_read_button_map(slot)
|
||||
.expect("read button map readback");
|
||||
assert_eq!(map_before.len(), map_after.len());
|
||||
|
||||
// Firmware smoke is preflight-only in CI: dry_run avoids any transfer/write.
|
||||
session
|
||||
.firmware_transfer(&[0xAA; 128], 32, true)
|
||||
.expect("firmware preflight dry-run");
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires lab hardware and BITDO_EXPECT_108JP_PID"]
|
||||
fn hardware_smoke_108jp_dedicated_ops() {
|
||||
if !hardware_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pid = assert_named_fixture(
|
||||
"BITDO_EXPECT_108JP_PID",
|
||||
"JP108",
|
||||
ProtocolFamily::JpHandshake,
|
||||
);
|
||||
let profile = device_profile_for(VidPid::new(0x2dc8, pid));
|
||||
assert!(profile.capability.supports_jp108_dedicated_map);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
HidTransport::new(),
|
||||
VidPid::new(0x2dc8, pid),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("open session");
|
||||
|
||||
let mappings_before = session
|
||||
.jp108_read_dedicated_mappings()
|
||||
.expect("read dedicated mappings");
|
||||
assert!(mappings_before.len() >= 3);
|
||||
|
||||
for idx in [0u8, 1u8, 2u8] {
|
||||
let usage = mappings_before
|
||||
.iter()
|
||||
.find(|(entry_idx, _)| *entry_idx == idx)
|
||||
.map(|(_, usage)| *usage)
|
||||
.unwrap_or(0);
|
||||
session
|
||||
.jp108_write_dedicated_mapping(idx, usage)
|
||||
.expect("write dedicated mapping");
|
||||
}
|
||||
|
||||
let mappings_after = session
|
||||
.jp108_read_dedicated_mappings()
|
||||
.expect("read dedicated mappings readback");
|
||||
assert!(mappings_after.len() >= 3);
|
||||
|
||||
session
|
||||
.firmware_transfer(&[0xBB; 128], 32, true)
|
||||
.expect("firmware preflight dry-run");
|
||||
|
||||
let _ = session.close();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use bitdo_proto::{find_pid, pid_registry, ProtocolFamily, SupportLevel, SupportTier};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -7,10 +8,87 @@ fn pid_registry_matches_spec_rows() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let csv_path = manifest.join("../../../spec/pid_matrix.csv");
|
||||
let content = fs::read_to_string(csv_path).expect("read pid_matrix.csv");
|
||||
let rows = content
|
||||
.lines()
|
||||
.skip(1)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.count();
|
||||
let mut lines = content.lines();
|
||||
let header = lines.next().expect("pid matrix header");
|
||||
let columns = header.split(',').collect::<Vec<_>>();
|
||||
let idx_name = col_index(&columns, "pid_name");
|
||||
let idx_pid = col_index(&columns, "pid_hex");
|
||||
let idx_level = col_index(&columns, "support_level");
|
||||
let idx_tier = col_index(&columns, "support_tier");
|
||||
let idx_family = col_index(&columns, "protocol_family");
|
||||
|
||||
let rows = lines.filter(|l| !l.trim().is_empty()).count();
|
||||
assert_eq!(rows, pid_registry().len());
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
for row in content.lines().skip(1).filter(|l| !l.trim().is_empty()) {
|
||||
let fields = row.split(',').collect::<Vec<_>>();
|
||||
let name = fields[idx_name];
|
||||
let pid_hex = fields[idx_pid];
|
||||
let level = fields[idx_level];
|
||||
let tier = fields[idx_tier];
|
||||
let family = fields[idx_family];
|
||||
|
||||
let pid = parse_hex_u16(pid_hex);
|
||||
assert!(
|
||||
seen.insert(pid),
|
||||
"duplicate PID found in pid_matrix.csv: {pid_hex} (name={name})"
|
||||
);
|
||||
let reg = find_pid(pid).unwrap_or_else(|| panic!("missing pid in registry: {pid_hex}"));
|
||||
assert_eq!(reg.name, name, "name mismatch for pid={pid_hex}");
|
||||
assert_eq!(
|
||||
reg.support_level,
|
||||
parse_support_level(level),
|
||||
"support_level mismatch for pid={pid_hex}"
|
||||
);
|
||||
assert_eq!(
|
||||
reg.support_tier,
|
||||
parse_support_tier(tier),
|
||||
"support_tier mismatch for pid={pid_hex}"
|
||||
);
|
||||
assert_eq!(
|
||||
reg.protocol_family,
|
||||
parse_family(family),
|
||||
"protocol_family mismatch for pid={pid_hex}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn col_index(columns: &[&str], name: &str) -> usize {
|
||||
columns
|
||||
.iter()
|
||||
.position(|c| *c == name)
|
||||
.unwrap_or_else(|| panic!("missing column: {name}"))
|
||||
}
|
||||
|
||||
fn parse_hex_u16(v: &str) -> u16 {
|
||||
u16::from_str_radix(v.trim_start_matches("0x"), 16).expect("valid pid hex")
|
||||
}
|
||||
|
||||
fn parse_support_level(v: &str) -> SupportLevel {
|
||||
match v {
|
||||
"full" => SupportLevel::Full,
|
||||
"detect-only" => SupportLevel::DetectOnly,
|
||||
other => panic!("unknown support_level: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_support_tier(v: &str) -> SupportTier {
|
||||
match v {
|
||||
"full" => SupportTier::Full,
|
||||
"candidate-readonly" => SupportTier::CandidateReadOnly,
|
||||
"detect-only" => SupportTier::DetectOnly,
|
||||
other => panic!("unknown support_tier: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_family(v: &str) -> ProtocolFamily {
|
||||
match v {
|
||||
"Standard64" => ProtocolFamily::Standard64,
|
||||
"JpHandshake" => ProtocolFamily::JpHandshake,
|
||||
"DInput" => ProtocolFamily::DInput,
|
||||
"DS4Boot" => ProtocolFamily::DS4Boot,
|
||||
"Unknown" => ProtocolFamily::Unknown,
|
||||
other => panic!("unknown protocol_family: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
15
sdk/tests/pid_registry_unique.rs
Normal file
15
sdk/tests/pid_registry_unique.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use bitdo_proto::pid_registry;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn pid_registry_contains_unique_pid_values() {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
78
sdk/tests/runtime_policy.rs
Normal file
78
sdk/tests/runtime_policy.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use bitdo_proto::{
|
||||
find_command, BitdoError, CommandId, CommandRuntimePolicy, DeviceSession, DiagSeverity,
|
||||
EvidenceConfidence, MockTransport, SessionConfig, VidPid,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn inferred_safe_read_requires_experimental_mode() {
|
||||
let row = find_command(CommandId::GetSuperButton).expect("command present");
|
||||
assert_eq!(row.runtime_policy(), CommandRuntimePolicy::ExperimentalGate);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
MockTransport::default(),
|
||||
VidPid::new(0x2dc8, 0x6012),
|
||||
SessionConfig::default(),
|
||||
)
|
||||
.expect("session opens");
|
||||
|
||||
let err = session
|
||||
.send_command(CommandId::GetSuperButton, None)
|
||||
.expect_err("experimental gate must deny inferred safe-read by default");
|
||||
assert!(matches!(err, BitdoError::ExperimentalRequired { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inferred_write_is_blocked_until_confirmed() {
|
||||
let row = find_command(CommandId::WriteProfile).expect("command present");
|
||||
assert_eq!(
|
||||
row.runtime_policy(),
|
||||
CommandRuntimePolicy::BlockedUntilConfirmed
|
||||
);
|
||||
|
||||
let mut session = DeviceSession::new(
|
||||
MockTransport::default(),
|
||||
VidPid::new(0x2dc8, 0x6012),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("session opens");
|
||||
|
||||
let err = session
|
||||
.send_command(CommandId::WriteProfile, Some(&[1, 2, 3]))
|
||||
.expect_err("inferred writes remain blocked even in experimental mode");
|
||||
assert!(matches!(err, BitdoError::UnsupportedForPid { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_read_remains_enabled_default() {
|
||||
let row = find_command(CommandId::GetPid).expect("command present");
|
||||
assert_eq!(row.runtime_policy(), CommandRuntimePolicy::EnabledDefault);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diag_probe_marks_inferred_reads_as_experimental() {
|
||||
let mut session = DeviceSession::new(
|
||||
MockTransport::default(),
|
||||
VidPid::new(0x2dc8, 0x6012),
|
||||
SessionConfig {
|
||||
experimental: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("session opens");
|
||||
|
||||
let diag = session.diag_probe();
|
||||
let inferred = diag
|
||||
.command_checks
|
||||
.iter()
|
||||
.find(|c| c.command == CommandId::GetSuperButton)
|
||||
.expect("inferred check present");
|
||||
assert!(inferred.is_experimental);
|
||||
assert_eq!(inferred.confidence, EvidenceConfidence::Inferred);
|
||||
assert!(matches!(
|
||||
inferred.severity,
|
||||
DiagSeverity::Ok | DiagSeverity::Warning | DiagSeverity::NeedsAttention
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user