Files
vfd/README.md
T

349 lines
15 KiB
Markdown
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.
# 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.3"
```
Tokio API подключается отдельным feature:
```toml
[dependencies]
escpos-vfd = { version = "0.3", 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()` пишет с указанной позиции и обрезает текст по правому краю. Автоматического
переноса на следующую строку нет.
Текстовые методы заменяют управляющие символы, включая `NUL`, `ESC`, `US`, `CR`, `LF`
и form feed, пробелами. Для намеренной отправки команд используется только `write_raw()`.
В CP866 и Windows-1251 каждый неподдерживаемый Unicode-символ заменяется одним `?`,
поэтому кодирование не нарушает настроенную ширину строки.
Неверные координаты возвращают `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` задаёт скорость в символах в секунду и ограничен значением
`MAX_MARQUEE_CPS`; `end_pause` — паузу после полного прохода. Текст ограничен
`MAX_MARQUEE_CHARS` символами.
`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()`. Размер одной 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` | повторную запись в позицию |
Например:
```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 -- <port>`.
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`.