pub mod autoexpose;
pub mod data;
pub mod focus;
pub mod image;
pub mod probe;
pub mod scan;
pub mod window;
use crate::{
error::Error,
protocol::{
caps::{Capabilities, frames::Frames},
cdbs::{
Abort, ModeSelect, ModeSense, PageControl, ReleaseUnit, ReserveUnit, TestUnitReady,
},
curves::Curves,
data::{CooperativeAction, FrameTable, Op, Operation},
mode,
sense::{Activity, Change, Coop, Fault, Intervention, Outcome, Refusal, interpret},
},
transport::{self, Completion, Data, Transport},
};
use std::sync::Arc;
use std::{
io,
thread::sleep,
time::{Duration, Instant},
};
use tracing::*;
pub struct Session {
caps: Capabilities,
transport: Box<dyn Transport>,
divisor: u16,
frames: Option<FrameTable>,
curves: Option<Arc<Curves>>,
reserved: bool,
}
pub(crate) const PROBE_TIMEOUT: Duration = Duration::from_secs(30);
const POLL_INTERVAL: Duration = Duration::from_millis(100);
const MAX_COOPERATION: usize = 16;
pub(crate) const MOVE_TIMEOUT: Duration = Duration::from_secs(180);
pub(crate) const DRAIN_TIMEOUT: Duration = Duration::from_secs(20);
pub(crate) const READY_TIMEOUT: Duration = Duration::from_secs(180);
const MAX_CHANGES: usize = 16;
pub(crate) fn malformed(what: String) -> Error {
Error::Transport(io::Error::new(io::ErrorKind::InvalidData, what).into())
}
impl Session {
pub fn open(mut transport: Box<dyn Transport>) -> Result<Self, Error> {
let caps = probe::capabilities(transport.as_mut())?;
let divisor = caps.address.x_axis.dpi_range.last;
let mut session = Self {
transport,
caps,
divisor,
reserved: false,
frames: None,
curves: None,
};
match session.test_unit_ready(READY_TIMEOUT) {
Ok(()) => {}
Err(Error::Media(Intervention::NoMedium)) => debug!("nothing is loaded"),
Err(Error::Device(fault))
if matches!(*fault, Fault::Rejected(Refusal::OutOfSequence, _)) =>
{
debug!("a scan was still valid from the last process, stopping it");
session.abort()?;
session.test_unit_ready(READY_TIMEOUT)?;
}
Err(e) => return Err(e),
}
session.stop_stale_scan()?;
session.reserved = session.reserve()?;
session.set_units(divisor)?;
session.fetch_curves();
match session.media_loaded()? {
true => session.stage()?,
false => debug!("nothing is loaded, so the mechanism is left alone"),
}
Ok(session)
}
pub fn media_loaded(&mut self) -> Result<bool, Error> {
if self.caps.frames.is_some() {
let page = probe::vpd(self.transport.as_mut(), Frames::PAGE_CODE)?;
let frames = Frames::try_from(&page)?;
let loaded = !frames.images.is_empty();
self.caps.frames = Some(frames);
return Ok(loaded);
}
match self.test_unit_ready(MOVE_TIMEOUT) {
Ok(()) => Ok(true),
Err(Error::Media(Intervention::NoMedium)) => Ok(false),
Err(e) => Err(e),
}
}
pub fn stage(&mut self) -> Result<(), Error> {
let held = self.windows()?;
let y_boundary = self.caps.address.y_axis.boundary;
for w in &held {
if w.size.1 > y_boundary {
debug!(id = w.id, size = w.size.1, "skipping stale power-on window");
continue;
}
if let Err(e) = self.set_window(w) {
debug!(id = w.id, %e, "this window would not go back");
}
}
if let Ok(params) = self.get_parameter(Op::FocusMove) {
let at = params.first.min(u32::from(u16::MAX)) as u16;
match self.focus_to(at) {
Ok(()) => debug!(at, "staged the focus"),
Err(e) => debug!(at, %e, "could not stage the focus"),
}
}
Ok(())
}
fn tolerate(&mut self, cdb: &[u8], timeout: Duration, allowed: Refusal) -> Result<bool, Error> {
match self.run(cdb, Data::None, timeout) {
Ok(_) => Ok(true),
Err(Error::Device(fault)) if matches!(*fault, Fault::Rejected(refusal, _) if refusal == allowed) => {
Ok(false)
}
Err(e) => Err(e),
}
}
fn reserve(&mut self) -> Result<bool, Error> {
let held = self.tolerate(&ReserveUnit.cdb(), PROBE_TIMEOUT, Refusal::UnknownOpcode)?;
if !held {
debug!("this unit has no RESERVE UNIT");
}
Ok(held)
}
fn stop_stale_scan(&mut self) -> Result<(), Error> {
match self.windows() {
Ok(_) => Ok(()),
Err(Error::Device(fault))
if matches!(*fault, Fault::Rejected(Refusal::OutOfSequence, _)) =>
{
debug!("a scan was still valid from earlier, stopping it");
self.abort().map(drop)
}
Err(e) => Err(e),
}
}
pub fn abort(&mut self) -> Result<bool, Error> {
if !self.tolerate(&Abort.cdb(), PROBE_TIMEOUT, Refusal::UnknownOpcode)? {
debug!("this unit has no ABORT");
return Ok(false);
}
self.test_unit_ready(MOVE_TIMEOUT)?;
Ok(true)
}
pub(crate) fn abandon_scan(&mut self) {
if let Err(e) = self.abort() {
warn!(
%e,
"could not stop the scan, so it is left open - the next command \
will be refused out of sequence"
);
}
}
pub fn eject(&mut self) -> Result<bool, Error> {
if !self.caps.features.execute.supports(Op::Unload) {
debug!("this unit has no UNLOAD");
return Ok(false);
}
match self.execute(Op::Unload, Operation::default(), MOVE_TIMEOUT) {
Ok(()) => Ok(true),
Err(Error::Media(Intervention::NoMedium)) => Ok(true),
Err(e) => Err(e),
}
}
pub fn load(&mut self) -> Result<bool, Error> {
if !self.caps.features.execute.supports(Op::Load) {
debug!("this unit has no LOAD");
return Ok(false);
}
match self.execute(Op::Load, Operation::default(), MOVE_TIMEOUT) {
Ok(_) => Ok(true),
Err(Error::Media(Intervention::NothingToLoad)) => {
debug!("the adapter has nothing left to take in");
Ok(false)
}
Err(e) => Err(e),
}
}
pub fn capabilities(&self) -> &Capabilities {
&self.caps
}
pub fn refresh(&mut self) -> Result<(), Error> {
self.caps = probe::capabilities(self.transport.as_mut())?;
Ok(())
}
pub fn test_unit_ready(&mut self, timeout: Duration) -> Result<(), Error> {
self.run(&TestUnitReady.cdb(), Data::None, timeout)?;
Ok(())
}
pub fn mode_sense(&mut self, page: u8, control: PageControl) -> Result<Vec<u8>, Error> {
let cmd = ModeSense::new(page, control);
let mut buf = vec![0u8; cmd.allocation_length()];
let completion = self.run(&cmd.cdb(), Data::In(&mut buf), PROBE_TIMEOUT)?;
buf.truncate(completion.transferred);
Ok(buf)
}
pub fn units(&mut self) -> Result<u16, Error> {
let reply = self.mode_sense(mode::MEASUREMENT_UNITS, PageControl::Current)?;
mode::divisor(&reply)
.ok_or_else(|| malformed(format!("no measurement units page in {reply:02x?}")))
}
pub fn set_units(&mut self, divisor: u16) -> Result<(), Error> {
let max = self.caps.address.x_axis.dpi_range.last;
if divisor != 1200 && divisor != max {
return Err(Error::Unsupported {
op: "measurement units",
reason: format!("the divisor must be 1200 or {max}, not {divisor}"),
});
}
let list = mode::set_divisor(divisor);
if enabled!(Level::TRACE) {
let hex: Vec<String> = list.iter().map(|b| format!("{b:02X}")).collect();
trace!(divisor, bytes = hex.join(" "), "mode select");
}
let cmd = ModeSelect::new(list.len() as u8);
self.run(&cmd.cdb(), Data::Out(&list), PROBE_TIMEOUT)?;
self.divisor = divisor;
Ok(())
}
pub fn run(
&mut self,
cdb: &[u8],
data: Data<'_>,
timeout: Duration,
) -> Result<Completion, Error> {
let (completion, _) = self.run_handshake(cdb, data, timeout)?;
Ok(completion)
}
pub(crate) fn run_handshake(
&mut self,
cdb: &[u8],
mut data: Data<'_>,
timeout: Duration,
) -> Result<(Completion, Vec<CooperativeAction>), Error> {
let deadline = Instant::now() + timeout;
let mut cooperations = Vec::new();
let mut asked: Vec<(Coop, CooperativeAction)> = Vec::new();
for _ in 0..=MAX_COOPERATION {
let payload = data.reborrow();
let (completion, coop) = self.run_cooperative(cdb, payload, deadline, timeout)?;
let Some(coop) = coop else {
return Ok((completion, cooperations));
};
let record = self.cooperation()?;
debug!(?coop, ?record, "the unit wants something doing");
if asked.contains(&(coop, record.clone())) {
return Err(Error::Unsupported {
op: "host cooperation",
reason: format!("the unit asked for {coop:?} twice over"),
});
}
asked.push((coop, record.clone()));
cooperations.push(record);
}
Err(Error::Unsupported {
op: "host cooperation",
reason: format!(
"the unit asked for {} things and was still going: {asked:?}",
asked.len()
),
})
}
fn run_cooperative(
&mut self,
cdb: &[u8],
mut data: Data<'_>,
deadline: Instant,
budget: Duration,
) -> Result<(Completion, Option<Coop>), Error> {
let mut changes = 0usize;
let mut reported: Option<Activity> = None;
loop {
let payload = data.reborrow();
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(Error::Transport(transport::Error::Timeout(budget)));
}
let completion = self.transport.execute(cdb, payload, left)?;
match interpret(&completion) {
Outcome::Complete => return Ok((completion, None)),
Outcome::CompleteWith(adjustment) => {
info!(
?adjustment,
opcode = cdb.first(),
sense = ?completion.sense,
"the scanner had a note about that"
);
return Ok((completion, None));
}
Outcome::NeedsHost(coop) => return Ok((completion, Some(coop))),
Outcome::Working(activity) => {
if reported.replace(activity) != Some(activity) {
debug!(?activity, "waiting");
}
sleep(POLL_INTERVAL);
}
Outcome::StateChanged(change) => {
debug!(?change, "device state changed under us, re-issuing");
self.refresh()?;
changes += 1;
if changes >= MAX_CHANGES {
return Err(unsettled(change, changes));
}
}
terminal => return Err(Error::from_outcome(terminal, &completion)),
}
}
}
}
fn unsettled(change: Change, changes: usize) -> Error {
warn!(
?change,
changes, "giving up on a device that will not settle"
);
Error::Unsupported {
op: "command",
reason: format!(
"the unit raised {changes} unit attentions without running it, last {change:?}"
),
}
}
impl Drop for Session {
fn drop(&mut self) {
if !self.reserved {
return;
}
if let Err(e) = self.run(&ReleaseUnit.cdb(), Data::None, PROBE_TIMEOUT) {
warn!(%e, "could not release the scanner");
}
}
}