cashcode 0.1.1

CashCode bill validator driver using the CCNET protocol
Documentation
use std::time::Duration;

use cashcode::{BillMask, CashcodeDevice, DeviceState, SecurityLevel};

fn main() -> cashcode::Result<()> {
    env_logger::init();

    let port = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "/dev/ttyUSB0".into());

    let mut dev = CashcodeDevice::open(&port, None, None)?;

    dev.reset()?;
    dev.wait_for_ready(Duration::from_secs(30))?;

    let table = dev.get_bill_table()?;

    println!("\nBill table:");

    for (idx, entry) in table.active_entries() {
        println!("  [{idx:2}] {entry}");
    }

    dev.enable_bill_types(BillMask::ALL)?;

    println!("\nReady. Insert a bill …\n");

    loop {
        let state = dev.poll()?;

        match &state {
            DeviceState::Idling => {}
            DeviceState::Initializing => dev.set_security(SecurityLevel::Low)?,
            DeviceState::Accepting => println!("Accepting …"),
            DeviceState::Stacking => println!("Stacking …"),
            DeviceState::Returning => println!("Returning …"),
            DeviceState::Holding => println!("Holding …"),
            DeviceState::EscrowPosition { bill_type } => match table.get(*bill_type) {
                Some(entry) if !entry.is_empty() => {
                    println!("Escrow: {entry} (type {bill_type}) — stacking");
                    dev.stack()?;
                }
                _ => {
                    println!("Escrow: unrecognised type {bill_type} — returning");
                    dev.return_bill()?;
                }
            },
            DeviceState::BillStacked { bill_type } => {
                let label = table
                    .get(*bill_type)
                    .map(|e| e.to_string())
                    .unwrap_or_else(|| format!("type {bill_type}"));

                println!("✓ Bill stacked: {label}");
            }
            DeviceState::BillReturned { bill_type } => {
                println!("Bill returned (type {bill_type})");
            }
            DeviceState::UnitDisabled => {
                println!("Unit disabled — re-enabling …");
                dev.enable_bill_types(BillMask::ALL)?;
                println!("Re-enabled. Ready for next bill.");
            }
            DeviceState::Rejecting(reason) => {
                println!("Bill rejected: {reason:?}");
            }
            DeviceState::CassetteFull => {
                eprintln!("ERROR: Cassette is full — please empty it.");
            }
            DeviceState::CassetteOutOfPosition => {
                eprintln!("ERROR: Cassette is out of position.");
            }
            DeviceState::ValidatorJammed => {
                eprintln!("ERROR: Validator jammed.");
            }
            DeviceState::CassetteJammed => {
                eprintln!("ERROR: Cassette jammed.");
            }
            DeviceState::Cheated => {
                eprintln!("ERROR: Tamper/cheat detected.");
            }
            DeviceState::Failure(code) => {
                eprintln!("ERROR: Hardware failure — {code:?}");
            }
            other => {
                println!("State: {other:?}");
            }
        }

        std::thread::sleep(cashcode::POLL_INTERVAL);
    }
}