libfreemkv v0.1.0 — Open source 4K UHD / Blu-ray / DVD drive library
Features: - Open drive identification via SPC-4 INQUIRY + MMC-6 GET CONFIGURATION - 141 supported drives with bundled profiles - MT1959 platform: unlock, calibrate, raw sector reads - DriveSpeed enum: BD1x-BD12x, DVD1x-DVD16x - Field names follow SPC-4 §6.4.2 and MMC-6 §5.3.10 standards - No proprietary fingerprints — open matching by SCSI fields - Zero config: profiles compiled into binary Tested on real hardware: HL-DT-ST BD-RE BU40N 1.03
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
//! freemkv-info — Drive identification and compatibility checker.
|
||||
//!
|
||||
//! Sends standard SCSI INQUIRY and GET CONFIGURATION commands to an optical drive,
|
||||
//! displays drive identity and compatibility status, and optionally outputs raw
|
||||
//! response data for profile contribution.
|
||||
//!
|
||||
//! Usage:
|
||||
//! freemkv-info /dev/sr0
|
||||
//! freemkv-info /dev/sr0 --raw
|
||||
//! freemkv-info /dev/sr0 --json
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("freemkv-info — Drive identification and compatibility checker");
|
||||
eprintln!();
|
||||
eprintln!("Usage: freemkv-info <device> [options]");
|
||||
eprintln!();
|
||||
eprintln!(" <device> Optical drive device (e.g. /dev/sr0)");
|
||||
eprintln!(" --raw Output raw SCSI response hex (for profile contribution)");
|
||||
eprintln!(" --json Output machine-readable JSON");
|
||||
eprintln!(" --profiles Path to profiles directory (default: ./profiles)");
|
||||
eprintln!();
|
||||
eprintln!("Examples:");
|
||||
eprintln!(" freemkv-info /dev/sr0");
|
||||
eprintln!(" freemkv-info /dev/sr0 --raw > my_drive.txt");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let device = Path::new(&args[1]);
|
||||
let raw_mode = args.iter().any(|a| a == "--raw");
|
||||
let json_mode = args.iter().any(|a| a == "--json");
|
||||
let profiles_dir = args.iter()
|
||||
.position(|a| a == "--profiles")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("profiles");
|
||||
|
||||
// Open SCSI transport
|
||||
let mut transport = match libfreemkv::scsi::SgIoTransport::open(device) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("Error: Cannot open {}: {}", device.display(), e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// INQUIRY
|
||||
let inquiry = match libfreemkv::scsi::inquiry(&mut transport) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
eprintln!("Error: INQUIRY failed: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// GET CONFIGURATION feature 0x010C
|
||||
let gc_010c = libfreemkv::scsi::get_config_010c(&mut transport).ok();
|
||||
|
||||
if json_mode {
|
||||
print_json(&inquiry, &gc_010c);
|
||||
} else if raw_mode {
|
||||
print_raw(&inquiry, &gc_010c);
|
||||
} else {
|
||||
print_human(&inquiry, &gc_010c, profiles_dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human(
|
||||
inquiry: &libfreemkv::scsi::InquiryResult,
|
||||
gc_010c: &Option<Vec<u8>>,
|
||||
profiles_dir: &str,
|
||||
) {
|
||||
println!("freemkv-info v{}", env!("CARGO_PKG_VERSION"));
|
||||
println!();
|
||||
println!("Drive: {} {} {}", inquiry.vendor_id, inquiry.model, inquiry.firmware);
|
||||
println!("INQUIRY: additional_length=0x{:02X} ({})",
|
||||
inquiry.raw.get(4).unwrap_or(&0),
|
||||
inquiry.raw.get(4).unwrap_or(&0));
|
||||
|
||||
if let Some(gc) = gc_010c {
|
||||
let data_hex: String = gc.iter().map(|b| format!("{:02x}", b)).collect();
|
||||
println!("Feature 0x010C: {}", data_hex);
|
||||
} else {
|
||||
println!("Feature 0x010C: not available");
|
||||
}
|
||||
|
||||
// Try to match profile
|
||||
if let Ok(profiles) = libfreemkv::profile::load_all(Path::new(profiles_dir)) {
|
||||
let matched = profiles.iter().find(|p| {
|
||||
p.drive_id.contains(&inquiry.vendor_id)
|
||||
&& p.drive_id.contains(&inquiry.model)
|
||||
});
|
||||
|
||||
println!();
|
||||
match matched {
|
||||
Some(p) => {
|
||||
println!("Profile: FOUND ({})", p.platform.name());
|
||||
println!("Raw Read: Supported");
|
||||
}
|
||||
None => {
|
||||
println!("Profile: NOT FOUND");
|
||||
println!("Raw Read: Unknown — run with --raw and submit a profile request");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!();
|
||||
println!("Profile: No profiles directory found at '{}'", profiles_dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_raw(
|
||||
inquiry: &libfreemkv::scsi::InquiryResult,
|
||||
gc_010c: &Option<Vec<u8>>,
|
||||
) {
|
||||
println!("# freemkv-info raw output");
|
||||
println!("# Submit this file to https://github.com/freemkv/libfreemkv/issues");
|
||||
println!();
|
||||
println!("vendor: {}", inquiry.vendor_id);
|
||||
println!("model: {}", inquiry.model);
|
||||
println!("firmware: {}", inquiry.firmware);
|
||||
println!();
|
||||
|
||||
// Full INQUIRY hex
|
||||
println!("inquiry_hex: {}", hex_encode(&inquiry.raw));
|
||||
println!("inquiry_length: {}", inquiry.raw.len());
|
||||
|
||||
// GET CONFIG 0x010C
|
||||
if let Some(gc) = gc_010c {
|
||||
println!("get_config_010c_hex: {}", hex_encode(gc));
|
||||
println!("get_config_010c_length: {}", gc.len());
|
||||
} else {
|
||||
println!("get_config_010c_hex: ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(
|
||||
inquiry: &libfreemkv::scsi::InquiryResult,
|
||||
gc_010c: &Option<Vec<u8>>,
|
||||
) {
|
||||
let json = serde_json::json!({
|
||||
"vendor": inquiry.vendor_id,
|
||||
"model": inquiry.model,
|
||||
"firmware": inquiry.firmware,
|
||||
"inquiry_hex": hex_encode(&inquiry.raw),
|
||||
"inquiry_length": inquiry.raw.len(),
|
||||
"get_config_010c_hex": gc_010c.as_ref().map(|g| hex_encode(g)),
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
||||
}
|
||||
|
||||
fn hex_encode(data: &[u8]) -> String {
|
||||
data.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
Reference in New Issue
Block a user