use std::time::Duration;
use log::{debug, warn};
use serialport::SerialPort;
use crate::{
bill_table::BillTable,
command::{BillMask, Command, SecurityLevel},
error::{Error, Result},
frame::{ACK, Frame, SYNC},
status::DeviceState,
};
pub const DEFAULT_BAUD_RATE: u32 = 9600;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
pub const POLL_INTERVAL: Duration = Duration::from_millis(200);
pub const BUS_SILENCE: Duration = Duration::from_millis(20);
#[derive(Debug, Clone)]
pub struct Identification {
pub part_number: String,
pub serial_number: String,
pub asset_number: String,
}
pub struct CashcodeDevice {
port: Box<dyn SerialPort>,
bill_table: Option<BillTable>,
}
impl CashcodeDevice {
pub fn open(path: &str, baud_rate: Option<u32>, timeout: Option<Duration>) -> Result<Self> {
let port = serialport::new(path, baud_rate.unwrap_or(DEFAULT_BAUD_RATE))
.data_bits(serialport::DataBits::Eight)
.parity(serialport::Parity::None)
.stop_bits(serialport::StopBits::One)
.timeout(timeout.unwrap_or(DEFAULT_TIMEOUT))
.open()?;
Ok(Self {
port,
bill_table: None,
})
}
fn send(&mut self, command: Command) -> Result<Frame> {
let frame = Frame::new(command.to_data());
let bytes = frame.encode();
debug!("TX → {:02X?}", bytes);
self.port.write_all(&bytes)?;
self.port.flush()?;
std::thread::sleep(BUS_SILENCE);
self.read_frame()
}
fn read_frame(&mut self) -> Result<Frame> {
let sync = loop {
let mut buf = [0u8; 1];
match self.port.read_exact(&mut buf) {
Ok(()) => {
if buf[0] == SYNC {
break buf[0];
}
warn!("discarding non-SYNC byte: {:#04x}", buf[0]);
}
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
return Err(Error::Timeout);
}
Err(e) => return Err(Error::Io(e)),
}
};
let mut header = [0u8; 2];
self.port.read_exact(&mut header)?;
let addr = header[0];
let lng = header[1] as usize;
if lng < 6 {
return Err(Error::InvalidFrame("LNG < 6 in received frame"));
}
let remaining = lng - 3;
let mut rest = vec![0u8; remaining];
self.port.read_exact(&mut rest)?;
let mut raw = Vec::with_capacity(lng);
raw.push(sync);
raw.push(addr);
raw.push(lng as u8);
raw.extend_from_slice(&rest);
debug!("RX ← {:02X?}", raw);
Frame::decode(&raw)
}
fn ack_command(&mut self, command: Command) -> Result<()> {
let response = self.send(command)?;
match response.data.first().copied() {
Some(ACK) => Ok(()),
Some(0xFF) => Err(Error::Nak),
_ => Ok(()), }
}
pub fn reset(&mut self) -> Result<()> {
debug!("sending RESET");
self.ack_command(Command::Reset)
}
pub fn poll(&mut self) -> Result<DeviceState> {
let response = self.send(Command::Poll)?;
let ack = Frame::new(vec![ACK]).encode();
debug!("TX ACK → {:02X?}", ack);
let _ = self.port.write_all(&ack);
let _ = self.port.flush();
DeviceState::from_poll_data(&response.data)
}
pub fn enable_bill_types(&mut self, mask: BillMask) -> Result<()> {
debug!("enabling bill types: {:?}", mask.0);
self.ack_command(Command::EnableBillTypes(mask))
}
pub fn disable(&mut self) -> Result<()> {
self.ack_command(Command::EnableBillTypes(BillMask::NONE))
}
pub fn set_security(&mut self, level: SecurityLevel) -> Result<()> {
debug!("setting security level: {:?}", level);
self.ack_command(Command::SetSecurity(level))?;
std::thread::sleep(Duration::from_secs(2));
Ok(())
}
pub fn stack(&mut self) -> Result<()> {
debug!("sending STACK");
self.ack_command(Command::Stack)
}
pub fn return_bill(&mut self) -> Result<()> {
debug!("sending RETURN");
self.ack_command(Command::Return)
}
pub fn hold(&mut self) -> Result<()> {
debug!("sending HOLD");
self.ack_command(Command::Hold)
}
pub fn get_bill_table(&mut self) -> Result<BillTable> {
let response = self.send(Command::GetBillTable)?;
let table = BillTable::from_response_data(&response.data)?;
self.bill_table = Some(table.clone());
Ok(table)
}
pub fn bill_table(&mut self) -> Result<&BillTable> {
if self.bill_table.is_none() {
self.get_bill_table()?;
}
Ok(self.bill_table.as_ref().unwrap())
}
pub fn identify(&mut self) -> Result<Identification> {
let response = self.send(Command::Identification)?;
let data = &response.data;
if data.len() < 26 {
return Err(Error::InvalidFrame("IDENTIFICATION response too short"));
}
Ok(Identification {
part_number: String::from_utf8_lossy(&data[0..7])
.trim_end_matches('\0')
.to_string(),
serial_number: String::from_utf8_lossy(&data[7..19])
.trim_end_matches('\0')
.to_string(),
asset_number: String::from_utf8_lossy(&data[19..26])
.trim_end_matches('\0')
.to_string(),
})
}
pub fn initialize(&mut self) -> Result<()> {
self.reset()?;
self.wait_for_ready(Duration::from_secs(30))?;
self.get_bill_table()?;
self.enable_bill_types(BillMask::ALL)?;
Ok(())
}
pub fn wait_for_ready(&mut self, timeout: Duration) -> Result<()> {
let deadline = std::time::Instant::now() + timeout;
loop {
if std::time::Instant::now() >= deadline {
return Err(Error::Timeout);
}
let state = self.poll()?;
debug!("wait_for_ready: state = {:?}", state);
match state {
DeviceState::Idling | DeviceState::UnitDisabled => return Ok(()),
DeviceState::Initializing => {
debug!("device initializing — sending SET_SECURITY");
let _ = self.set_security(SecurityLevel::Low);
}
DeviceState::PowerUp
| DeviceState::PowerUpBillInValidator
| DeviceState::PowerUpBillInStacker
| DeviceState::Busy => {}
other if other.is_error() => {
return Err(Error::NotReady(format!("{:?}", other)));
}
_ => {}
}
std::thread::sleep(POLL_INTERVAL);
}
}
}