Bootstrap OpenBitdo clean-room SDK and reliability milestone

This commit is contained in:
2026-02-27 20:43:34 -05:00
commit d5afadf560
46 changed files with 3652 additions and 0 deletions

41
sdk/tests/boot_safety.rs Normal file
View File

@@ -0,0 +1,41 @@
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
#[test]
fn unsafe_boot_requires_dual_ack() {
let transport = MockTransport::default();
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, 24585),
SessionConfig {
allow_unsafe: true,
brick_risk_ack: false,
experimental: true,
..SessionConfig::default()
},
)
.expect("session init");
let err = session.enter_bootloader().expect_err("expected denial");
match err {
BitdoError::UnsafeCommandDenied { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn unsafe_boot_succeeds_with_dual_ack() {
let transport = MockTransport::default();
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, 24585),
SessionConfig {
allow_unsafe: true,
brick_risk_ack: true,
experimental: true,
..SessionConfig::default()
},
)
.expect("session init");
session.enter_bootloader().expect("boot sequence");
}

View File

@@ -0,0 +1,23 @@
use bitdo_proto::{BitdoError, DeviceSession, MockTransport, SessionConfig, VidPid};
#[test]
fn detect_only_pid_blocks_unsafe_operations() {
let transport = MockTransport::default();
let config = SessionConfig {
allow_unsafe: true,
brick_risk_ack: true,
experimental: true,
..SessionConfig::default()
};
let mut session =
DeviceSession::new(transport, VidPid::new(0x2dc8, 8448), config).expect("session init");
let err = session
.enter_bootloader()
.expect_err("must reject unsafe op");
match err {
BitdoError::UnsupportedForPid { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}

View File

@@ -0,0 +1,17 @@
use std::path::PathBuf;
use std::process::Command;
#[test]
fn guard_script_passes() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let sdk_root = manifest.join("../..");
let script = sdk_root.join("scripts/cleanroom_guard.sh");
let status = Command::new("bash")
.arg(script)
.current_dir(&sdk_root)
.status()
.expect("run cleanroom_guard.sh");
assert!(status.success());
}

66
sdk/tests/cli_snapshot.rs Normal file
View File

@@ -0,0 +1,66 @@
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);
}

45
sdk/tests/diag_probe.rs Normal file
View File

@@ -0,0 +1,45 @@
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
#[test]
fn diag_probe_returns_command_checks() {
let mut transport = MockTransport::default();
let mut pid = vec![0u8; 64];
pid[0] = 0x02;
pid[1] = 0x05;
pid[4] = 0xC1;
pid[22] = 0x09;
pid[23] = 0x60;
transport.push_read_data(pid);
let mut rr = vec![0u8; 64];
rr[0] = 0x02;
rr[1] = 0x04;
rr[5] = 0x01;
transport.push_read_data(rr);
let mut mode = vec![0u8; 64];
mode[0] = 0x02;
mode[1] = 0x05;
mode[5] = 2;
transport.push_read_data(mode);
let mut ver = vec![0u8; 64];
ver[0] = 0x02;
ver[1] = 0x22;
ver[2] = 0x2A;
ver[3] = 0x00;
ver[4] = 1;
transport.push_read_data(ver);
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, 24585),
SessionConfig::default(),
)
.expect("session init");
let diag = session.diag_probe();
assert_eq!(diag.command_checks.len(), 4);
assert!(diag.command_checks.iter().all(|c| c.ok));
}

10
sdk/tests/error_codes.rs Normal file
View File

@@ -0,0 +1,10 @@
use bitdo_proto::{BitdoError, BitdoErrorCode};
#[test]
fn bitdo_error_maps_to_stable_codes() {
let err = BitdoError::InvalidInput("bad".to_owned());
assert_eq!(err.code(), BitdoErrorCode::InvalidInput);
let err = BitdoError::Timeout;
assert_eq!(err.code(), BitdoErrorCode::Timeout);
}

View File

@@ -0,0 +1,30 @@
use bitdo_proto::{DeviceSession, MockTransport, SessionConfig, VidPid};
#[test]
fn firmware_transfer_chunks_and_commit() {
let mut transport = MockTransport::default();
for _ in 0..4 {
transport.push_read_data(vec![0x02, 0x10, 0x00, 0x00]);
}
let mut session = DeviceSession::new(
transport,
VidPid::new(0x2dc8, 24585),
SessionConfig {
allow_unsafe: true,
brick_risk_ack: true,
experimental: true,
..SessionConfig::default()
},
)
.expect("session init");
let image = vec![0xAB; 120];
let report = session
.firmware_transfer(&image, 50, false)
.expect("fw transfer");
assert_eq!(report.chunks_sent, 3);
let transport = session.into_transport();
assert_eq!(transport.writes().len(), 4);
}

View File

@@ -0,0 +1,23 @@
use bitdo_proto::{command_registry, CommandFrame, CommandId, Report64};
#[test]
fn frame_encode_decode_roundtrip_for_all_commands() {
assert_eq!(command_registry().len(), CommandId::all().len());
for row in command_registry() {
let frame = CommandFrame {
id: row.id,
payload: row.request.to_vec(),
report_id: row.report_id,
expected_response: row.expected_response,
};
let encoded = frame.encode();
if encoded.len() == 64 {
let parsed = Report64::try_from(encoded.as_slice()).expect("64-byte frame parses");
assert_eq!(parsed.as_slice(), encoded.as_slice());
} else {
assert!(!encoded.is_empty());
}
}
}

View File

@@ -0,0 +1,96 @@
use bitdo_proto::{device_profile_for, enumerate_hid_devices, ProtocolFamily, 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()
}
})
}
#[test]
#[ignore = "requires lab hardware and BITDO_HARDWARE=1"]
fn hardware_smoke_detect_devices() {
if !hardware_enabled() {
eprintln!("BITDO_HARDWARE!=1, skipping");
return;
}
let devices = enumerate_hid_devices().expect("enumeration");
let eight_bitdo: Vec<_> = devices
.into_iter()
.filter(|d| d.vid_pid.vid == 0x2dc8)
.collect();
assert!(!eight_bitdo.is_empty(), "no 8BitDo devices detected");
}
#[test]
#[ignore = "optional family check; set 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
);
}
#[test]
#[ignore = "optional family check; set 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,
ProtocolFamily::Standard64,
"expected Standard64 family for pid={pid:#06x}, got {:?}",
profile.protocol_family
);
}
#[test]
#[ignore = "optional family check; set BITDO_EXPECT_JPHANDSHAKE_PID"]
fn hardware_smoke_jphandshake_family() {
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
);
}

View File

@@ -0,0 +1,34 @@
use bitdo_proto::{
DeviceSession, MockTransport, RetryPolicy, SessionConfig, TimeoutProfile, VidPid,
};
#[test]
fn set_mode_reads_back_latest_mode() {
let mut transport = MockTransport::default();
transport.push_read_data(vec![0x02, 0x01, 0x00, 0x00]);
let mut mode = vec![0u8; 64];
mode[0] = 0x02;
mode[1] = 0x05;
mode[5] = 3;
transport.push_read_data(mode);
let config = SessionConfig {
retry_policy: RetryPolicy {
max_attempts: 2,
backoff_ms: 0,
},
timeout_profile: TimeoutProfile {
probe_ms: 10,
io_ms: 10,
firmware_ms: 10,
},
..SessionConfig::default()
};
let mut session =
DeviceSession::new(transport, VidPid::new(0x2dc8, 24585), config).expect("session init");
let mode_state = session.set_mode(3).expect("set mode");
assert_eq!(mode_state.mode, 3);
}

View File

@@ -0,0 +1,29 @@
use bitdo_proto::{validate_response, CommandId, ResponseStatus};
#[test]
fn malformed_response_is_rejected() {
let status = validate_response(CommandId::GetPid, &[0x02]);
assert_eq!(status, ResponseStatus::Malformed);
}
#[test]
fn invalid_signature_is_rejected() {
let mut bad = vec![0u8; 64];
bad[0] = 0x00;
bad[1] = 0x05;
bad[4] = 0xC1;
let status = validate_response(CommandId::GetPid, &bad);
assert_eq!(status, ResponseStatus::Invalid);
}
#[test]
fn valid_signature_is_accepted() {
let mut good = vec![0u8; 64];
good[0] = 0x02;
good[1] = 0x05;
good[4] = 0xC1;
good[22] = 0x09;
good[23] = 0x60;
let status = validate_response(CommandId::GetPid, &good);
assert_eq!(status, ResponseStatus::Ok);
}

View File

@@ -0,0 +1,16 @@
use bitdo_proto::pid_registry;
use std::fs;
use std::path::PathBuf;
#[test]
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();
assert_eq!(rows, pid_registry().len());
}

View File

@@ -0,0 +1,17 @@
use bitdo_proto::ProfileBlob;
use std::fs;
use std::path::PathBuf;
#[test]
fn golden_profile_fixture_roundtrips() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let path = manifest.join("../../../harness/golden/profile_fixture.bin");
let fixture = fs::read(path).expect("read fixture");
let blob = ProfileBlob::from_bytes(&fixture).expect("parse fixture");
assert_eq!(blob.slot, 2);
assert_eq!(blob.payload.len(), 16);
let serialized = blob.to_bytes();
assert_eq!(serialized, fixture);
}

View File

@@ -0,0 +1,86 @@
use bitdo_proto::{
DeviceSession, MockTransport, RetryPolicy, SessionConfig, TimeoutProfile, VidPid,
};
#[test]
fn retries_after_timeout_then_succeeds() {
let mut transport = MockTransport::default();
transport.push_read_timeout();
let mut good = vec![0u8; 64];
good[0] = 0x02;
good[1] = 0x05;
good[4] = 0xC1;
good[22] = 0x09;
good[23] = 0x60;
transport.push_read_data(good);
let config = SessionConfig {
retry_policy: RetryPolicy {
max_attempts: 3,
backoff_ms: 0,
},
timeout_profile: TimeoutProfile {
probe_ms: 1,
io_ms: 1,
firmware_ms: 1,
},
allow_unsafe: false,
brick_risk_ack: false,
experimental: false,
trace_enabled: true,
};
let mut session =
DeviceSession::new(transport, VidPid::new(0x2dc8, 24585), config).expect("session init");
let response = session
.send_command(bitdo_proto::CommandId::GetPid, None)
.expect("response");
assert_eq!(
response.parsed_fields.get("detected_pid").copied(),
Some(24585)
);
}
#[test]
fn retries_after_malformed_then_succeeds() {
let mut transport = MockTransport::default();
let mut malformed = vec![0u8; 64];
malformed[0] = 0x00;
malformed[1] = 0x05;
malformed[4] = 0xC1;
transport.push_read_data(malformed);
let mut good = vec![0u8; 64];
good[0] = 0x02;
good[1] = 0x05;
good[4] = 0xC1;
good[22] = 0x09;
good[23] = 0x60;
transport.push_read_data(good);
let config = SessionConfig {
retry_policy: RetryPolicy {
max_attempts: 3,
backoff_ms: 0,
},
timeout_profile: TimeoutProfile {
probe_ms: 1,
io_ms: 1,
firmware_ms: 1,
},
allow_unsafe: false,
brick_risk_ack: false,
experimental: false,
trace_enabled: true,
};
let mut session =
DeviceSession::new(transport, VidPid::new(0x2dc8, 24585), config).expect("session init");
let response = session
.send_command(bitdo_proto::CommandId::GetPid, None)
.expect("response");
assert_eq!(
response.parsed_fields.get("detected_pid").copied(),
Some(24585)
);
}