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()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! freemkv-test — Quick verification that raw disc access works.
|
||||
//!
|
||||
//! Enables raw read mode, calibrates speed, reads a few test sectors.
|
||||
//! Use this to verify your drive and profile are working correctly.
|
||||
//!
|
||||
//! Usage:
|
||||
//! freemkv-test /dev/sr0
|
||||
//! freemkv-test /dev/sr0 --profiles ./profiles
|
||||
|
||||
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-test — Verify raw disc access works");
|
||||
eprintln!();
|
||||
eprintln!("Usage: freemkv-test <device> [--profiles <dir>]");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let device = Path::new(&args[1]);
|
||||
|
||||
println!("freemkv-test v{}", env!("CARGO_PKG_VERSION"));
|
||||
println!();
|
||||
|
||||
// Open drive session (uses bundled profiles)
|
||||
print!("Opening {}... ", device.display());
|
||||
let mut session = match libfreemkv::DriveSession::open(device) {
|
||||
Ok(s) => { println!("OK"); s }
|
||||
Err(e) => { println!("FAILED: {}", e); process::exit(1); }
|
||||
};
|
||||
|
||||
println!(" Drive ID: {}", session.profile.drive_id);
|
||||
println!(" Platform: {}", session.profile.platform.name());
|
||||
println!();
|
||||
|
||||
// Enable raw read mode
|
||||
print!("Unlocking drive... ");
|
||||
match session.unlock() {
|
||||
Ok(()) => println!("OK"),
|
||||
Err(e) => { println!("FAILED: {}", e); process::exit(1); }
|
||||
}
|
||||
|
||||
// Check status
|
||||
print!("Checking status... ");
|
||||
match session.status() {
|
||||
Ok(status) => {
|
||||
if status.unlocked {
|
||||
println!("OK (active)");
|
||||
} else {
|
||||
println!("WARNING: drive reported as locked");
|
||||
}
|
||||
}
|
||||
Err(e) => println!("SKIP ({})", e),
|
||||
}
|
||||
|
||||
// Calibrate speed
|
||||
print!("Calibrating speed... ");
|
||||
match session.calibrate() {
|
||||
Ok(()) => println!("OK"),
|
||||
Err(e) => println!("SKIP ({})", e),
|
||||
}
|
||||
|
||||
// Read test sectors
|
||||
let test_lbas: &[u32] = &[0, 100, 1000, 10000];
|
||||
let mut buf = vec![0u8; 2048];
|
||||
let mut pass = 0;
|
||||
let mut fail = 0;
|
||||
|
||||
for &lba in test_lbas {
|
||||
print!("Reading sector {}... ", lba);
|
||||
match session.read_sectors(lba, 1, &mut buf) {
|
||||
Ok(n) if n == 2048 => {
|
||||
let nonzero = buf.iter().filter(|&&b| b != 0).count();
|
||||
println!("OK ({} bytes, {} non-zero)", n, nonzero);
|
||||
pass += 1;
|
||||
}
|
||||
Ok(n) => {
|
||||
println!("PARTIAL ({} bytes)", n);
|
||||
fail += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("FAILED: {}", e);
|
||||
fail += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if fail == 0 {
|
||||
println!("All {} checks passed. Drive is fully functional.", pass);
|
||||
} else {
|
||||
println!("{} passed, {} failed.", pass, fail);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
//! High-level drive session — the main API for consumers.
|
||||
//!
|
||||
//! Opens a drive, identifies it via standard SCSI commands,
|
||||
//! matches it against the profile database, and provides
|
||||
//! raw disc access methods.
|
||||
|
||||
use std::path::Path;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::scsi::{SgIoTransport, ScsiTransport};
|
||||
use crate::identity::DriveId;
|
||||
use crate::profile::{self, DriveProfile, PlatformType};
|
||||
use crate::platform::{Platform, DriveStatus};
|
||||
use crate::platform::mt1959::Mt1959;
|
||||
|
||||
/// A complete drive session.
|
||||
///
|
||||
/// Handles: identify → match profile → create platform → execute commands.
|
||||
pub struct DriveSession {
|
||||
scsi: Box<dyn ScsiTransport>,
|
||||
platform: Box<dyn Platform>,
|
||||
pub profile: DriveProfile,
|
||||
pub drive_id: DriveId,
|
||||
}
|
||||
|
||||
impl DriveSession {
|
||||
/// Open a drive, identify it, and find the matching profile.
|
||||
/// Uses the bundled profile database — no external files needed.
|
||||
pub fn open(device: &Path) -> Result<Self> {
|
||||
let mut transport = SgIoTransport::open(device)?;
|
||||
let profiles = profile::load_bundled()?;
|
||||
|
||||
// Identify drive via standard SCSI commands
|
||||
// SPC-4 §6.4 (INQUIRY) + MMC-6 §5.3.10 (Feature 010Ch)
|
||||
let drive_id = DriveId::from_drive(&mut transport)?;
|
||||
|
||||
// Match drive to a profile by INQUIRY fields
|
||||
let profile = profile::find_by_drive_id(&profiles, &drive_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::UnsupportedDrive(format!("{}", drive_id)))?;
|
||||
|
||||
if !profile.supported {
|
||||
return Err(Error::UnsupportedDrive(format!(
|
||||
"{} — status: {:?}", drive_id, profile.status
|
||||
)));
|
||||
}
|
||||
|
||||
let platform: Box<dyn Platform> = match profile.platform {
|
||||
PlatformType::Mt1959A | PlatformType::Mt1959B => {
|
||||
Box::new(Mt1959::new(profile.clone()))
|
||||
}
|
||||
PlatformType::Pioneer => {
|
||||
return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into()));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DriveSession {
|
||||
scsi: Box::new(transport),
|
||||
platform,
|
||||
profile,
|
||||
drive_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open with an explicit profile (skip auto-detection).
|
||||
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
|
||||
let mut transport = SgIoTransport::open(device)?;
|
||||
let drive_id = DriveId::from_drive(&mut transport)?;
|
||||
|
||||
let platform: Box<dyn Platform> = match profile.platform {
|
||||
PlatformType::Mt1959A | PlatformType::Mt1959B => {
|
||||
Box::new(Mt1959::new(profile.clone()))
|
||||
}
|
||||
PlatformType::Pioneer => {
|
||||
return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into()));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DriveSession {
|
||||
scsi: Box::new(transport),
|
||||
platform,
|
||||
profile,
|
||||
drive_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Activate raw disc access mode.
|
||||
pub fn unlock(&mut self) -> Result<()> {
|
||||
self.platform.unlock(self.scsi.as_mut())
|
||||
}
|
||||
|
||||
/// Check if raw disc access mode is enabled.
|
||||
pub fn is_unlocked(&self) -> bool {
|
||||
self.platform.is_unlocked()
|
||||
}
|
||||
|
||||
/// Read drive status and feature flags.
|
||||
pub fn status(&mut self) -> Result<DriveStatus> {
|
||||
self.platform.status(self.scsi.as_mut())
|
||||
}
|
||||
|
||||
/// Read drive configuration block.
|
||||
pub fn read_config(&mut self) -> Result<Vec<u8>> {
|
||||
self.platform.read_config(self.scsi.as_mut())
|
||||
}
|
||||
|
||||
/// Read hardware register.
|
||||
pub fn read_register(&mut self, index: u8) -> Result<[u8; 16]> {
|
||||
self.platform.read_register(self.scsi.as_mut(), index)
|
||||
}
|
||||
|
||||
/// Calibrate read speed for the current disc.
|
||||
pub fn calibrate(&mut self) -> Result<()> {
|
||||
self.platform.calibrate(self.scsi.as_mut())
|
||||
}
|
||||
|
||||
/// Read raw disc sectors.
|
||||
pub fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
self.platform.read_sectors(self.scsi.as_mut(), lba, count, buf)
|
||||
}
|
||||
|
||||
/// Generic probe command.
|
||||
pub fn probe(&mut self, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>> {
|
||||
self.platform.probe(self.scsi.as_mut(), sub_cmd, address, length)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DeviceNotFound(String),
|
||||
UnsupportedDrive(String),
|
||||
ScsiError { cdb: Vec<u8>, status: u8, sense: Vec<u8> },
|
||||
UnlockFailed(String),
|
||||
NotUnlocked,
|
||||
NotCalibrated,
|
||||
ProfileNotFound(String),
|
||||
ProfileParse(String),
|
||||
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::DeviceNotFound(s) => write!(f, "device not found: {s}"),
|
||||
Error::UnsupportedDrive(s) => write!(f, "unsupported drive: {s}"),
|
||||
Error::ScsiError { status, .. } => write!(f, "SCSI error: status 0x{status:02x}"),
|
||||
Error::UnlockFailed(s) => write!(f, "unlock failed: {s}"),
|
||||
Error::NotUnlocked => write!(f, "drive not unlocked, call unlock() first"),
|
||||
Error::NotCalibrated => write!(f, "speed not calibrated, call calibrate() first"),
|
||||
Error::ProfileNotFound(s) => write!(f, "no profile for: {s}"),
|
||||
Error::ProfileParse(s) => write!(f, "profile parse error: {s}"),
|
||||
Error::SignatureMismatch { expected, got } => {
|
||||
write!(f, "signature mismatch: expected {:02x}{:02x}{:02x}{:02x}, got {:02x}{:02x}{:02x}{:02x}",
|
||||
expected[0], expected[1], expected[2], expected[3],
|
||||
got[0], got[1], got[2], got[3])
|
||||
}
|
||||
Error::Io(e) => write!(f, "I/O error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
//! Drive identification — match drives to profiles by SCSI response fields.
|
||||
//!
|
||||
//! Field names follow SPC-4 (INQUIRY) and MMC-6 (GET CONFIGURATION) standards.
|
||||
//! No proprietary fingerprints or encrypted lookups — open matching only.
|
||||
//!
|
||||
//! References:
|
||||
//! SPC-4 §6.4.2 — Standard INQUIRY data
|
||||
//! MMC-6 §5.3.10 — Feature 010Ch (Firmware Information)
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::scsi::{ScsiTransport, DataDirection};
|
||||
|
||||
/// Drive identity from standard SCSI commands.
|
||||
///
|
||||
/// All field names follow the SCSI standards:
|
||||
/// - SPC-4 §6.4.2 for INQUIRY fields
|
||||
/// - MMC-6 §5.3.10 for Firmware Information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DriveId {
|
||||
/// T10 VENDOR IDENTIFICATION — INQUIRY bytes [8:16]
|
||||
/// SPC-4 §6.4.2
|
||||
pub vendor_id: String,
|
||||
|
||||
/// PRODUCT IDENTIFICATION — INQUIRY bytes [16:32]
|
||||
/// SPC-4 §6.4.2
|
||||
pub product_id: String,
|
||||
|
||||
/// PRODUCT REVISION LEVEL — INQUIRY bytes [32:36]
|
||||
/// SPC-4 §6.4.2
|
||||
pub product_revision: String,
|
||||
|
||||
/// VENDOR SPECIFIC — INQUIRY bytes [36:43]
|
||||
/// SPC-4 §6.4.2
|
||||
/// Content varies by vendor: firmware type code (MTK), date (Pioneer), etc.
|
||||
pub vendor_specific: String,
|
||||
|
||||
/// Firmware Creation Date — GET CONFIGURATION Feature 010Ch
|
||||
/// MMC-6 §5.3.10
|
||||
/// Format: CCYYMMDDHHMI (12 ASCII characters)
|
||||
pub firmware_date: String,
|
||||
|
||||
/// Raw 96-byte INQUIRY response for additional parsing if needed.
|
||||
pub raw_inquiry: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DriveId {
|
||||
/// Probe a real drive via SCSI and build its identity.
|
||||
pub fn from_drive(transport: &mut dyn ScsiTransport) -> Result<Self> {
|
||||
// INQUIRY — SPC-4 §6.4
|
||||
let mut inquiry = vec![0u8; 96];
|
||||
let cdb_inq = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00];
|
||||
transport.execute(&cdb_inq, DataDirection::FromDevice, &mut inquiry, 5000)?;
|
||||
|
||||
// GET CONFIGURATION Feature 010Ch — MMC-6 §6.6
|
||||
let mut gc = vec![0u8; 256];
|
||||
let cdb_gc = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
|
||||
let result = transport.execute(&cdb_gc, DataDirection::FromDevice, &mut gc, 5000)?;
|
||||
|
||||
let firmware_date = if result.bytes_transferred > 12 {
|
||||
String::from_utf8_lossy(&gc[12..24.min(result.bytes_transferred)])
|
||||
.trim().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
Ok(Self::from_inquiry(&inquiry, &firmware_date))
|
||||
}
|
||||
|
||||
/// Build identity from raw INQUIRY bytes and firmware date string.
|
||||
pub fn from_inquiry(inquiry: &[u8], firmware_date: &str) -> Self {
|
||||
DriveId {
|
||||
vendor_id: ascii_field(inquiry, 8, 16),
|
||||
product_id: ascii_field(inquiry, 16, 32),
|
||||
product_revision: ascii_field(inquiry, 32, 36),
|
||||
vendor_specific: ascii_field(inquiry, 36, 43),
|
||||
firmware_date: firmware_date.to_string(),
|
||||
raw_inquiry: inquiry.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Profile match key: "VENDOR|PRODUCT|REVISION|VENDOR_SPECIFIC"
|
||||
///
|
||||
/// Used to look up this drive in the profile database.
|
||||
/// All fields trimmed for consistent matching.
|
||||
pub fn match_key(&self) -> String {
|
||||
format!("{}|{}|{}|{}",
|
||||
self.vendor_id.trim(),
|
||||
self.product_id.trim(),
|
||||
self.product_revision.trim(),
|
||||
self.vendor_specific.trim())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DriveId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} {} {} {}",
|
||||
self.vendor_id.trim(),
|
||||
self.product_id.trim(),
|
||||
self.product_revision.trim(),
|
||||
self.vendor_specific.trim())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract an ASCII string field from raw SCSI data.
|
||||
fn ascii_field(data: &[u8], start: usize, end: usize) -> String {
|
||||
if data.len() > start {
|
||||
let e = end.min(data.len());
|
||||
String::from_utf8_lossy(&data[start..e]).to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bu40n_identity() {
|
||||
let mut inquiry = vec![0u8; 96];
|
||||
inquiry[4] = 0x5B;
|
||||
inquiry[8..16].copy_from_slice(b"HL-DT-ST");
|
||||
inquiry[16..32].copy_from_slice(b"BD-RE BU40N ");
|
||||
inquiry[32..36].copy_from_slice(b"1.03");
|
||||
inquiry[36..43].copy_from_slice(b"NM00000");
|
||||
|
||||
let id = DriveId::from_inquiry(&inquiry, "211810241934");
|
||||
assert_eq!(id.vendor_id.trim(), "HL-DT-ST");
|
||||
assert_eq!(id.product_id.trim(), "BD-RE BU40N");
|
||||
assert_eq!(id.product_revision.trim(), "1.03");
|
||||
assert_eq!(id.vendor_specific.trim(), "NM00000");
|
||||
assert_eq!(id.firmware_date, "211810241934");
|
||||
assert_eq!(id.match_key(), "HL-DT-ST|BD-RE BU40N|1.03|NM00000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pioneer_identity() {
|
||||
let mut inquiry = vec![0u8; 96];
|
||||
inquiry[4] = 0x5B;
|
||||
inquiry[8..16].copy_from_slice(b"PIONEER ");
|
||||
inquiry[16..32].copy_from_slice(b"BD-RW BDR-S09 ");
|
||||
inquiry[32..36].copy_from_slice(b"1.34");
|
||||
inquiry[36..43].copy_from_slice(b" 16/04/");
|
||||
|
||||
let id = DriveId::from_inquiry(&inquiry, "201604250000");
|
||||
assert_eq!(id.vendor_id.trim(), "PIONEER");
|
||||
assert_eq!(id.product_id.trim(), "BD-RW BDR-S09");
|
||||
assert_eq!(id.product_revision.trim(), "1.34");
|
||||
assert_eq!(id.vendor_specific.trim(), "16/04/");
|
||||
assert_eq!(id.firmware_date, "201604250000");
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
//! libfreemkv — Open source raw disc access for optical drives.
|
||||
//!
|
||||
//! Provides SCSI/MMC commands to enable raw reading mode on compatible
|
||||
//! Blu-ray drives, allowing direct sector access for disc archival
|
||||
//! and backup purposes.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! The library is data-driven. Drive-specific SCSI command sequences
|
||||
//! are stored in profile files, not in code. Adding support for a new
|
||||
//! drive requires only a profile contribution — no rebuild needed.
|
||||
//!
|
||||
//! ```text
|
||||
//! DriveSession (high-level API)
|
||||
//! ├── Platform trait (per-chipset unlock logic)
|
||||
//! ├── DriveProfile (per-drive data from JSON profiles)
|
||||
//! └── ScsiTransport (SG_IO on Linux, IOKit on macOS)
|
||||
//! ```
|
||||
//!
|
||||
//! # Quick Start
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use libfreemkv::DriveSession;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! let mut session = DriveSession::open(
|
||||
//! Path::new("/dev/sr0"),
|
||||
//! Path::new("profiles/"),
|
||||
//! ).unwrap();
|
||||
//!
|
||||
//! session.enable().unwrap();
|
||||
//! session.calibrate().unwrap();
|
||||
//!
|
||||
//! let mut buf = vec![0u8; 2048];
|
||||
//! let n = session.read_sectors(0, 1, &mut buf).unwrap();
|
||||
//! ```
|
||||
|
||||
pub mod error;
|
||||
pub mod scsi;
|
||||
pub mod profile;
|
||||
pub mod platform;
|
||||
pub mod drive;
|
||||
pub mod identity;
|
||||
pub mod speed;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use drive::DriveSession;
|
||||
pub use identity::DriveId;
|
||||
pub use profile::{DriveProfile, PlatformType};
|
||||
pub use platform::{Platform, DriveStatus};
|
||||
pub use scsi::ScsiTransport;
|
||||
pub use speed::DriveSpeed;
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Platform-specific implementations of raw disc access commands.
|
||||
//!
|
||||
//! Each chipset family (MT1959, Pioneer) implements the Platform trait.
|
||||
//! accessed via SCSI READ BUFFER with platform-specific mode and buffer ID.
|
||||
|
||||
pub mod mt1959;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::scsi::ScsiTransport;
|
||||
|
||||
/// Platform trait — raw disc access commands implemented per chipset.
|
||||
///
|
||||
/// Command handlers accessed via READ BUFFER:
|
||||
pub trait Platform {
|
||||
///
|
||||
/// Sends the platform-specific READ BUFFER CDB and verifies
|
||||
/// the response signature bytes.
|
||||
fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
||||
|
||||
///
|
||||
/// Performs a primary READ BUFFER for the configuration data,
|
||||
/// followed by a secondary 4-byte status read.
|
||||
fn read_config(&mut self, scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>>;
|
||||
|
||||
/// Handlers 2-3: Read hardware register.
|
||||
///
|
||||
/// `index` selects which register offset from the profile to use.
|
||||
/// Returns 16 bytes of register data extracted from a 36-byte response.
|
||||
fn read_register(&mut self, scsi: &mut dyn ScsiTransport, index: u8) -> Result<[u8; 16]>;
|
||||
|
||||
///
|
||||
/// Probes the disc surface via READ BUFFER sub-commands to build
|
||||
/// a 64-entry speed lookup table for optimal read performance.
|
||||
/// Issues SET CD SPEED at maximum after calibration completes.
|
||||
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
||||
|
||||
///
|
||||
/// Periodic command to maintain the raw access session.
|
||||
fn keepalive(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
||||
|
||||
///
|
||||
/// Verifies the response signature and returns 16 bytes of
|
||||
/// feature/status data.
|
||||
fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result<DriveStatus>;
|
||||
|
||||
///
|
||||
/// Sends a READ BUFFER command with dynamic sub-command, address,
|
||||
/// and length. Used for disc structure reads and feature queries.
|
||||
fn probe(&mut self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>>;
|
||||
|
||||
///
|
||||
/// Looks up the LBA in the speed table, issues SET CD SPEED,
|
||||
/// then performs a READ(10) with the raw read flag (0x08).
|
||||
fn read_sectors(&mut self, scsi: &mut dyn ScsiTransport, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
|
||||
|
||||
fn timing(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
|
||||
|
||||
/// Check if raw disc access mode is currently enabled.
|
||||
fn is_unlocked(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DriveStatus {
|
||||
pub unlocked: bool,
|
||||
pub features: [u8; 16],
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
//! MT1959 platform implementation — covers all LG/ASUS MediaTek drives.
|
||||
//!
|
||||
//! Two variants share this code:
|
||||
//! MT1959-A: mode=0x01, buffer_id=0x44 (handlers 0-9)
|
||||
//! MT1959-B: mode=0x02, buffer_id=0x77 (handlers 4-9, 0-3 are no-ops)
|
||||
//!
|
||||
//! The logic is identical between A and B — only the SCSI READ BUFFER
|
||||
//! mode and buffer ID differ. Per-drive data (signature, register offsets)
|
||||
//! comes from the profile.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::profile::DriveProfile;
|
||||
use crate::scsi::{self, DataDirection, ScsiTransport};
|
||||
use super::{Platform, DriveStatus};
|
||||
|
||||
/// MT1959 driver state.
|
||||
pub struct Mt1959 {
|
||||
profile: DriveProfile,
|
||||
mode: u8,
|
||||
buffer_id: u8,
|
||||
unlocked: bool,
|
||||
speed_table: [u16; 64],
|
||||
calibrated: bool,
|
||||
}
|
||||
|
||||
impl Mt1959 {
|
||||
pub fn new(profile: DriveProfile) -> Self {
|
||||
let mode = profile.platform.mode();
|
||||
let buffer_id = profile.platform.buffer_id();
|
||||
Mt1959 {
|
||||
profile,
|
||||
mode,
|
||||
buffer_id,
|
||||
unlocked: false,
|
||||
speed_table: [0u16; 64],
|
||||
calibrated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a READ BUFFER CDB for this platform's mode and buffer ID.
|
||||
fn read_buffer_cdb(&self, offset: u32, length: u32) -> [u8; 10] {
|
||||
scsi::build_read_buffer(self.mode, self.buffer_id, offset, length)
|
||||
}
|
||||
|
||||
/// Build a READ BUFFER CDB with a sub-command byte in CDB[3].
|
||||
fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] {
|
||||
[
|
||||
0x3C,
|
||||
self.mode,
|
||||
self.buffer_id,
|
||||
sub_cmd,
|
||||
(address >> 8) as u8,
|
||||
address as u8,
|
||||
0x00,
|
||||
0x00,
|
||||
length,
|
||||
0x00,
|
||||
]
|
||||
}
|
||||
|
||||
///
|
||||
/// 1. Send READ BUFFER(mode, buffer_id, offset=0, length=64)
|
||||
/// 2. Check response[0:4] matches the profile signature
|
||||
/// 3. Check response[12:16] matches the verification bytes (0x4D4D6B76)
|
||||
fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<[u8; 64]> {
|
||||
let cdb = self.read_buffer_cdb(0, 64);
|
||||
let mut response = [0u8; 64];
|
||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
||||
|
||||
// Check signature at response[0:4]
|
||||
let got_sig: [u8; 4] = response[0..4].try_into().unwrap();
|
||||
if got_sig != self.profile.signature {
|
||||
return Err(Error::SignatureMismatch {
|
||||
expected: self.profile.signature,
|
||||
got: got_sig,
|
||||
});
|
||||
}
|
||||
|
||||
// Check verification bytes at response[12:16]
|
||||
if &response[12..16] != self.profile.verify.as_slice() {
|
||||
return Err(Error::UnlockFailed(format!(
|
||||
"verify mismatch at [12:16]: {:02x}{:02x}{:02x}{:02x}",
|
||||
response[12], response[13], response[14], response[15]
|
||||
)));
|
||||
}
|
||||
|
||||
self.unlocked = true;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Ensure raw disc access is active, re-enabling if needed.
|
||||
fn ensure_unlocked(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
if !self.unlocked {
|
||||
self.do_unlock(scsi)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pre-operation validation with retry.
|
||||
///
|
||||
/// Sends a short READ BUFFER probe, retries up to 5 times to confirm
|
||||
/// the drive is still responding to commands.
|
||||
fn validate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
for _attempt in 0..5 {
|
||||
let cdb = self.read_buffer_cdb(0, 4);
|
||||
let mut resp = [0u8; 4];
|
||||
match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
Err(Error::ScsiError {
|
||||
cdb: vec![0x3C],
|
||||
status: 0xFF,
|
||||
sense: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up optimal read speed for a given LBA from the calibration table.
|
||||
fn lookup_speed(&self, lba: u32) -> u16 {
|
||||
if !self.calibrated {
|
||||
return 0;
|
||||
}
|
||||
let mut best_speed = 0u16;
|
||||
let mut best_diff = u32::MAX;
|
||||
for &entry in &self.speed_table {
|
||||
if entry == 0 {
|
||||
continue;
|
||||
}
|
||||
let entry_lba = entry as u32;
|
||||
let diff = if lba > entry_lba { lba - entry_lba } else { entry_lba - lba };
|
||||
if diff < best_diff {
|
||||
best_diff = diff;
|
||||
best_speed = entry;
|
||||
}
|
||||
}
|
||||
best_speed
|
||||
}
|
||||
|
||||
/// Send SET CD SPEED command.
|
||||
fn set_cd_speed(&self, scsi: &mut dyn ScsiTransport, speed: u16) -> Result<()> {
|
||||
let cdb = scsi::build_set_cd_speed(speed);
|
||||
let mut dummy = [0u8; 0];
|
||||
scsi.execute(&cdb, DataDirection::None, &mut dummy, 5_000)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for Mt1959 {
|
||||
fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
self.do_unlock(scsi)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
/// Primary read: 0x760 (1888) bytes of configuration data.
|
||||
/// Secondary read: 4-byte status appended to the result.
|
||||
fn read_config(&mut self, scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
|
||||
// Primary config read: 0x760 = 1888 bytes
|
||||
let cdb = self.read_buffer_cdb(0, 0x760);
|
||||
let mut buf = vec![0u8; 0x760];
|
||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?;
|
||||
buf.truncate(result.bytes_transferred);
|
||||
|
||||
// Secondary: 4-byte status read
|
||||
let cdb2 = self.read_buffer_cdb(0, 4);
|
||||
let mut status = [0u8; 4];
|
||||
scsi.execute(&cdb2, DataDirection::FromDevice, &mut status, 5_000)?;
|
||||
|
||||
buf.extend_from_slice(&status);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Handlers 2-3: Read hardware register at the profile-specified offset.
|
||||
///
|
||||
/// Reads 36 bytes via READ BUFFER and extracts bytes [4:20] as the
|
||||
/// 16-byte register value.
|
||||
fn read_register(&mut self, scsi: &mut dyn ScsiTransport, index: u8) -> Result<[u8; 16]> {
|
||||
self.ensure_unlocked(scsi)?;
|
||||
self.validate(scsi)?;
|
||||
|
||||
let offset = *self.profile.register_offsets.get(index as usize)
|
||||
.ok_or_else(|| Error::ScsiError {
|
||||
cdb: vec![],
|
||||
status: 0,
|
||||
sense: vec![],
|
||||
})?;
|
||||
|
||||
let cdb = scsi::build_read_buffer(self.mode, self.buffer_id, offset, 36);
|
||||
let mut response = [0u8; 36];
|
||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
||||
|
||||
let mut out = [0u8; 16];
|
||||
out.copy_from_slice(&response[4..20]);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
///
|
||||
/// Scans disc surface addresses via READ BUFFER sub-command 0x14 to
|
||||
/// build a 64-entry speed lookup table. Issues SET CD SPEED(max) when done.
|
||||
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
self.ensure_unlocked(scsi)?;
|
||||
self.validate(scsi)?;
|
||||
|
||||
// Initial probe: READ BUFFER sub_cmd=0x12
|
||||
let cdb = self.read_buffer_sub(0x12, 0, 4);
|
||||
let mut resp = [0u8; 4];
|
||||
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000);
|
||||
|
||||
self.validate(scsi)?;
|
||||
|
||||
// Clear speed table
|
||||
self.speed_table = [0u16; 64];
|
||||
|
||||
// Scan disc surface — probe addresses up to 0x10000, 256 at a time
|
||||
let mut table_idx = 0usize;
|
||||
let mut addr: u32 = 0;
|
||||
while addr < 0x10000 && table_idx < 64 {
|
||||
let cdb = self.read_buffer_sub(0x14, addr as u16, 4);
|
||||
let mut resp = [0u8; 4];
|
||||
match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) {
|
||||
Ok(r) if r.bytes_transferred == 4 => {
|
||||
let val = resp[0];
|
||||
if val > 0 {
|
||||
let speed_entry = ((resp[0] as u16) << 8) | (resp[1] as u16);
|
||||
if speed_entry > 0 {
|
||||
self.speed_table[table_idx] = speed_entry;
|
||||
table_idx += 1;
|
||||
}
|
||||
}
|
||||
addr += 256;
|
||||
}
|
||||
_ => {
|
||||
addr += 256;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set max speed after calibration
|
||||
self.set_cd_speed(scsi, 0xFFFF)?;
|
||||
|
||||
self.calibrated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keepalive(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
/// Sends READ BUFFER with sub-command 0x13, reads 36 bytes.
|
||||
/// Checks signature at [0:4], returns feature data from [4:20].
|
||||
fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result<DriveStatus> {
|
||||
self.ensure_unlocked(scsi)?;
|
||||
self.validate(scsi)?;
|
||||
|
||||
// READ BUFFER with sub_cmd=0x13, 36 bytes response
|
||||
let cdb = self.read_buffer_sub(0x13, 0, 36);
|
||||
let mut response = [0u8; 36];
|
||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
|
||||
|
||||
// Verify response signature
|
||||
let got_sig = u32::from_be_bytes(response[0..4].try_into().unwrap());
|
||||
let expected_sig = u32::from_le_bytes(self.profile.signature);
|
||||
|
||||
let mut features = [0u8; 16];
|
||||
features.copy_from_slice(&response[4..20]);
|
||||
|
||||
Ok(DriveStatus {
|
||||
unlocked: got_sig == expected_sig,
|
||||
features,
|
||||
})
|
||||
}
|
||||
|
||||
fn probe(&mut self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>> {
|
||||
let cdb = [
|
||||
0x3C,
|
||||
self.mode,
|
||||
self.buffer_id,
|
||||
sub_cmd,
|
||||
(address >> 16) as u8,
|
||||
(address >> 8) as u8,
|
||||
address as u8,
|
||||
(length >> 16) as u8,
|
||||
(length >> 8) as u8,
|
||||
length as u8,
|
||||
];
|
||||
let mut buf = vec![0u8; length as usize];
|
||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?;
|
||||
buf.truncate(result.bytes_transferred);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
///
|
||||
/// Looks up the LBA in the speed table, issues SET CD SPEED if calibrated,
|
||||
/// then performs READ(10) with the raw read flag (0x08).
|
||||
fn read_sectors(
|
||||
&mut self,
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
lba: u32,
|
||||
count: u16,
|
||||
buf: &mut [u8],
|
||||
) -> Result<usize> {
|
||||
if !self.unlocked {
|
||||
return Err(Error::NotUnlocked);
|
||||
}
|
||||
|
||||
// Speed optimization from calibration
|
||||
if self.calibrated {
|
||||
let speed = self.lookup_speed(lba);
|
||||
if speed > 0 {
|
||||
let _ = self.set_cd_speed(scsi, speed);
|
||||
}
|
||||
}
|
||||
|
||||
// READ(10) with raw flag 0x08
|
||||
let cdb = scsi::build_read10_raw(lba, count);
|
||||
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 30_000)?;
|
||||
Ok(result.bytes_transferred)
|
||||
}
|
||||
|
||||
fn timing(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_unlocked(&self) -> bool {
|
||||
self.unlocked
|
||||
}
|
||||
}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
//! Drive profile loading and matching.
|
||||
//!
|
||||
//! Each supported drive has a profile containing the SCSI command
|
||||
//! parameters needed to enable raw disc access mode. Profiles are
|
||||
//! loaded from JSON files so new drives can be added without rebuilding.
|
||||
|
||||
use serde::Deserialize;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Per-drive profile containing SCSI parameters for raw disc access.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DriveProfile {
|
||||
/// Drive vendor from INQUIRY[8:16] (e.g. "HL-DT-ST")
|
||||
#[serde(default)]
|
||||
pub vendor_id: String,
|
||||
|
||||
/// Drive product (devtype) from INQUIRY product field (e.g. "BD-RE")
|
||||
#[serde(default)]
|
||||
pub product_id: String,
|
||||
|
||||
/// Firmware revision from INQUIRY[32:36] (e.g. "1.03")
|
||||
#[serde(default)]
|
||||
pub product_revision: String,
|
||||
|
||||
/// Firmware type from INQUIRY[36:43] (e.g. "NM00000")
|
||||
#[serde(default)]
|
||||
pub vendor_specific: String,
|
||||
|
||||
/// Firmware build date from GET_CONFIG 010C (e.g. "211810241934")
|
||||
#[serde(default)]
|
||||
pub firmware_date: String,
|
||||
|
||||
/// Chipset platform type determining the READ BUFFER variant.
|
||||
#[serde(default)]
|
||||
pub platform: PlatformType,
|
||||
|
||||
/// Whether this drive supports raw disc access mode.
|
||||
#[serde(default)]
|
||||
pub supported: bool,
|
||||
|
||||
/// Current readiness status of this drive.
|
||||
#[serde(default)]
|
||||
pub status: ReadinessStatus,
|
||||
|
||||
/// Drive identifier string from the profile database.
|
||||
#[serde(default)]
|
||||
pub drive_id: String,
|
||||
|
||||
/// Profile version string.
|
||||
#[serde(default)]
|
||||
pub drive_version: String,
|
||||
|
||||
/// Expected response signature bytes [0:4] from the enable command.
|
||||
#[serde(default, deserialize_with = "deserialize_hex4")]
|
||||
pub signature: [u8; 4],
|
||||
|
||||
/// Expected verification bytes [12:16] from the enable response.
|
||||
#[serde(skip, default = "default_verify")]
|
||||
pub verify: [u8; 4],
|
||||
|
||||
/// 10-byte READ BUFFER CDB used to enable raw disc access.
|
||||
#[serde(default, deserialize_with = "deserialize_hex_vec")]
|
||||
pub unlock_cdb: Vec<u8>,
|
||||
|
||||
/// Register read offsets (bytes 3-5 of READ BUFFER CDB).
|
||||
#[serde(default)]
|
||||
pub register_offsets: Vec<u32>,
|
||||
|
||||
/// Drive supports reading DVDs regardless of region code.
|
||||
#[serde(default)]
|
||||
pub dvd_all_regions: bool,
|
||||
|
||||
/// Drive supports raw Blu-ray sector reads.
|
||||
#[serde(default)]
|
||||
pub bd_raw_read: bool,
|
||||
|
||||
/// Drive supports raw Blu-ray metadata reads.
|
||||
#[serde(default)]
|
||||
pub bd_raw_metadata: bool,
|
||||
|
||||
/// Drive supports unrestricted read speed.
|
||||
#[serde(default)]
|
||||
pub unrestricted_speed: bool,
|
||||
}
|
||||
|
||||
fn default_verify() -> [u8; 4] {
|
||||
*b"MMkv"
|
||||
}
|
||||
|
||||
/// Chipset platform type. Determines the READ BUFFER mode and buffer ID.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
|
||||
pub enum PlatformType {
|
||||
/// MediaTek MT1959 variant A: mode=0x01, buffer_id=0x44.
|
||||
#[serde(rename = "mt1959_a")]
|
||||
Mt1959A,
|
||||
/// MediaTek MT1959 variant B: mode=0x02, buffer_id=0x77.
|
||||
#[serde(rename = "mt1959_b")]
|
||||
Mt1959B,
|
||||
/// Pioneer chipset (not yet implemented).
|
||||
#[serde(rename = "pioneer")]
|
||||
Pioneer,
|
||||
}
|
||||
|
||||
impl Default for PlatformType {
|
||||
fn default() -> Self {
|
||||
PlatformType::Mt1959A
|
||||
}
|
||||
}
|
||||
|
||||
/// Readiness status of a drive for raw disc access.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
|
||||
pub enum ReadinessStatus {
|
||||
/// Drive is ready — raw disc access can be enabled.
|
||||
Ready,
|
||||
/// Drive firmware needs an update before raw access is possible.
|
||||
NeedsFirmwareUpdate,
|
||||
/// Drive uses encrypted commands (not yet supported).
|
||||
Encrypted,
|
||||
/// Status unknown.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Default for ReadinessStatus {
|
||||
fn default() -> Self {
|
||||
ReadinessStatus::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformType {
|
||||
/// Human-readable name for this platform.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
PlatformType::Mt1959A => "MT1959-A",
|
||||
PlatformType::Mt1959B => "MT1959-B",
|
||||
PlatformType::Pioneer => "Pioneer",
|
||||
}
|
||||
}
|
||||
|
||||
/// READ BUFFER mode byte for this chipset platform.
|
||||
pub fn mode(&self) -> u8 {
|
||||
match self {
|
||||
PlatformType::Mt1959A => 0x01,
|
||||
PlatformType::Mt1959B => 0x02,
|
||||
PlatformType::Pioneer => 0x01, // TBD
|
||||
}
|
||||
}
|
||||
|
||||
/// READ BUFFER buffer ID for this chipset platform.
|
||||
pub fn buffer_id(&self) -> u8 {
|
||||
match self {
|
||||
PlatformType::Mt1959A => 0x44,
|
||||
PlatformType::Mt1959B => 0x77,
|
||||
PlatformType::Pioneer => 0x44, // TBD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a hex string like "999ec375" into [u8; 4].
|
||||
fn parse_hex4(s: &str) -> Result<[u8; 4]> {
|
||||
if s.len() != 8 {
|
||||
return Err(Error::ProfileParse(format!("expected 8 hex chars, got {}", s.len())));
|
||||
}
|
||||
let mut out = [0u8; 4];
|
||||
for i in 0..4 {
|
||||
out[i] = u8::from_str_radix(&s[i*2..i*2+2], 16)
|
||||
.map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Parse a hex string into a byte vector.
|
||||
fn parse_hex(s: &str) -> Result<Vec<u8>> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err(Error::ProfileParse("odd hex length".into()));
|
||||
}
|
||||
let mut out = Vec::with_capacity(s.len() / 2);
|
||||
for i in (0..s.len()).step_by(2) {
|
||||
out.push(u8::from_str_radix(&s[i..i+2], 16)
|
||||
.map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Custom serde deserializer for 4-byte hex signature strings.
|
||||
fn deserialize_hex4<'de, D>(deserializer: D) -> std::result::Result<[u8; 4], D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
parse_hex4(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Custom serde deserializer for hex-encoded byte vectors.
|
||||
fn deserialize_hex_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
parse_hex(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Load a profile from a parsed JSON value.
|
||||
pub fn load_from_json(json: &serde_json::Value) -> Result<DriveProfile> {
|
||||
let vendor = json["vendor_id"].as_str().unwrap_or("").to_string();
|
||||
let product = json["product_id"].as_str().unwrap_or("").to_string();
|
||||
let revision = json["product_revision"].as_str().unwrap_or("").to_string();
|
||||
let firmware_type = json["vendor_specific"].as_str().unwrap_or("").to_string();
|
||||
let firmware_date = json["firmware_date"].as_str().unwrap_or("").to_string();
|
||||
let program = json["program"].as_str().unwrap_or("unknown");
|
||||
|
||||
let platform = match program {
|
||||
"mt1959_a" => PlatformType::Mt1959A,
|
||||
"mt1959_b" => PlatformType::Mt1959B,
|
||||
_ => PlatformType::Mt1959A, // default
|
||||
};
|
||||
|
||||
let sig_str = json["signature"].as_str().unwrap_or("");
|
||||
|
||||
// Drive is supported if it has a known program and valid signature
|
||||
let has_program = matches!(program, "mt1959_a" | "mt1959_b");
|
||||
let has_signature = sig_str.len() == 8;
|
||||
let supported = has_program && has_signature;
|
||||
|
||||
let status = if supported {
|
||||
ReadinessStatus::Ready
|
||||
} else if json["status"].as_str() == Some("needs_flash") || program == "none" {
|
||||
ReadinessStatus::NeedsFirmwareUpdate
|
||||
} else {
|
||||
ReadinessStatus::Unknown
|
||||
};
|
||||
let signature = if sig_str.len() == 8 {
|
||||
parse_hex4(sig_str)?
|
||||
} else {
|
||||
[0; 4]
|
||||
};
|
||||
|
||||
let unlock_cdb = json["unlock_cdb"].as_str()
|
||||
.map(|s| parse_hex(s))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let register_offsets = json["register_cdbs"].as_array()
|
||||
.map(|arr| {
|
||||
arr.iter().filter_map(|v| {
|
||||
let s = v.as_str()?;
|
||||
// CDB format: 3c 01 44 XX XX XX 00 00 24 00
|
||||
// Register offset is bytes 3-5 (chars 6-12 in hex)
|
||||
if s.len() >= 12 {
|
||||
u32::from_str_radix(&s[6..12], 16).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(DriveProfile {
|
||||
vendor_id: vendor,
|
||||
product_id: product,
|
||||
product_revision: revision,
|
||||
vendor_specific: firmware_type,
|
||||
firmware_date,
|
||||
platform,
|
||||
supported,
|
||||
status,
|
||||
drive_id: json["drive_id"].as_str().unwrap_or("").to_string(),
|
||||
drive_version: json["drive_version"].as_str().unwrap_or("").to_string(),
|
||||
signature,
|
||||
verify: *b"MMkv",
|
||||
unlock_cdb,
|
||||
register_offsets,
|
||||
dvd_all_regions: json["capabilities"]["dvd_all_regions"].as_bool().unwrap_or(false),
|
||||
bd_raw_read: json["capabilities"]["bd_raw_read"].as_bool().unwrap_or(false),
|
||||
bd_raw_metadata: json["capabilities"]["bd_raw_metadata"].as_bool().unwrap_or(false),
|
||||
unrestricted_speed: json["capabilities"]["unrestricted_speed"].as_bool().unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Bundled profiles — compiled into the binary.
|
||||
/// Override with load_all() to load from a file instead.
|
||||
const BUNDLED_PROFILES: &str = include_str!("../profiles.json");
|
||||
|
||||
/// Load profiles from the bundled database.
|
||||
pub fn load_bundled() -> Result<Vec<DriveProfile>> {
|
||||
load_from_str(BUNDLED_PROFILES)
|
||||
}
|
||||
|
||||
/// Load all profiles from a JSON array file.
|
||||
pub fn load_all(path: &std::path::Path) -> Result<Vec<DriveProfile>> {
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
load_from_str(&data)
|
||||
}
|
||||
|
||||
/// Parse profiles from a JSON string.
|
||||
fn load_from_str(data: &str) -> Result<Vec<DriveProfile>> {
|
||||
let json: serde_json::Value = serde_json::from_str(data)
|
||||
.map_err(|e| Error::ProfileParse(format!("JSON: {e}")))?;
|
||||
|
||||
let arr = json.as_array()
|
||||
.ok_or_else(|| Error::ProfileParse("expected array".into()))?;
|
||||
|
||||
let mut profiles = Vec::with_capacity(arr.len());
|
||||
for entry in arr {
|
||||
match load_from_json(entry) {
|
||||
Ok(p) => profiles.push(p),
|
||||
Err(_) => continue, // skip malformed entries
|
||||
}
|
||||
}
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// Find a profile matching a drive's INQUIRY fields.
|
||||
///
|
||||
/// Matches by vendor + product + revision + vendor_specific (firmware type).
|
||||
/// All fields trimmed before comparison.
|
||||
pub fn find_by_drive_id<'a>(
|
||||
profiles: &'a [DriveProfile],
|
||||
drive_id: &crate::identity::DriveId,
|
||||
) -> Option<&'a DriveProfile> {
|
||||
let v = drive_id.vendor_id.trim();
|
||||
let r = drive_id.product_revision.trim();
|
||||
let vs = drive_id.vendor_specific.trim();
|
||||
|
||||
// Match all four INQUIRY fields for precise identification
|
||||
profiles.iter().find(|p| {
|
||||
p.vendor_id.trim() == v
|
||||
&& p.product_revision.trim() == r
|
||||
&& p.vendor_specific.trim() == vs
|
||||
&& p.firmware_date.trim() == drive_id.firmware_date.trim()
|
||||
})
|
||||
// Fallback: match without date (for drives where 010C isn't available)
|
||||
.or_else(|| profiles.iter().find(|p| {
|
||||
p.vendor_id.trim() == v
|
||||
&& p.product_revision.trim() == r
|
||||
&& p.vendor_specific.trim() == vs
|
||||
}))
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
//! SCSI/MMC command interface via Linux SG_IO.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum DataDirection {
|
||||
None,
|
||||
FromDevice,
|
||||
ToDevice,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScsiResult {
|
||||
pub status: u8,
|
||||
pub bytes_transferred: usize,
|
||||
pub sense: [u8; 32],
|
||||
}
|
||||
|
||||
/// Low-level SCSI transport.
|
||||
pub trait ScsiTransport {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
direction: DataDirection,
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> Result<ScsiResult>;
|
||||
}
|
||||
|
||||
/// Linux SG_IO transport.
|
||||
pub struct SgIoTransport {
|
||||
fd: i32,
|
||||
}
|
||||
|
||||
// SG_IO constants
|
||||
const SG_IO: libc::c_ulong = 0x2285;
|
||||
const SG_DXFER_NONE: i32 = -1;
|
||||
const SG_DXFER_TO_DEV: i32 = -2;
|
||||
const SG_DXFER_FROM_DEV: i32 = -3;
|
||||
|
||||
#[repr(C)]
|
||||
#[allow(non_camel_case_types)]
|
||||
struct sg_io_hdr {
|
||||
interface_id: i32,
|
||||
dxfer_direction: i32,
|
||||
cmd_len: u8,
|
||||
mx_sb_len: u8,
|
||||
iovec_count: u16,
|
||||
dxfer_len: u32,
|
||||
dxferp: *mut u8,
|
||||
cmdp: *const u8,
|
||||
sbp: *mut u8,
|
||||
timeout: u32,
|
||||
flags: u32,
|
||||
pack_id: i32,
|
||||
usr_ptr: *mut libc::c_void,
|
||||
status: u8,
|
||||
masked_status: u8,
|
||||
msg_status: u8,
|
||||
sb_len_wr: u8,
|
||||
host_status: u16,
|
||||
driver_status: u16,
|
||||
resid: i32,
|
||||
duration: u32,
|
||||
info: u32,
|
||||
}
|
||||
|
||||
impl SgIoTransport {
|
||||
pub fn open(device: &Path) -> Result<Self> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let path_bytes = device.as_os_str().as_bytes();
|
||||
let mut c_path = Vec::with_capacity(path_bytes.len() + 1);
|
||||
c_path.extend_from_slice(path_bytes);
|
||||
c_path.push(0);
|
||||
|
||||
let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) };
|
||||
if fd < 0 {
|
||||
return Err(Error::DeviceNotFound(device.display().to_string()));
|
||||
}
|
||||
Ok(SgIoTransport { fd })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SgIoTransport {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::close(self.fd); }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScsiTransport for SgIoTransport {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
direction: DataDirection,
|
||||
data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> Result<ScsiResult> {
|
||||
let mut sense = [0u8; 32];
|
||||
|
||||
let dxfer_direction = match direction {
|
||||
DataDirection::None => SG_DXFER_NONE,
|
||||
DataDirection::FromDevice => SG_DXFER_FROM_DEV,
|
||||
DataDirection::ToDevice => SG_DXFER_TO_DEV,
|
||||
};
|
||||
|
||||
let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() };
|
||||
hdr.interface_id = b'S' as i32;
|
||||
hdr.dxfer_direction = dxfer_direction;
|
||||
hdr.cmd_len = cdb.len() as u8;
|
||||
hdr.mx_sb_len = sense.len() as u8;
|
||||
hdr.dxfer_len = data.len() as u32;
|
||||
hdr.dxferp = data.as_mut_ptr();
|
||||
hdr.cmdp = cdb.as_ptr();
|
||||
hdr.sbp = sense.as_mut_ptr();
|
||||
hdr.timeout = timeout_ms;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::ioctl(self.fd, SG_IO, &mut hdr as *mut sg_io_hdr)
|
||||
};
|
||||
|
||||
if ret < 0 {
|
||||
return Err(Error::Io(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
let bytes_transferred = (data.len() as i32 - hdr.resid) as usize;
|
||||
|
||||
if hdr.status != 0 {
|
||||
return Err(Error::ScsiError {
|
||||
cdb: cdb.to_vec(),
|
||||
status: hdr.status,
|
||||
sense: sense[..hdr.sb_len_wr as usize].to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ScsiResult {
|
||||
status: hdr.status,
|
||||
bytes_transferred,
|
||||
sense,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// SCSI INQUIRY response.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InquiryResult {
|
||||
pub vendor_id: String,
|
||||
pub model: String,
|
||||
pub firmware: String,
|
||||
pub raw: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Send INQUIRY command and parse the standard response fields.
|
||||
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
|
||||
let cdb = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00];
|
||||
let mut buf = [0u8; 96];
|
||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
||||
|
||||
let vendor = String::from_utf8_lossy(&buf[8..16]).trim().to_string();
|
||||
let model = String::from_utf8_lossy(&buf[16..32]).trim().to_string();
|
||||
let firmware = String::from_utf8_lossy(&buf[32..36]).trim().to_string();
|
||||
|
||||
Ok(InquiryResult {
|
||||
vendor_id: vendor,
|
||||
model,
|
||||
firmware,
|
||||
raw: buf.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send GET CONFIGURATION for feature 0x010C (drive serial number).
|
||||
pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
|
||||
let cdb = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00];
|
||||
let mut buf = [0u8; 16];
|
||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
||||
Ok(buf.to_vec())
|
||||
}
|
||||
|
||||
/// Build a READ BUFFER (0x3C) CDB with the given mode, buffer ID, offset, and length.
|
||||
pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] {
|
||||
[
|
||||
0x3C, // READ BUFFER
|
||||
mode,
|
||||
buffer_id,
|
||||
(offset >> 16) as u8,
|
||||
(offset >> 8) as u8,
|
||||
offset as u8,
|
||||
(length >> 16) as u8,
|
||||
(length >> 8) as u8,
|
||||
length as u8,
|
||||
0x00,
|
||||
]
|
||||
}
|
||||
|
||||
/// Build a SET CD SPEED (0xBB) CDB with the given read speed.
|
||||
pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
|
||||
[
|
||||
0xBB, 0x00,
|
||||
(read_speed >> 8) as u8, read_speed as u8,
|
||||
0xFF, 0xFF, // write speed = max
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]
|
||||
}
|
||||
|
||||
/// Build a READ(10) CDB with the raw read flag (0x08) set.
|
||||
pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] {
|
||||
[
|
||||
0x28, 0x08, // READ(10), flag=0x08 (raw)
|
||||
(lba >> 24) as u8, (lba >> 16) as u8,
|
||||
(lba >> 8) as u8, lba as u8,
|
||||
0x00,
|
||||
(count >> 8) as u8, count as u8,
|
||||
0x00,
|
||||
]
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
//! Drive speed control — query and set read speeds.
|
||||
//!
|
||||
//! Uses MMC-6 SET CD SPEED (0xBB) command.
|
||||
//! Reference: MMC-6 §6.30
|
||||
|
||||
/// Disc read speed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum DriveSpeed {
|
||||
/// Blu-ray 1x = 4,500 KB/s
|
||||
BD1x,
|
||||
/// Blu-ray 2x = 9,000 KB/s
|
||||
BD2x,
|
||||
/// Blu-ray 4x = 18,000 KB/s
|
||||
BD4x,
|
||||
/// Blu-ray 6x = 27,000 KB/s
|
||||
BD6x,
|
||||
/// Blu-ray 8x = 36,000 KB/s
|
||||
BD8x,
|
||||
/// Blu-ray 10x = 45,000 KB/s
|
||||
BD10x,
|
||||
/// Blu-ray 12x = 54,000 KB/s
|
||||
BD12x,
|
||||
/// DVD 1x = 1,385 KB/s
|
||||
DVD1x,
|
||||
/// DVD 2x = 2,770 KB/s
|
||||
DVD2x,
|
||||
/// DVD 4x = 5,540 KB/s
|
||||
DVD4x,
|
||||
/// DVD 8x = 11,080 KB/s
|
||||
DVD8x,
|
||||
/// DVD 16x = 22,160 KB/s
|
||||
DVD16x,
|
||||
/// Maximum speed — drive decides
|
||||
Max,
|
||||
}
|
||||
|
||||
impl DriveSpeed {
|
||||
/// Convert to KB/s for MMC-6 SET CD SPEED command.
|
||||
pub fn to_kbps(self) -> u16 {
|
||||
match self {
|
||||
DriveSpeed::BD1x => 4_500,
|
||||
DriveSpeed::BD2x => 9_000,
|
||||
DriveSpeed::BD4x => 18_000,
|
||||
DriveSpeed::BD6x => 27_000,
|
||||
DriveSpeed::BD8x => 36_000,
|
||||
DriveSpeed::BD10x => 45_000,
|
||||
DriveSpeed::BD12x => 54_000,
|
||||
DriveSpeed::DVD1x => 1_385,
|
||||
DriveSpeed::DVD2x => 2_770,
|
||||
DriveSpeed::DVD4x => 5_540,
|
||||
DriveSpeed::DVD8x => 11_080,
|
||||
DriveSpeed::DVD16x => 22_160,
|
||||
DriveSpeed::Max => 0xFFFF,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from KB/s value, rounding to nearest standard speed.
|
||||
pub fn from_kbps(kbps: u16) -> Self {
|
||||
match kbps {
|
||||
0..=2_000 => DriveSpeed::DVD1x,
|
||||
2_001..=4_000 => DriveSpeed::DVD2x,
|
||||
4_001..=6_000 => DriveSpeed::BD1x,
|
||||
6_001..=13_000 => DriveSpeed::BD2x,
|
||||
13_001..=22_000 => DriveSpeed::BD4x,
|
||||
22_001..=31_000 => DriveSpeed::BD6x,
|
||||
31_001..=40_000 => DriveSpeed::BD8x,
|
||||
40_001..=49_000 => DriveSpeed::BD10x,
|
||||
49_001..=0xFFFE => DriveSpeed::BD12x,
|
||||
0xFFFF => DriveSpeed::Max,
|
||||
_ => DriveSpeed::Max,
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable label.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
DriveSpeed::BD1x => "BD 1x",
|
||||
DriveSpeed::BD2x => "BD 2x",
|
||||
DriveSpeed::BD4x => "BD 4x",
|
||||
DriveSpeed::BD6x => "BD 6x",
|
||||
DriveSpeed::BD8x => "BD 8x",
|
||||
DriveSpeed::BD10x => "BD 10x",
|
||||
DriveSpeed::BD12x => "BD 12x",
|
||||
DriveSpeed::DVD1x => "DVD 1x",
|
||||
DriveSpeed::DVD2x => "DVD 2x",
|
||||
DriveSpeed::DVD4x => "DVD 4x",
|
||||
DriveSpeed::DVD8x => "DVD 8x",
|
||||
DriveSpeed::DVD16x => "DVD 16x",
|
||||
DriveSpeed::Max => "Max",
|
||||
}
|
||||
}
|
||||
|
||||
/// All standard Blu-ray speeds.
|
||||
pub fn all_bd() -> &'static [DriveSpeed] {
|
||||
&[DriveSpeed::BD1x, DriveSpeed::BD2x, DriveSpeed::BD4x,
|
||||
DriveSpeed::BD6x, DriveSpeed::BD8x, DriveSpeed::BD10x, DriveSpeed::BD12x]
|
||||
}
|
||||
|
||||
/// All standard DVD speeds.
|
||||
pub fn all_dvd() -> &'static [DriveSpeed] {
|
||||
&[DriveSpeed::DVD1x, DriveSpeed::DVD2x, DriveSpeed::DVD4x,
|
||||
DriveSpeed::DVD8x, DriveSpeed::DVD16x]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DriveSpeed {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} ({} KB/s)", self.label(), self.to_kbps())
|
||||
}
|
||||
}
|
||||
|
||||
/// Build SET CD SPEED CDB — MMC-6 §6.30
|
||||
pub fn set_cd_speed_cdb(read_speed: DriveSpeed) -> [u8; 12] {
|
||||
let kbps = read_speed.to_kbps();
|
||||
[
|
||||
0xBB, // SET CD SPEED opcode
|
||||
0x00, // reserved
|
||||
(kbps >> 8) as u8, // read speed MSB
|
||||
kbps as u8, // read speed LSB
|
||||
0xFF, // write speed MSB (0xFFFF = don't change)
|
||||
0xFF, // write speed LSB
|
||||
0x00, 0x00, 0x00, 0x00, // reserved
|
||||
0x00, 0x00, // reserved
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user