Files
vfd/src/vfd.rs
T

322 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Синхронный низкоуровневый драйвер поверх любого [`std::io::Write`].
//!
//! `Vfd` полезен, когда приложение само управляет потоками и хочет немедленно выполнять
//! команды на конкретном транспорте. Каждый метод выполняет запись до возврата из
//! функции. Для фоновой сериализации команд из разных частей приложения используйте
//! [`crate::VfdWorker`] или `escpos_vfd::tokio::AsyncVfdWorker`.
use crate::codec::{EpsonCodec, fit_to_width, truncate_chars};
use crate::config::{DisplaySettings, VfdConfig};
use crate::error::{Result, VfdError};
use serialport::SerialPort;
use std::io::Write;
/// Низкоуровневое соединение с дисплеем, владеющее транспортом записи.
///
/// Тип транспорта параметризован, поэтому в тестах можно использовать `Vec<u8>`, а в
/// приложении - serial-порт. Все координаты в публичных методах задаются от единицы.
/// Тип не синхронизирует доступ между потоками; если один дисплей используют несколько
/// producer-ов, берите [`crate::VfdWorker`].
pub struct Vfd<T: Write = Box<dyn SerialPort>> {
transport: T,
codec: EpsonCodec,
}
impl Vfd<Box<dyn SerialPort>> {
/// Открывает serial-порт и инициализирует дисплей.
///
/// Перед открытием выполняется полная проверка [`VfdConfig`]. После успешного
/// открытия драйвер отправляет команды инициализации из [`DisplaySettings`]:
/// опциональный `ESC @` и опциональный `ESC t n`.
///
/// # Блокировка
///
/// Метод блокирует текущий поток на открытии serial-порта и записи init-команд.
///
/// # Ошибки
///
/// Возвращает [`VfdError::Config`] при неверной конфигурации,
/// [`VfdError::Serial`] при ошибке открытия порта и [`VfdError::Io`] при ошибке
/// записи init-последовательности.
pub fn open(cfg: VfdConfig) -> Result<Self> {
cfg.validate()?;
let serial = cfg.serial;
let display = cfg.display;
let mut builder = serialport::new(&serial.port_name, serial.baud_rate)
.data_bits(serial.data_bits)
.parity(serial.parity)
.stop_bits(serial.stop_bits)
.flow_control(serial.flow_control)
.timeout(serial.timeout);
#[cfg(unix)]
{
builder = builder.exclusive(serial.exclusive);
}
let port = builder.open()?;
Self::from_transport(port, display)
}
}
impl<T: Write> Vfd<T> {
/// Создаёт драйвер поверх произвольного транспорта.
///
/// Метод сразу записывает init-последовательность в переданный транспорт. Это удобно
/// для mock transport в тестах и для нестандартных serial-обёрток.
///
/// # Ошибки
///
/// Возвращает [`VfdError::Config`], если настройки дисплея неверны, или
/// [`VfdError::Io`], если транспорт не принял init-байты.
///
/// # Примеры
///
/// ```
/// use escpos_vfd::{DisplaySettings, TextEncoding, Vfd};
///
/// let display = DisplaySettings::new(4, 1, TextEncoding::Ascii);
/// let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display)?;
/// vfd.print_line(1, "OK")?;
/// let bytes = vfd.into_inner();
/// assert!(bytes.starts_with(&[0x1B, 0x40]));
/// # Ok::<(), escpos_vfd::VfdError>(())
/// ```
pub fn from_transport(mut transport: T, display: DisplaySettings) -> Result<Self> {
display.validate()?;
let codec = EpsonCodec::new(display)?;
let init = codec.init();
if !init.is_empty() {
transport.write_all(&init)?;
}
Ok(Self { transport, codec })
}
/// Возвращает геометрию и настройки дисплея.
pub fn display(&self) -> &DisplaySettings {
self.codec.display()
}
/// Количество символов в строке.
pub fn columns(&self) -> usize {
self.display().columns
}
/// Количество строк дисплея.
pub fn rows(&self) -> usize {
self.display().rows
}
/// Очищает дисплей стандартной командой очистки.
///
/// Метод отправляет байт `0x0C`. Он не обновляет внешние кэши приложения и не
/// вызывает `flush`; при необходимости вызовите [`Vfd::flush`] явно.
///
/// # Ошибки
///
/// Возвращает [`VfdError::Io`] при ошибке записи.
pub fn clear(&mut self) -> Result<()> {
self.transport.write_all(&self.codec.clear())?;
Ok(())
}
/// Полностью перезаписывает строку текстом фиксированной ширины.
///
/// Текст санитизируется, обрезается по числу символов и дополняется пробелами до
/// ширины дисплея, чтобы удалить остаток предыдущего содержимого строки.
///
/// # Ошибки
///
/// Возвращает [`VfdError::InvalidLine`], если `line` вне диапазона `1..=rows`, или
/// [`VfdError::Io`] при ошибке записи.
pub fn print_line(&mut self, line: u8, text: &str) -> Result<()> {
self.codec.validate_line(line)?;
self.goto_xy(1, line)?;
let fitted = self.codec.fit_line(text);
self.write_text(&fitted)
}
/// Выводит подготовленный кадр с начала строки без предварительной очистки.
///
/// В отличие от [`Vfd::print_line`], метод не выполняет типографскую нормализацию до
/// подгонки ширины. Управляющие символы всё равно нейтрализуются на этапе кодирования.
///
/// # Ошибки
///
/// Возвращает [`VfdError::InvalidLine`] или [`VfdError::Io`].
pub fn print_frame(&mut self, line: u8, frame: &str) -> Result<()> {
self.codec.validate_line(line)?;
self.goto_xy(1, line)?;
let fitted = fit_to_width(frame, self.columns());
self.write_text(&fitted)
}
/// Печатает текст с координаты `(x, y)`, выполняя санацию и обрезку по правому краю.
///
/// Координаты задаются от единицы. Если текст длиннее оставшегося места в строке,
/// лишние символы отбрасываются, а следующая строка не затрагивается.
///
/// # Ошибки
///
/// Возвращает [`VfdError::InvalidCoordinate`] или [`VfdError::Io`].
pub fn print_at(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
self.print_at_prepared(x, y, text)
}
/// Печатает уже подготовленный текст; управляющие символы нейтрализуются codec-ом.
pub(crate) fn print_at_prepared(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
self.codec.validate_xy(x, y)?;
let remaining = self.columns() - usize::from(x) + 1;
let text = truncate_chars(text, remaining).to_string();
self.goto_xy(x, y)?;
self.write_text(&text)
}
/// Записывает байты напрямую без кодировки и проверки содержимого.
///
/// Используйте этот escape hatch только для команд, которых нет в типизированном API,
/// или для нестандартных таблиц символов конкретного устройства.
///
/// # Ошибки
///
/// Возвращает [`VfdError::Io`] при ошибке записи.
pub fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
self.transport.write_all(bytes)?;
Ok(())
}
/// Устанавливает яркость.
///
/// Команда кодируется как `US X n` (`0x1F 0x58 level`). После записи выполняется
/// `flush`, затем sync sleep на `display.brightness_settle`, если задержка не нулевая.
///
/// # Ошибки
///
/// Возвращает [`VfdError::UnsupportedBrightness`], если уровень вне настроенного
/// диапазона или яркость отключена, и [`VfdError::Io`] при ошибке записи/flush.
pub fn set_brightness(&mut self, level: u8) -> Result<()> {
let cmd = self.codec.brightness(level)?;
self.transport.write_all(&cmd)?;
self.transport.flush()?;
let settle = self.display().brightness_settle;
if !settle.is_zero() {
std::thread::sleep(settle);
}
Ok(())
}
/// Завершает буферизированные записи транспорта.
///
/// # Ошибки
///
/// Возвращает [`VfdError::Io`], если внутренний транспорт не смог выполнить flush.
pub fn flush(&mut self) -> Result<()> {
self.transport.flush()?;
Ok(())
}
/// Возвращает внутренний транспорт.
///
/// Метод потребляет драйвер. Это удобно в тестах, где внутренним транспортом служит
/// `Vec<u8>`, или при передаче ownership обратно вызывающему коду после shutdown.
pub fn into_inner(self) -> T {
self.transport
}
fn goto_xy(&mut self, x: u8, y: u8) -> Result<()> {
let cmd = self.codec.goto_xy(x, y)?;
self.transport.write_all(&cmd)?;
Ok(())
}
fn write_text(&mut self, s: &str) -> Result<()> {
let bytes = self.codec.encode_text(s);
self.transport.write_all(&bytes)?;
Ok(())
}
}
impl From<std::convert::Infallible> for VfdError {
fn from(value: std::convert::Infallible) -> Self {
match value {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{DisplaySettings, Preset, TextEncoding, VfdConfig};
#[test]
fn mock_transport_gets_legacy_preset_bytes() {
let cfg = VfdConfig::preset("test", Preset::Epson20x2Cp866).unwrap();
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), cfg.display).unwrap();
vfd.clear().unwrap();
vfd.print_line(1, "Привет").unwrap();
vfd.print_at(20, 2, "X!").unwrap();
vfd.set_brightness(4).unwrap();
let bytes = vfd.into_inner();
let mut expected = vec![0x1B, 0x40, 0x1B, 0x74, 6, 0x0C];
expected.extend_from_slice(&[0x1F, 0x24, 1, 1]);
expected.extend_from_slice(&encoding_rs::IBM866.encode("Привет ").0);
expected.extend_from_slice(&[0x1F, 0x24, 20, 2, b'X']);
expected.extend_from_slice(&[0x1F, 0x58, 4]);
assert_eq!(bytes, expected);
}
#[test]
fn raw_write_bypasses_encoding() {
let display = DisplaySettings::new(20, 2, TextEncoding::Ascii);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
vfd.write_raw(&[0x1B, b'?', 0xFF]).unwrap();
assert_eq!(vfd.into_inner(), vec![0x1B, 0x40, 0x1B, b'?', 0xFF]);
}
#[test]
fn invalid_coordinates_and_brightness_are_errors() {
let display = DisplaySettings::new(20, 3, TextEncoding::Cp866);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
assert!(matches!(
vfd.print_line(4, "bad"),
Err(VfdError::InvalidLine { .. })
));
assert!(matches!(
vfd.print_at(21, 1, "bad"),
Err(VfdError::InvalidCoordinate { .. })
));
assert!(matches!(
vfd.set_brightness(5),
Err(VfdError::UnsupportedBrightness { .. })
));
}
#[test]
fn alternate_encodings_are_selectable_manually() {
let display = DisplaySettings::new(4, 1, TextEncoding::Windows1251);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
vfd.print_line(1, "т").unwrap();
assert_eq!(
vfd.into_inner(),
vec![0x1B, 0x40, 0x1F, 0x24, 1, 1, 0xF2, b' ', b' ', b' ']
);
}
#[test]
fn high_level_text_neutralizes_protocol_controls() {
let display = DisplaySettings::new(8, 1, TextEncoding::Ascii);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
vfd.print_line(1, "A\u{1b}@B\u{0c}C").unwrap();
assert_eq!(&vfd.into_inner()[6..], b"A @B C ");
}
}