Rename crate to escpos-vfd and add tokio support Introduce a configurable crate-ready API with presets, typed errors, optional Tokio async support, and expanded documentation.
escpos-vfd
escpos-vfd — Rust-библиотека для символьных VFD-дисплеев покупателя (customer display),
которые работают через serial-порт и понимают Epson/ESC/POS-совместимые команды.
Настройки порта и дисплея разделены: можно независимо задать скорость, геометрию,
кодировку, аппаратную таблицу символов и яркость. Для проверенного 20×2 дисплея с CP866
есть готовый пресет Preset::Epson20x2Cp866.
Установка
[dependencies]
escpos-vfd = "0.2"
Tokio API подключается отдельным feature:
[dependencies]
escpos-vfd = { version = "0.2", features = ["tokio"] }
Без feature tokio async-зависимости не подключаются.
Быстрый старт
Для 20×2 дисплея с 9600 8N1, CP866 и таблицей ESC t 6 достаточно готового пресета:
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.
Что выбрать
| Задача | API |
|---|---|
| Запустить проверенный 20×2 CP866 дисплей | VfdConfig::preset(...) |
| Настроить другой дисплей | SerialSettings + DisplaySettings |
| Писать напрямую из текущего потока | Vfd |
| Отправлять команды из нескольких потоков | VfdWorker + VfdHandle |
| Писать напрямую из Tokio task | tokio::AsyncVfd |
| Отправлять команды из нескольких Tokio tasks | tokio::AsyncVfdWorker |
| Отправить нестандартную команду | write_raw(...) |
Настройка другого дисплея
Пресет не обязателен. Параметры serial-порта и дисплея можно задать вручную:
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.
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-дисплею могут
одновременно понадобиться:
let mut display = DisplaySettings::new(20, 2, TextEncoding::Cp866);
display.code_table = Some(6);
Если таблица уже выбрана DIP-переключателями или устройство использует другой механизм,
оставьте code_table = None.
Перед обычным выводом библиотека заменяет некоторые типографские символы на более
безопасные для символьного VFD варианты: например, длинное тире на -, № на #, а
табуляцию на пробел.
Вывод текста
Все публичные координаты начинаются с 1:
vfd.print_line(1, "Первая строка")?;
vfd.print_at(6, 2, "текст")?;
print_line() обрезает слишком длинный текст и дополняет короткий пробелами до ширины
дисплея. Это позволяет полностью перезаписать строку без остатка предыдущего текста.
print_at() пишет с указанной позиции и обрезает текст по правому краю. Автоматического
переноса на следующую строку нет.
Неверные координаты возвращают VfdError::InvalidLine или
VfdError::InvalidCoordinate.
Worker и частичные обновления
VfdWorker владеет дисплеем в отдельном потоке, а VfdHandle можно клонировать и
передавать между потоками.
Очередь ограничена по размеру: если она заполнена, отправитель ждёт свободное место вместо неограниченного накопления команд. Вызов метода handle завершается после фактического выполнения команды или ошибки I/O.
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 по таймеру:
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.
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, байты можно отправить напрямую:
vfd.write_raw(&[0x1b, 0x40])?;
write_raw() не кодирует и не интерпретирует данные. В worker API такая запись также не
обновляет строковый кэш, поэтому после raw-команд, меняющих текст на экране, лучше
выполнить clear() или print_line().
Для нестандартных транспортов и диагностики доступен публичный
escpos_vfd::codec::EpsonCodec, который формирует байты команд без открытия serial-порта.
Ошибки
Библиотека использует escpos_vfd::Result<T> = Result<T, VfdError> и разделяет ошибки по
смыслу:
| Ошибка | Когда возникает |
|---|---|
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 |
повторную запись в позицию |
Например:
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-поведение на тестовых транспортах. Конкретное устройство всё равно стоит проверить отдельно:
- Выставьте правильный serial/DIP-режим.
- Запустите
cargo run --example preset_sync -- <port>. - Проверьте очистку, строки, кириллицу и яркость.
- Проверьте
update_atиmarquee. - При использовании Tokio запустите пример
tokio_worker.
Разработка
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.
Лицензия
escpos-vfd распространяется под двойной лицензией MIT OR Apache-2.0.
