use bitflags::bitflags;
use crate::{
commands::VariousModeSettings, error::StatusParsingError, media::LabelType,
printer::PrinterModel,
};
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ErrorFlags: u16 {
const NoMediaError = 0b1 << 0;
const EndOfMediaError = 0b1 << 1;
const CutterJamError=0b1 << 2;
const PrinterInUseError = 0b1 << 4;
const PrinterTurnedOffError = 0b1 << 5;
const HighVoltageAdapterError = 0b1 << 6;
const FanMotorError = 0b1 << 7;
const ReplaceMediaError = 0b1 << 8;
const ExpansionBufferFullError = 0b1 << 9;
const CommunicationError = 0b1 << 10;
const CommunicationBufferFullError = 0b1 << 11;
const CoverOpenError = 0b1 << 12;
const CancelKeyError = 0b1 << 13;
const FeedingError = 0b1 << 14;
const SystemError = 0b1 << 15;
const _ = !0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusType {
StatusRequestReply,
PrintingCompleted,
ErrorOccurred,
TurnedOff,
Notification,
PhaseChange,
}
impl TryFrom<u8> for StatusType {
type Error = StatusParsingError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(Self::StatusRequestReply),
0x01 => Ok(Self::PrintingCompleted),
0x02 => Ok(Self::ErrorOccurred),
0x04 => Ok(Self::TurnedOff),
0x05 => Ok(Self::Notification),
0x06 => Ok(Self::PhaseChange),
unused @ 0x08..=0x20 => Err(StatusParsingError {
reason: format!("{unused:#x} is an unused status type"),
}),
reserved @ 0x21..=0xff => Err(StatusParsingError {
reason: format!("{reserved:#x} is a reserved status type"),
}),
invalid => Err(StatusParsingError {
reason: format!("invalid status type {invalid:#x}"),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Receiving,
Printing,
}
impl TryFrom<[u8; 3]> for Phase {
type Error = StatusParsingError;
fn try_from(value: [u8; 3]) -> Result<Self, Self::Error> {
match value {
[0x00, 0x00, 0x00] => Ok(Self::Receiving),
[0x01, 0x00, 0x00] => Ok(Self::Printing),
[a, b, c] => Err(StatusParsingError {
reason: format!("invalid phase state {a:#x}{b:x}{c:x}"),
}),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Notification {
Unavailable,
CoolingStarted,
CoolingFinished,
}
impl TryFrom<u8> for Notification {
type Error = StatusParsingError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(Self::Unavailable),
0x03 => Ok(Self::CoolingStarted),
0x04 => Ok(Self::CoolingFinished),
invalid => Err(StatusParsingError {
reason: format!("invalid notification number {invalid:#x}"),
}),
}
}
}
#[derive(Debug)]
pub struct StatusInformation {
pub model: PrinterModel,
pub errors: ErrorFlags,
pub media_width: u8,
pub media_type: Option<LabelType>,
pub mode: VariousModeSettings,
pub media_length: u8,
pub status_type: StatusType,
pub phase: Phase,
pub notification: Notification,
}
impl StatusInformation {
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
impl std::fmt::Display for StatusInformation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Printer: {}", self.model)?;
match self.media_type {
Some(LabelType::Continuous) => {
writeln!(f, "Media: {}mm continuous", self.media_width)?;
}
Some(LabelType::DieCut) => {
writeln!(
f,
"Media: {}mm x {}mm die-cut",
self.media_width, self.media_length
)?;
}
None => {
writeln!(f, "Media: unknown")?;
}
}
writeln!(f, "Phase: {:?}", self.phase)?;
write!(f, "Status: {:?}", self.status_type)?;
if self.has_errors() {
write!(f, "\nErrors: {:?}", self.errors)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_status_packet() -> [u8; 32] {
let mut buf = [0u8; 32];
buf[0] = 0x80; buf[1] = 0x20; buf[2] = 0x42; buf[3] = 0x34; buf[4] = 0x41; buf[5] = 0x30; buf[6] = 0x04; buf[7] = 0x00; buf[8] = 0x00; buf[9] = 0x00; buf[10] = 62; buf[11] = 0x0a; buf[12] = 0x00; buf[13] = 0x00; buf[14] = 0x15; buf[15] = 0x00; buf[16] = 0x00; buf[17] = 0x00; buf[18] = 0x00; buf[19] = 0x00; buf[20] = 0x00;
buf[21] = 0x00;
buf[22] = 0x00; buf[23] = 0x00; buf[24] = 0x00; buf
}
#[test]
fn parse_valid_status_packet() {
let packet = valid_status_packet();
let status = StatusInformation::try_from(&packet[..]).unwrap();
assert_eq!(status.model, PrinterModel::QL820NWB);
assert_eq!(status.media_width, 62);
assert_eq!(status.media_type, Some(LabelType::Continuous));
assert_eq!(status.status_type, StatusType::StatusRequestReply);
assert_eq!(status.phase, Phase::Receiving);
assert!(!status.has_errors());
}
#[test]
fn parse_status_with_errors() {
let mut packet = valid_status_packet();
packet[8] = 0x01; let status = StatusInformation::try_from(&packet[..]).unwrap();
assert!(status.has_errors());
assert!(status.errors.contains(ErrorFlags::NoMediaError));
}
#[test]
fn parse_status_wrong_size() {
let short = [0u8; 16];
assert!(StatusInformation::try_from(&short[..]).is_err());
}
#[test]
fn status_type_roundtrip() {
assert_eq!(
StatusType::try_from(0x00).unwrap(),
StatusType::StatusRequestReply
);
assert_eq!(
StatusType::try_from(0x01).unwrap(),
StatusType::PrintingCompleted
);
assert_eq!(
StatusType::try_from(0x02).unwrap(),
StatusType::ErrorOccurred
);
assert_eq!(StatusType::try_from(0x06).unwrap(), StatusType::PhaseChange);
assert!(StatusType::try_from(0x08).is_err());
}
#[test]
fn phase_roundtrip() {
assert_eq!(
Phase::try_from([0x00, 0x00, 0x00]).unwrap(),
Phase::Receiving
);
assert_eq!(
Phase::try_from([0x01, 0x00, 0x00]).unwrap(),
Phase::Printing
);
assert!(Phase::try_from([0x02, 0x00, 0x00]).is_err());
}
#[test]
fn display_impl() {
let packet = valid_status_packet();
let status = StatusInformation::try_from(&packet[..]).unwrap();
let display = format!("{status}");
assert!(display.contains("QL-820NWB"));
assert!(display.contains("62mm"));
assert!(display.contains("continuous"));
}
}
impl TryFrom<&[u8]> for StatusInformation {
type Error = StatusParsingError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let status: &[u8; 32] = value.try_into().map_err(|_| StatusParsingError {
reason: format!("invalid size of {}B", value.len()),
})?;
let check_fixed_field = |offset: usize,
name: &str,
expected_value: u8|
-> Result<(), StatusParsingError> {
if status[offset] != expected_value {
return Err(StatusParsingError {
reason: format!(
"expected value {expected_value:#x} for field {name} at offset {offset} but was {:#x}",
status[offset]
),
});
}
Ok(())
};
check_fixed_field(0, "Print head mark", 0x80)?;
check_fixed_field(1, "Size", 0x20)?;
check_fixed_field(2, "Reserved", 0x42)?;
check_fixed_field(3, "Series code", 0x34)?;
let model = PrinterModel::try_from(status[4])?;
check_fixed_field(5, "Reserved", 0x30)?;
check_fixed_field(7, "Reserved", 0x00)?;
let errors = ErrorFlags::from_bits_retain(u16::from_le_bytes([status[8], status[9]]));
let media_width = status[10];
let media_type = match status[11] {
0x00 => None,
other => Some(LabelType::try_from(other)?),
};
check_fixed_field(12, "Reserved", 0x00)?;
check_fixed_field(13, "Reserved", 0x00)?;
let mode = VariousModeSettings::try_from(status[15])?;
check_fixed_field(16, "Reserved", 0x00)?;
let media_length = status[17];
let status_type = StatusType::try_from(status[18])?;
let phase_bytes: [u8; 3] = status[19..=21]
.try_into()
.expect("This conversion is infallible due to the earlier size assertion");
let phase = Phase::try_from(phase_bytes)?;
let notification = Notification::try_from(status[22])?;
check_fixed_field(23, "Reserved", 0x00)?;
check_fixed_field(24, "Reserved", 0x00)?;
Ok(StatusInformation {
model,
errors,
media_width,
media_type,
mode,
media_length,
status_type,
phase,
notification,
})
}
}