API: Drive object, typed StreamUrl, tray lock/unlock, Send traits

- Rename DriveSession → Drive across entire codebase
- find_drives() returns Vec<Drive>, find_drive() returns Option<Drive>
- resolve_device() now pub(crate) — internal only
- StreamUrl is now a typed enum (Disc, Mkv, M2ts, Iso, Network, Stdio, Null)
  with scheme() and path_str() accessors, replacing struct of Strings
- Add lock_tray() / unlock_tray() for safe disc access during rips
- Improve reset() with eject cycle that clears LibreDrive stuck state
- Add Send bounds to ScsiTransport and PlatformDriver traits
- DiscOptions uses PathBuf instead of String for device/keydb paths
- Update doc example to use new Drive API
This commit is contained in:
MattJackson
2026-04-13 00:13:41 +00:00
parent c3d7a7e867
commit 5467d8e69e
13 changed files with 254 additions and 201 deletions
+78 -36
View File
@@ -40,7 +40,7 @@ pub enum DriveStatus {
}
/// Optical disc drive session -- open, identify, unlock, and read.
pub struct DriveSession {
pub struct Drive {
scsi: Box<dyn ScsiTransport>,
driver: Option<Box<dyn PlatformDriver>>,
pub profile: Option<DriveProfile>,
@@ -49,7 +49,7 @@ pub struct DriveSession {
device_path: String,
}
impl DriveSession {
impl Drive {
pub fn open(device: &Path) -> Result<Self> {
let mut transport = crate::scsi::open(device)?;
let profiles = profile::load_bundled()?;
@@ -65,7 +65,7 @@ impl DriveSession {
None => (None, None, None),
};
Ok(DriveSession {
Ok(Drive {
scsi: transport,
driver,
platform,
@@ -159,47 +159,66 @@ impl DriveSession {
}
/// Attempt to reset the drive to a clean state.
/// Tries multiple approaches in order:
/// 1. PREVENT ALLOW MEDIUM REMOVAL (allow) — clears command locks
/// 2. START STOP UNIT (start)restarts the disc
/// 3. If the drive has a profile, re-init (firmware re-upload + unlock)
///
/// Escalates through increasingly aggressive recovery:
/// 1. Unlock tray + stop/start — handles normal stuck states
/// 2. Eject cycle — clears LibreDrive firmware stuck state (proven on BU40N)
/// 3. Re-init — firmware re-upload if profile available
///
/// Note: step 2 physically ejects the tray. On slimline drives the user
/// must push it back in manually. Returns Ok(()) if TUR succeeds after
/// any step, even if the drive reports "tray open" (that's a valid state).
pub fn reset(&mut self) -> Result<()> {
let mut buf = [0u8; 0];
let tur = [0x00u8, 0x00, 0x00, 0x00, 0x00, 0x00];
// 1. Allow medium removal (clears any prevent lock)
let allow = [0x1Eu8, 0x00, 0x00, 0x00, 0x00, 0x00];
let _ = self.scsi.as_mut().execute(
&allow, crate::scsi::DataDirection::None, &mut buf, 5_000,
);
// 2. START STOP UNIT: stop then start (forces disc re-spin)
let stop = [0x1Bu8, 0x00, 0x00, 0x00, 0x00, 0x00]; // stop
// 1. Unlock + stop/start
self.unlock_tray();
let stop = [0x1Bu8, 0x00, 0x00, 0x00, 0x00, 0x00];
let _ = self.scsi.as_mut().execute(
&stop, crate::scsi::DataDirection::None, &mut buf, 5_000,
);
std::thread::sleep(std::time::Duration::from_millis(500));
let start = [0x1Bu8, 0x00, 0x00, 0x00, 0x01, 0x00]; // start
let start = [0x1Bu8, 0x00, 0x00, 0x00, 0x01, 0x00];
let _ = self.scsi.as_mut().execute(
&start, crate::scsi::DataDirection::None, &mut buf, 5_000,
);
std::thread::sleep(std::time::Duration::from_millis(2000));
// 3. Check if TUR works now
let tur = [0x00u8, 0x00, 0x00, 0x00, 0x00, 0x00];
if self.scsi.as_mut().execute(
&tur, crate::scsi::DataDirection::None, &mut buf, 5_000,
).is_ok() {
return Ok(());
}
// 4. If still stuck and we have a profile, try re-init
// 2. Eject cycle — clears MT1959 LibreDrive stuck state.
// After eject, TUR returning "Not Ready — tray open" (sense key 2)
// counts as success: the drive is functional, just needs disc reinserted.
self.unlock_tray();
let eject = [0x1Bu8, 0x00, 0x00, 0x00, 0x02, 0x00];
let _ = self.scsi.as_mut().execute(
&eject, crate::scsi::DataDirection::None, &mut buf, 30_000,
);
std::thread::sleep(std::time::Duration::from_millis(2000));
match self.scsi.as_mut().execute(
&tur, crate::scsi::DataDirection::None, &mut buf, 5_000,
) {
Ok(_) => return Ok(()),
Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()), // tray open = valid
_ => {}
}
// 3. If still stuck and we have a profile, try re-init
if self.driver.is_some() {
self.init()?;
std::thread::sleep(std::time::Duration::from_millis(1000));
if self.scsi.as_mut().execute(
match self.scsi.as_mut().execute(
&tur, crate::scsi::DataDirection::None, &mut buf, 5_000,
).is_ok() {
return Ok(());
) {
Ok(_) => return Ok(()),
Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()),
_ => {}
}
}
@@ -346,16 +365,29 @@ impl DriveSession {
let _ = self.scsi_execute(&cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
}
pub fn eject(&mut self) -> Result<()> {
let allow_cdb = [0x1Eu8, 0, 0, 0, 0x00, 0];
/// Lock the tray so the disc cannot be ejected during a rip.
pub fn lock_tray(&mut self) {
let prevent = [0x1Eu8, 0x00, 0x00, 0x00, 0x01, 0x00];
let mut buf = [0u8; 0];
let _ = self.scsi.as_mut().execute(
&allow_cdb,
crate::scsi::DataDirection::None,
&mut buf,
5_000,
&prevent, crate::scsi::DataDirection::None, &mut buf, 5_000,
);
}
/// Unlock the tray so the user can manually eject the disc.
pub fn unlock_tray(&mut self) {
let allow = [0x1Eu8, 0x00, 0x00, 0x00, 0x00, 0x00];
let mut buf = [0u8; 0];
let _ = self.scsi.as_mut().execute(
&allow, crate::scsi::DataDirection::None, &mut buf, 5_000,
);
}
/// Eject the disc tray. Unlocks first, then ejects.
pub fn eject(&mut self) -> Result<()> {
self.unlock_tray();
let eject_cdb = [0x1Bu8, 0, 0, 0, 0x02, 0];
let mut buf = [0u8; 0];
self.scsi.as_mut().execute(
&eject_cdb,
crate::scsi::DataDirection::None,
@@ -376,14 +408,29 @@ impl DriveSession {
}
}
impl SectorReader for DriveSession {
impl SectorReader for Drive {
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
self.read_disc(lba, count, buf)
}
}
/// Find all optical drives connected to this system.
pub fn find_drives() -> Vec<(String, DriveId)> {
/// Returns opened Drive objects ready for use.
pub fn find_drives() -> Vec<Drive> {
discover_drives()
.into_iter()
.filter_map(|(path, _)| Drive::open(std::path::Path::new(&path)).ok())
.collect()
}
/// Find the first optical drive.
/// Returns an opened Drive ready for use.
pub fn find_drive() -> Option<Drive> {
find_drives().into_iter().next()
}
/// Internal: discover drive paths + IDs without opening full Drive objects.
fn discover_drives() -> Vec<(String, DriveId)> {
#[cfg(target_os = "linux")]
{
linux::find_drives()
@@ -398,13 +445,8 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
}
}
/// Find the first optical drive, returning its device path.
pub fn find_drive() -> Option<String> {
find_drives().into_iter().next().map(|(path, _)| path)
}
/// Resolve a device path to its raw SCSI device, with optional warning message.
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
pub(crate) fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
#[cfg(target_os = "linux")]
{
linux::resolve_device(path)