use std::time::Duration;
use psdk_father::device::ConnectedDevice;
use crate::{
chunker::{chunk_print_data, file_transfer_payload_capacity, print_transfer_payload_capacity},
command::EmapiCommand,
connection::EmapiConnection,
constants::{
CHILD_DEVICE_INFO, CHILD_FILE_END, CHILD_FILE_START, CHILD_FILE_TRANSFER, CHILD_FILE_UPGRADE,
CHILD_PRINTER_PARAMS, CHILD_PRINT_SELF_TEST_PAGE, CHILD_PRINT_STATUS,
CHILD_RFID_AUTH_FAILURE_HANDLING, CHILD_RFID_CARD_INFO, CHILD_RFID_PAPER_LENGTH,
CHILD_RFID_UID, CHILD_SET_SHUTDOWN_TIME, CHILD_SLEEP_SHUTDOWN, CHILD_TRANSFER_DATA,
CHILD_WIFI_CONFIG, CHILD_WIFI_CONNECTION_STATE, CHILD_WIFI_FILE_END, CHILD_WIFI_FILE_START,
CHILD_WIFI_FILE_TRANSFER, CHILD_WIFI_HOTSPOT_INFO, PARENT_FILE, PARENT_PRINTER, PARENT_RFID,
PARENT_SYSTEM, PARENT_WIFI, TAG_MTU, TYPE_PASSTHROUGH_REQUEST, TYPE_PASSTHROUGH_RESPONSE,
TYPE_REQUEST, TYPE_RESPONSE,
},
device_connection::ConnectedDeviceEmapiConnection,
endian::read_int16,
error::{EmapiError, Result},
models::{
EmapiPrintStatus, EmapiPrinterParams, EmapiReport, EmapiRfidAuthFailurePolicy,
EmapiRfidCardInfo, EmapiWifiConnectionState, EmapiWifiHotspotInfo,
},
payload::EmapiPayload,
printer_info::EmapiPrinterInfo,
session::EmapiSession,
tlv::{Tlv, TlvData, TlvEntry},
};
pub struct EmapiPrinter<C> {
session: EmapiSession<C>,
fallback_mtu: usize,
mtu: Option<usize>,
}
impl<C> EmapiPrinter<C>
where
C: EmapiConnection,
{
pub fn new(connection: C) -> Self {
Self {
session: EmapiSession::new(connection),
fallback_mtu: 512,
mtu: None,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.session.set_timeout(timeout);
self
}
pub fn with_max_retries(mut self, max_retries: usize) -> Result<Self> {
self.session.set_max_retries(max_retries);
Ok(self)
}
pub fn with_mtu(mut self, mtu: usize) -> Result<Self> {
if mtu == 0 {
return Err(EmapiError::Protocol {
message: "mtu must be greater than zero".to_string(),
});
}
self.mtu = Some(mtu);
Ok(self)
}
pub fn with_fallback_mtu(mut self, fallback_mtu: usize) -> Result<Self> {
if fallback_mtu == 0 {
return Err(EmapiError::Protocol {
message: "fallbackMtu must be greater than zero".to_string(),
});
}
self.fallback_mtu = fallback_mtu;
Ok(self)
}
pub fn reports(&self) -> &[EmapiReport] {
self.session.reports()
}
pub fn read_report(&mut self) -> Result<EmapiReport> {
self.session.read_report()
}
pub fn into_inner(self) -> C {
self.session.into_inner()
}
pub fn query_device_info(&mut self) -> Result<EmapiPrinterInfo> {
let response = self.send_command(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_SYSTEM,
CHILD_DEVICE_INFO,
Vec::new(),
false,
)?;
let tlv = Tlv::decode(&response.payload)?;
let info = EmapiPrinterInfo {
device_type: tlv.string(0x01)?,
device_model: tlv.string(0x02)?,
brand: tlv.string(0x03)?,
serial_number: tlv.string(0x04)?,
hardware_version: tlv.string(0x05)?,
software_version: tlv.string(0x06)?,
boot_version: tlv.string(0x07)?,
mtu: tlv.uint16(TAG_MTU)?,
};
if let Some(mtu) = info.mtu {
self.mtu = Some(usize::from(mtu));
}
Ok(info)
}
pub fn sleep_shutdown(&mut self) -> Result<()> {
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_SYSTEM,
CHILD_SLEEP_SHUTDOWN,
[],
)
}
pub fn set_shutdown_time(&mut self, minutes: u16) -> Result<()> {
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_SYSTEM,
CHILD_SET_SHUTDOWN_TIME,
EmapiPayload::uint16(minutes),
)
}
pub fn query_rfid_uid(&mut self) -> Result<String> {
let response = self.send_command(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_RFID,
CHILD_RFID_UID,
Vec::new(),
false,
)?;
String::from_utf8(response.payload).map_err(|_| EmapiError::InvalidUtf8)
}
pub fn query_rfid_card_info(&mut self) -> Result<EmapiRfidCardInfo> {
let response = self.send_command(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_RFID,
CHILD_RFID_CARD_INFO,
Vec::new(),
false,
)?;
let tlv = Tlv::decode(&response.payload)?;
Ok(EmapiRfidCardInfo {
paper_model: tlv.string(0x01)?,
paper_length: tlv.string(0x02)?,
paper_width: tlv.string(0x03)?,
paper_color: tlv.string(0x04)?,
paper_material_number: tlv.string(0x05)?,
})
}
pub fn query_rfid_paper_length(&mut self) -> Result<u32> {
let response = self.send_command(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_RFID,
CHILD_RFID_PAPER_LENGTH,
Vec::new(),
false,
)?;
EmapiPayload::read_uint32(&response.payload)
}
pub fn set_rfid_auth_failure_handling(
&mut self,
policy: EmapiRfidAuthFailurePolicy,
) -> Result<()> {
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_RFID,
CHILD_RFID_AUTH_FAILURE_HANDLING,
EmapiPayload::uint8(if policy == EmapiRfidAuthFailurePolicy::ForbidPrint {
0x01
} else {
0x00
}),
)
}
pub fn set_wifi_config(
&mut self,
ssid: &str,
password: &str,
encryption_method: Option<u8>,
) -> Result<()> {
let mut entries = vec![
TlvEntry::string(0x01, ssid)?,
TlvEntry::string(0x02, password)?,
];
if let Some(encryption_method) = encryption_method {
entries.push(TlvEntry::uint8(0x03, encryption_method)?);
}
self.send_acked(
TYPE_PASSTHROUGH_REQUEST,
TYPE_PASSTHROUGH_RESPONSE,
PARENT_WIFI,
CHILD_WIFI_CONFIG,
Tlv::encode(&entries)?,
)
}
pub fn query_wifi_connection_state(&mut self) -> Result<EmapiWifiConnectionState> {
let response = self.send_command(
TYPE_PASSTHROUGH_REQUEST,
TYPE_PASSTHROUGH_RESPONSE,
PARENT_WIFI,
CHILD_WIFI_CONNECTION_STATE,
Vec::new(),
false,
)?;
match response.payload.first().copied() {
Some(0x00) => Ok(EmapiWifiConnectionState::NotConnected),
Some(0x01) => Ok(EmapiWifiConnectionState::HotspotConnected),
Some(0x02) => Ok(EmapiWifiConnectionState::IotConnected),
Some(_) => Ok(EmapiWifiConnectionState::Unknown),
None => Err(EmapiError::Protocol {
message: "missing WIFI connection state payload".to_string(),
}),
}
}
pub fn query_wifi_hotspot_info(&mut self) -> Result<EmapiWifiHotspotInfo> {
let response = self.send_command(
TYPE_PASSTHROUGH_REQUEST,
TYPE_PASSTHROUGH_RESPONSE,
PARENT_WIFI,
CHILD_WIFI_HOTSPOT_INFO,
Vec::new(),
false,
)?;
let tlv = Tlv::decode(&response.payload)?;
Ok(EmapiWifiHotspotInfo {
ssid: tlv.string(0x01)?,
rssi: tlv_int16(&tlv, 0x02)?,
ip: tlv.string(0x03)?,
port: tlv.string(0x04)?,
})
}
pub fn start_wifi_file_download(&mut self, file_type: u16, total_size: u32) -> Result<()> {
self.send_acked(
TYPE_PASSTHROUGH_REQUEST,
TYPE_PASSTHROUGH_RESPONSE,
PARENT_WIFI,
CHILD_WIFI_FILE_START,
file_start_payload(file_type, total_size)?,
)
}
pub fn transfer_wifi_file_download_chunk(&mut self, index: u32, data: &[u8]) -> Result<()> {
self.validate_file_transfer_chunk(data)?;
self.send_acked(
TYPE_PASSTHROUGH_REQUEST,
TYPE_PASSTHROUGH_RESPONSE,
PARENT_WIFI,
CHILD_WIFI_FILE_TRANSFER,
EmapiPayload::file_chunk(index, data)?,
)
}
pub fn finish_wifi_file_download(&mut self) -> Result<()> {
self.send_acked(
TYPE_PASSTHROUGH_REQUEST,
TYPE_PASSTHROUGH_RESPONSE,
PARENT_WIFI,
CHILD_WIFI_FILE_END,
[],
)
}
pub fn query_printer_params(&mut self) -> Result<EmapiPrinterParams> {
let response = self.send_command(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_PRINTER,
CHILD_PRINTER_PARAMS,
Vec::new(),
false,
)?;
let tlv = Tlv::decode(&response.payload)?;
Ok(EmapiPrinterParams {
print_size: tlv.uint32(0x01)?,
resolution: tlv.uint32(0x02)?,
dots_per_byte: tlv.uint32(0x03)?,
})
}
pub fn query_print_status(&mut self) -> Result<EmapiPrintStatus> {
let response = self.send_command(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_PRINTER,
CHILD_PRINT_STATUS,
Vec::new(),
false,
)?;
let tlv = Tlv::decode(&response.payload)?;
Ok(EmapiPrintStatus {
paper_status: tlv.uint8(0x01)?,
cover_status: tlv.uint8(0x02)?,
low_battery: tlv.uint8(0x03)?,
overheat: tlv.uint8(0x04)?,
battery_percent: tlv.uint8(0x05)?,
battery_voltage: tlv.uint16(0x06)?,
tph_temperature: tlv.uint8(0x07)?.map(|value| value as i8),
})
}
pub fn print_self_test_page(&mut self) -> Result<()> {
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_PRINTER,
CHILD_PRINT_SELF_TEST_PAGE,
[],
)
}
pub fn start_main_controller_ota(&mut self, file_type: u16, total_size: u32) -> Result<()> {
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_FILE,
CHILD_FILE_START,
file_start_payload(file_type, total_size)?,
)
}
pub fn start_main_controller_ota_default(&mut self, total_size: u32) -> Result<()> {
self.start_main_controller_ota(0x0001, total_size)
}
pub fn transfer_main_controller_ota_chunk(&mut self, index: u32, data: &[u8]) -> Result<()> {
self.validate_file_transfer_chunk(data)?;
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_FILE,
CHILD_FILE_TRANSFER,
EmapiPayload::file_chunk(index, data)?,
)
}
pub fn finish_main_controller_ota(&mut self) -> Result<()> {
self.send_acked(TYPE_REQUEST, TYPE_RESPONSE, PARENT_FILE, CHILD_FILE_END, [])
}
pub fn upgrade_main_controller(&mut self) -> Result<()> {
self.send_acked(
TYPE_REQUEST,
TYPE_RESPONSE,
PARENT_FILE,
CHILD_FILE_UPGRADE,
[],
)
}
pub fn print_esc(&mut self, data: &[u8]) -> Result<()> {
let mtu = self.ensure_mtu()?;
print_transfer_payload_capacity(mtu).map_err(|_| EmapiError::Protocol {
message: format!("invalid max packet length for print transfer: {mtu}"),
})?;
for chunk in chunk_print_data(data, mtu)? {
self.session.send_and_wait(
EmapiCommand::new(TYPE_REQUEST, PARENT_PRINTER, CHILD_TRANSFER_DATA, chunk),
|command| command.is_ack_for(PARENT_PRINTER, CHILD_TRANSFER_DATA),
)?;
}
Ok(())
}
fn ensure_mtu(&mut self) -> Result<usize> {
if let Some(mtu) = self.mtu {
return Ok(mtu);
}
let info = self.query_device_info()?;
let mtu = info.mtu.map(usize::from).unwrap_or(self.fallback_mtu);
if mtu == 0 {
return Err(EmapiError::Protocol {
message: "invalid max packet length: 0".to_string(),
});
}
self.mtu = Some(mtu);
Ok(mtu)
}
fn validate_file_transfer_chunk(&mut self, data: &[u8]) -> Result<()> {
let mtu = self.ensure_mtu()?;
let capacity = file_transfer_payload_capacity(mtu).map_err(|_| EmapiError::Protocol {
message: format!("invalid max packet length for file transfer: {mtu}"),
})?;
if data.len() > capacity {
return Err(EmapiError::Protocol {
message: format!(
"file transfer chunk is too large: {} bytes (capacity: {capacity}, mtu: {mtu})",
data.len()
),
});
}
Ok(())
}
fn send_acked(
&mut self,
command_type: u8,
response_type: u8,
parent: u8,
child: u8,
payload: impl Into<Vec<u8>>,
) -> Result<()> {
self.send_command(command_type, response_type, parent, child, payload, true)?;
Ok(())
}
fn send_command(
&mut self,
command_type: u8,
response_type: u8,
parent: u8,
child: u8,
payload: impl Into<Vec<u8>>,
require_empty_payload: bool,
) -> Result<EmapiCommand> {
self.session.send_and_wait(
EmapiCommand::new(command_type, parent, child, payload),
|command| {
command.is_response_for(response_type, parent, child)
&& (!require_empty_payload || command.payload.is_empty())
},
)
}
}
impl<D> EmapiPrinter<ConnectedDeviceEmapiConnection<D>>
where
D: ConnectedDevice,
{
pub fn connected_device(device: D) -> Self {
EmapiPrinter::new(ConnectedDeviceEmapiConnection::new(device))
}
}
fn file_start_payload(file_type: u16, total_size: u32) -> Result<Vec<u8>> {
Tlv::encode(&[
TlvEntry::uint16(0x01, file_type)?,
TlvEntry::uint32(0x02, total_size)?,
])
}
fn tlv_int16(tlv: &TlvData, tag: u8) -> Result<Option<i16>> {
let Some(entry) = tlv.get(tag) else {
return Ok(None);
};
if entry.value.len() != 2 {
return Err(EmapiError::InvalidTlvType {
tag,
expected: "int16",
actual_len: entry.value.len(),
});
}
read_int16(&entry.value, 0).map(Some)
}