# escpos-vfd `escpos-vfd` — Rust-библиотека для символьных VFD-дисплеев покупателя (customer display), которые работают через serial-порт и понимают Epson/ESC/POS-совместимые команды. Настройки порта и дисплея разделены: можно независимо задать скорость, геометрию, кодировку, аппаратную таблицу символов и яркость. Для проверенного 20×2 дисплея с CP866 есть готовый пресет `Preset::Epson20x2Cp866`. ![VFD-дисплей покупателя с кириллическим текстом](docs/display.jpg) ## Установка ```toml [dependencies] escpos-vfd = "0.2" ``` Tokio API подключается отдельным feature: ```toml [dependencies] escpos-vfd = { version = "0.2", features = ["tokio"] } ``` Без feature `tokio` async-зависимости не подключаются. ## Быстрый старт Для 20×2 дисплея с `9600 8N1`, CP866 и таблицей `ESC t 6` достаточно готового пресета: ```rust,no_run use escpos_vfd::{Preset, Vfd, VfdConfig}; fn main() -> escpos_vfd::Result<()> { let config = VfdConfig::preset( "/dev/cu.usbmodem101", Preset::Epson20x2Cp866, )?; let mut display = Vfd::open(config)?; display.clear()?; display.set_brightness(2)?; display.print_line(1, "Привет!")?; display.print_line(2, "escpos-vfd")?; Ok(()) } ``` Полный пример: [examples/preset_sync.rs](examples/preset_sync.rs). ## Что выбрать | Задача | API | | --- | --- | | Запустить проверенный 20×2 CP866 дисплей | `VfdConfig::preset(...)` | | Настроить другой дисплей | `SerialSettings` + `DisplaySettings` | | Писать напрямую из текущего потока | `Vfd` | | Отправлять команды из нескольких потоков | `VfdWorker` + `VfdHandle` | | Писать напрямую из Tokio task | `tokio::AsyncVfd` | | Отправлять команды из нескольких Tokio tasks | `tokio::AsyncVfdWorker` | | Отправить нестандартную команду | `write_raw(...)` | ## Настройка другого дисплея Пресет не обязателен. Параметры serial-порта и дисплея можно задать вручную: ```rust,no_run use escpos_vfd::{DisplaySettings, SerialSettings, TextEncoding, Vfd, VfdConfig}; use serialport::{DataBits, FlowControl, Parity, StopBits}; use std::time::Duration; fn main() -> escpos_vfd::Result<()> { let mut serial = SerialSettings::new("/dev/ttyUSB0", 9_600); serial.data_bits = DataBits::Eight; serial.parity = Parity::None; serial.stop_bits = StopBits::One; serial.flow_control = FlowControl::None; serial.timeout = Duration::from_millis(100); let mut display = DisplaySettings::new(20, 2, TextEncoding::Cp866); display.code_table = Some(6); display.brightness = Some(1..=4); let config = VfdConfig::new(serial, display)?; let mut vfd = Vfd::open(config)?; vfd.print_line(1, "Ручной режим")?; vfd.print_at(1, 2, "20x2, CP866")?; Ok(()) } ``` Полный пример: [examples/manual_sync.rs](examples/manual_sync.rs). ### Serial-порт `SerialSettings::new(port, baud_rate)` использует обычные значения `8N1`, без flow control, с тайм-аутом `100 ms`. На Unix порт по умолчанию открывается эксклюзивно. Все эти параметры можно изменить через поля `SerialSettings`. ### Дисплей `DisplaySettings::new(columns, rows, encoding)` задаёт геометрию и текстовую кодировку. Размеры должны быть в диапазоне `1..=255`. Дополнительно можно настроить: - `code_table` — аппаратную таблицу символов через `ESC t n`; - `reset_on_open` — отправку `ESC @` при открытии; - `brightness` — допустимый диапазон яркости; - `brightness_settle` — паузу после изменения яркости. `VfdConfig` также содержит `queue_capacity` для worker API. По умолчанию очередь вмещает 32 команды; значение `0` запрещено. ## Кодировка и таблица символов Кодировка текста и таблица символов самого дисплея — разные настройки. `TextEncoding` определяет, как Rust-строка превращается в байты: | Значение | Назначение | | --- | --- | | `Cp866` | CP866 / IBM866 | | `Windows1251` | Windows-1251 / CP1251 | | `Ascii` | только ASCII, остальные символы заменяются на `?` | | `Utf8` | UTF-8 без перекодирования | `code_table` отвечает за команду `ESC t n`. Например, конкретному CP866-дисплею могут одновременно понадобиться: ```rust let mut display = DisplaySettings::new(20, 2, TextEncoding::Cp866); display.code_table = Some(6); ``` Если таблица уже выбрана DIP-переключателями или устройство использует другой механизм, оставьте `code_table = None`. Перед обычным выводом библиотека заменяет некоторые типографские символы на более безопасные для символьного VFD варианты: например, длинное тире на `-`, `№` на `#`, а табуляцию на пробел. ## Вывод текста Все публичные координаты начинаются с **1**: ```rust vfd.print_line(1, "Первая строка")?; vfd.print_at(6, 2, "текст")?; ``` `print_line()` обрезает слишком длинный текст и дополняет короткий пробелами до ширины дисплея. Это позволяет полностью перезаписать строку без остатка предыдущего текста. `print_at()` пишет с указанной позиции и обрезает текст по правому краю. Автоматического переноса на следующую строку нет. Неверные координаты возвращают `VfdError::InvalidLine` или `VfdError::InvalidCoordinate`. ## Worker и частичные обновления `VfdWorker` владеет дисплеем в отдельном потоке, а `VfdHandle` можно клонировать и передавать между потоками. Очередь ограничена по размеру: если она заполнена, отправитель ждёт свободное место вместо неограниченного накопления команд. Вызов метода handle завершается после фактического выполнения команды или ошибки I/O. ```rust,no_run use escpos_vfd::{Preset, VfdConfig, VfdWorker}; fn main() -> escpos_vfd::Result<()> { let config = VfdConfig::preset( "/dev/cu.usbmodem101", Preset::Epson20x2Cp866, )?; let worker = VfdWorker::start(config)?; let display = worker.handle(); display.print_line_diff(1, "Temp: 21.5 C")?; display.print_line_diff(2, "Humidity: 42%")?; worker.shutdown()?; Ok(()) } ``` `print_line_diff()` хранит кэш последнего содержимого строк. Если текст не изменился, запись не выполняется; если изменился только фрагмент, worker отправляет только изменённые смежные диапазоны. Это удобно для часов, статусов и других часто обновляемых значений. Для нормального завершения используйте `VfdWorker::shutdown()`. ### Бегущая строка Worker также умеет обновлять marquee по таймеру: ```rust use std::time::Duration; display.set_marquee_text("Длинный текст для бегущей строки")?; display.start_marquee(1, 8, Duration::from_millis(1500))?; // ... display.stop_marquee()?; ``` `cps` задаёт скорость в символах в секунду, `end_pause` — паузу после полного прохода. `stop_marquee()` останавливает анимацию, но не очищает уже отображённый текст. ## Tokio Feature `tokio` добавляет два варианта API: - `AsyncVfd` — прямой драйвер поверх `AsyncWrite`; - `AsyncVfdWorker` — одна задача-писатель, bounded queue и клонируемый `AsyncVfdHandle`. ```rust,no_run use escpos_vfd::tokio::AsyncVfdWorker; use escpos_vfd::{Preset, VfdConfig}; #[tokio::main] async fn main() -> escpos_vfd::Result<()> { let config = VfdConfig::preset( "/dev/cu.usbmodem101", Preset::Epson20x2Cp866, )?; let worker = AsyncVfdWorker::start(config).await?; let display = worker.handle(); display.print_line_diff(1, "Tokio worker").await?; display.print_line_diff(2, "async I/O").await?; worker.shutdown().await?; Ok(()) } ``` После попадания команды в очередь отмена ожидающего future не отменяет уже поставленную запись в устройство. Для корректного завершения используйте `AsyncVfdWorker::shutdown().await`. ## Низкоуровневая запись Если нужной команды нет в типизированном API, байты можно отправить напрямую: ```rust vfd.write_raw(&[0x1b, 0x40])?; ``` `write_raw()` не кодирует и не интерпретирует данные. В worker API такая запись также не обновляет строковый кэш, поэтому после raw-команд, меняющих текст на экране, лучше выполнить `clear()` или `print_line()`. Для нестандартных транспортов и диагностики доступен публичный `escpos_vfd::codec::EpsonCodec`, который формирует байты команд без открытия serial-порта. ## Ошибки Библиотека использует `escpos_vfd::Result = Result` и разделяет ошибки по смыслу: | Ошибка | Когда возникает | | --- | --- | | `VfdError::Config(...)` | неверная конфигурация | | `VfdError::Serial(...)` | ошибка открытия или настройки serial-порта | | `VfdError::Io(...)` | ошибка записи или `flush` | | `VfdError::InvalidCoordinate` | координата вне дисплея | | `VfdError::InvalidLine` | строка вне `1..=rows` | | `VfdError::UnsupportedBrightness` | неподдерживаемый уровень яркости | | `VfdError::QueueClosed` | очередь worker-а закрыта | | `VfdError::WorkerStopped` | worker остановился до подтверждения команды | | `VfdError::WorkerPanicked` | sync worker завершился с panic | | `VfdError::WorkerCancelled` | Tokio worker был отменён | ## Примеры В репозитории есть небольшие аппаратные примеры для основных сценариев: | Пример | Что показывает | | --- | --- | | `preset_sync` | запуск с готовым пресетом | | `manual_sync` | ручную конфигурацию | | `tokio_worker` | Tokio worker | | `clock` | частые diff-обновления | | `marquee` | бегущую строку | | `brightness` | яркость | | `position` | позиционирование | | `update_at` | частичные обновления | | `blink` | повторную запись в позицию | Например: ```bash cargo run --example preset_sync -- /dev/cu.usbmodem101 cargo run --example marquee -- /dev/cu.usbmodem101 20 8 1500 2 cargo run --features tokio --example tokio_worker -- /dev/cu.usbmodem101 20 ``` ## Проверка на реальном дисплее Автоматические тесты проверяют формирование команд, кодировки, координаты, кэш строк, worker queue, marquee и async-поведение на тестовых транспортах. Конкретное устройство всё равно стоит проверить отдельно: 1. Выставьте правильный serial/DIP-режим. 2. Запустите `cargo run --example preset_sync -- `. 3. Проверьте очистку, строки, кириллицу и яркость. 4. Проверьте `update_at` и `marquee`. 5. При использовании Tokio запустите пример `tokio_worker`. ## Разработка ```bash cargo fmt --all -- --check cargo clippy --all-targets --no-default-features --locked -- -D warnings cargo clippy --all-targets --all-features --locked -- -D warnings cargo test --all-targets --no-default-features --locked cargo test --all-targets --all-features --locked RUSTDOCFLAGS='-D warnings -D missing_docs -D rustdoc::broken_intra_doc_links' \ cargo doc --all-features --no-deps --locked ``` Подробная документация публичного API: [docs.rs/escpos-vfd](https://docs.rs/escpos-vfd). ## Лицензия `escpos-vfd` распространяется под двойной лицензией `MIT OR Apache-2.0`.