рефакторинг библиотеки.

This commit is contained in:
Кобелев Андрей Андреевич
2026-08-15 23:53:45 +05:00
parent 896670b62f
commit efc53e6ef5
13 changed files with 584 additions and 371 deletions
+82 -23
View File
@@ -6,15 +6,21 @@ use std::time::Duration;
const TABLE_CYR: u8 = 6; // PD-2600: your Cyrillic table (ESC t 6)
const FIXED_BAUD: u32 = 9600;
const MAX_WIDTH: usize = u8::MAX as usize;
#[derive(Debug, Clone)]
/// Настройки serial-подключения и геометрии VFD-дисплея.
pub struct VfdConfig {
/// Имя serial-порта, например `/dev/cu.usbmodem101`.
pub port_name: String,
/// Число символов в строке; ограничивается диапазоном `1..=255`.
pub width: usize,
/// Тайм-аут операций чтения и записи serial-порта.
pub timeout: Duration,
}
impl VfdConfig {
/// Создаёт конфигурацию с шириной 20 символов и тайм-аутом 100 мс.
pub fn new(port_name: impl Into<String>) -> Self {
Self {
port_name: port_name.into(),
@@ -23,25 +29,27 @@ impl VfdConfig {
}
}
/// Устанавливает ширину строки с учётом диапазона координат протокола.
pub fn with_width(mut self, width: usize) -> Self {
self.width = width.max(1);
self.width = width.clamp(1, MAX_WIDTH);
self
}
#[allow(dead_code)]
/// Устанавливает тайм-аут операций serial-порта.
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
}
/// Низкоуровневое соединение с дисплеем, владеющее serial-портом.
pub struct Vfd {
port: Box<dyn SerialPort>,
pub width: usize,
blank_line: String,
}
impl Vfd {
/// Открывает serial-порт и инициализирует дисплей с таблицей CP866.
pub fn open(cfg: VfdConfig) -> Result<Self> {
let mut port = serialport::new(cfg.port_name, FIXED_BAUD)
.timeout(cfg.timeout)
@@ -55,61 +63,69 @@ impl Vfd {
Ok(Self {
port,
width: cfg.width,
blank_line: " ".repeat(cfg.width),
})
}
/// Очищает обе строки дисплея стандартной командой очистки.
pub fn clear(&mut self) -> std::io::Result<()> {
self.port.write_all(&[0x0C])
}
/// Полностью перезаписывает первую или вторую строку текстом фиксированной ширины.
pub fn print_line(&mut self, line: u8, text: &str) -> std::io::Result<()> {
if !(1..=2).contains(&line) {
return Ok(());
}
// overwrite the whole line to avoid leftovers
self.goto_xy(1, line)?;
// FIX: take owned copy so self isn't immutably borrowed during &mut self call
let blank = self.blank_line.clone();
self.write_cp866(&blank)?;
// Кадр фиксированной ширины сам перезаписывает остаток предыдущей строки.
self.goto_xy(1, line)?;
let fitted = fit_to_width(&sanitize_for_cp866(text), self.width);
self.write_cp866(&fitted)
}
/// Print already-prepared fixed-width frame at (1, line) without clearing.
/// Use this for marquee to avoid double-writes.
/// Выводит подготовленный кадр с начала строки без предварительной очистки.
pub fn print_frame(&mut self, line: u8, frame: &str) -> std::io::Result<()> {
if !(1..=2).contains(&line) {
// silently ignore invalid lines (or return an error if you want)
// Некорректный номер строки игнорируется так же, как в остальных методах печати.
return Ok(());
}
self.goto_xy(1, line)?;
// frame should already be width-sized; if not, fit it
// На случай короткого или длинного кадра приводим его к ширине дисплея.
let fitted = fit_to_width(frame, self.width);
self.write_cp866(&fitted)
}
#[allow(dead_code)]
/// Печатает текст с координаты `(x, y)`, выполняя санацию и обрезку по правому краю.
pub fn print_at(&mut self, x: u8, y: u8, text: &str) -> std::io::Result<()> {
self.goto_xy(x, y)?;
self.write_cp866(&sanitize_for_cp866(text))
let text = sanitize_for_cp866(text);
self.print_at_prepared(x, y, &text)
}
/// Печатает уже подготовленный текст без повторной санации.
pub(crate) fn print_at_prepared(&mut self, x: u8, y: u8, text: &str) -> std::io::Result<()> {
if x == 0 || usize::from(x) > self.width || !(1..=2).contains(&y) {
return Ok(());
}
let remaining = self.width - usize::from(x) + 1;
let text = truncate_chars(text, remaining);
self.goto_xy(x, y)?;
self.write_cp866(text)
}
/// Перемещает аппаратный курсор в координаты дисплея, заданные от единицы.
fn goto_xy(&mut self, x: u8, y: u8) -> std::io::Result<()> {
// US $ x y
self.port.write_all(&[0x1F, 0x24, x, y])
}
/// Кодирует строку в CP866 и целиком записывает байты в serial-порт.
fn write_cp866(&mut self, s: &str) -> std::io::Result<()> {
let (bytes, _, _) = IBM866.encode(s);
self.port.write_all(&bytes)
}
/// Epson customer display: US X n (brightness), n=1..4
/// Устанавливает яркость; значение ограничивается диапазоном `1..=4`.
pub fn set_brightness(&mut self, n: u8) -> Result<()> {
let n = n.clamp(1, 4);
let cmd = [0x1F, 0x58, n]; // US 'X' n
@@ -120,6 +136,14 @@ impl Vfd {
}
}
/// Возвращает срез не длиннее заданного числа символов, не разрывая UTF-8.
fn truncate_chars(s: &str, max_chars: usize) -> &str {
s.char_indices()
.nth(max_chars)
.map_or(s, |(byte_index, _)| &s[..byte_index])
}
/// Заменяет типографские символы на безопасные аналоги, представимые в CP866.
pub fn sanitize_for_cp866(s: &str) -> String {
s.chars()
.map(|c| match c {
@@ -134,11 +158,46 @@ pub fn sanitize_for_cp866(s: &str) -> String {
.collect()
}
/// Обрезает строку по числу символов и дополняет пробелами до заданной ширины.
pub fn fit_to_width(s: &str, width: usize) -> String {
let mut out: String = s.chars().take(width).collect();
let len = out.chars().count();
if len < width {
out.push_str(&" ".repeat(width - len));
let mut out = String::with_capacity(width);
let mut len = 0;
for ch in s.chars().take(width) {
out.push(ch);
len += 1;
}
out.extend(std::iter::repeat_n(' ', width - len));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_width_stays_within_protocol_coordinate_range() {
assert_eq!(VfdConfig::new("test").with_width(0).width, 1);
assert_eq!(VfdConfig::new("test").with_width(20).width, 20);
assert_eq!(VfdConfig::new("test").with_width(usize::MAX).width, 255);
}
#[test]
fn sanitizes_typographic_characters_without_changing_regular_text() {
assert_eq!(sanitize_for_cp866("№1\t“тест”—‘да’…"), "#1 \"тест\"-'да'.");
assert_eq!(sanitize_for_cp866("обычный text"), "обычный text");
}
#[test]
fn fits_unicode_by_characters_and_pads_short_input() {
assert_eq!(fit_to_width("Привет", 4), "Прив");
assert_eq!(fit_to_width("да", 4), "да ");
assert_eq!(fit_to_width("text", 0), "");
}
#[test]
fn truncates_without_splitting_utf8_characters() {
assert_eq!(truncate_chars("ёжик", 3), "ёжи");
assert_eq!(truncate_chars("ёжик", 10), "ёжик");
assert_eq!(truncate_chars("ёжик", 0), "");
}
}