2026-08-21 09:40:19 +05:00
2026-08-20 19:25:08 +05:00
2026-08-20 19:25:08 +05:00
2026-08-20 19:25:08 +05:00
2026-01-28 01:36:35 +05:00
2026-08-20 19:25:08 +05:00
2026-08-20 19:25:08 +05:00
2026-08-20 19:25:08 +05:00
2026-08-21 09:40:19 +05:00
2026-08-20 19:25:08 +05:00

escpos-vfd

crates.io docs.rs rust license

escpos-vfd — Rust-библиотека для символьных VFD-дисплеев покупателя (customer display), которые работают через serial-порт и понимают Epson/ESC/POS-совместимые команды.

Настройки порта и дисплея разделены: можно независимо задать скорость, геометрию, кодировку, аппаратную таблицу символов и яркость. Для проверенного 20×2 дисплея с CP866 есть готовый пресет Preset::Epson20x2Cp866.

VFD-дисплей покупателя с кириллическим текстом

Установка

[dependencies]
escpos-vfd = "0.3"

Tokio API подключается отдельным feature:

[dependencies]
escpos-vfd = { version = "0.3", 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() пишет с указанной позиции и обрезает текст по правому краю. Автоматического переноса на следующую строку нет.

Текстовые методы заменяют управляющие символы, включая NUL, ESC, US, CR, LF и form feed, пробелами. Для намеренной отправки команд используется только write_raw(). В CP866 и Windows-1251 каждый неподдерживаемый Unicode-символ заменяется одним ?, поэтому кодирование не нарушает настроенную ширину строки.

Неверные координаты возвращают 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 задаёт скорость в символах в секунду и ограничен значением MAX_MARQUEE_CPS; end_pause — паузу после полного прохода. Текст ограничен MAX_MARQUEE_CHARS символами. 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(). Размер одной queued raw-команды ограничен MAX_QUEUED_RAW_BYTES; низкоуровневые Vfd::write_raw() и AsyncVfd::write_raw() пишут предоставленный slice напрямую без промежуточной очереди.

Для нестандартных транспортов и диагностики доступен публичный escpos_vfd::codec::EpsonCodec, который формирует байты команд без открытия serial-порта. EpsonCodec::new(display) валидирует геометрию и возвращает Result.

Ошибки

Библиотека использует 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-поведение на тестовых транспортах. Конкретное устройство всё равно стоит проверить отдельно:

  1. Выставьте правильный serial/DIP-режим.
  2. Запустите cargo run --example preset_sync -- <port>.
  3. Проверьте очистку, строки, кириллицу и яркость.
  4. Проверьте update_at и marquee.
  5. При использовании 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.

S
Description
Rust-библиотека для управления VFD-дисплеями Epson/ESC-совместимого режима
Readme
6.1 MiB
Languages
Rust 100%