anybox-rs 0.1.0

one API for the most popular passthrough boxes
Documentation
use std::borrow::Cow;

use crate::capability::{Action, Family, Firmware, StableId};

pub type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    #[error("no box answered{}, check it's plugged in and give it a few seconds if you just plugged it in. without a port, anybox only asks CH343 ports (1A86:55D3), so if your box sits behind another chip, name its port with Connect::port", tried_text(.tried))]
    #[non_exhaustive]
    NotFound { tried: Vec<String> },

    #[error(
        "{port} is already open in another program or another Device in this one, close it there first"
    )]
    #[non_exhaustive]
    PortBusy { port: String },

    #[error(
        "you're not allowed to open {port}, on Linux that usually means adding yourself to the dialout group"
    )]
    #[non_exhaustive]
    PermissionDenied { port: String },

    #[error("the box on {port} isn't a {expected}{}, pick the right family or leave it out and let anybox find it. if you just plugged it in, give it a couple seconds to start first", found_text(.found))]
    #[non_exhaustive]
    WrongFamily {
        port: String,
        expected: Family,
        found: Option<Family>,
    },

    #[error("the box on {port} is {}, not {expected}, so use Connect::auto().id(..) and anybox finds it on whatever port it's on", found_id_text(.found))]
    #[non_exhaustive]
    WrongBox {
        port: String,
        expected: StableId,
        found: Option<StableId>,
    },

    #[error(
        "this {family} runs firmware {found} and anybox needs {needed} or newer, so update the box first"
    )]
    #[non_exhaustive]
    FirmwareTooOld {
        family: Family,
        found: Firmware,
        needed: Firmware,
    },

    #[error(
        "this {family} runs firmware {found}, which is newer than this anybox knows, so update anybox"
    )]
    #[non_exhaustive]
    LibraryTooOld { family: Family, found: Firmware },

    #[error("{family} can't {action}, {reason}")]
    #[non_exhaustive]
    Unsupported {
        action: Action,
        family: Family,
        reason: Cow<'static, str>,
    },

    #[error("can't {action} like that, {detail}")]
    #[non_exhaustive]
    InvalidArgument {
        action: Action,
        detail: Cow<'static, str>,
    },

    #[error("the box refused to {action}, {detail}")]
    #[non_exhaustive]
    Rejected { action: Action, detail: String },

    #[error(
        "the box didn't answer in time when asked to {action}, check it's still plugged in and nothing else is talking to it"
    )]
    #[non_exhaustive]
    Timeout { action: Action },

    #[error(
        "the box is disconnected and anybox is already trying to reconnect, so try again once you get Reconnected"
    )]
    Disconnected,

    #[error(
        "couldn't {action} because the box reconnected or reset in the middle of it. anybox already let go of it, so do it again if you still want it"
    )]
    #[non_exhaustive]
    Interrupted { action: Action },

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

fn tried_text(tried: &[String]) -> String {
    if tried.is_empty() {
        String::new()
    } else {
        format!(" on {}", tried.join(", "))
    }
}

fn found_id_text(found: &Option<StableId>) -> String {
    match found {
        Some(id) => id.to_string(),
        None => "a box with no id".into(),
    }
}

fn found_text(found: &Option<Family>) -> String {
    match found {
        Some(family) => format!(", it's a {family}"),
        None => String::new(),
    }
}