escpos-vfd 0.3.0

ESC/POS-compatible VFD customer display driver with sync and optional Tokio APIs
Documentation
# 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`.