237 lines
10 KiB
Markdown
237 lines
10 KiB
Markdown
# VFD — управление дисплеями в Epson-совместимом режиме
|
||
|
||
Небольшая Rust-библиотека для двухстрочных VFD-дисплеев покупателя, работающих через
|
||
serial-порт в Epson/ESC-совместимом режиме. Проект ориентирован на дисплеи семейства
|
||
PD-2600/PD-2800 и похожие модели с поддержкой команд позиционирования, яркости и
|
||
таблицы символов CP866.
|
||
|
||
Библиотека подходит для часов, локальных дашбордов, кассовых приложений, уведомлений,
|
||
индикаторов состояния и других проектов, где данные нужно обновлять без мерцания всей
|
||
строки.
|
||
|
||
## Возможности
|
||
|
||
- вывод текста в первую или вторую строку;
|
||
- печать с координаты `(x, y)`;
|
||
- обновление только изменившихся диапазонов строки через `print_line_diff`;
|
||
- фоновая бегущая строка с регулируемой скоростью и паузой;
|
||
- четыре уровня яркости;
|
||
- кириллица через CP866;
|
||
- потокобезопасный интерфейс `VfdHandle`, который можно клонировать;
|
||
- автоматическая обрезка текста по правому краю и дополнение строк пробелами.
|
||
|
||
## Поддерживаемый протокол
|
||
|
||
При открытии устройства библиотека использует следующие параметры и команды:
|
||
|
||
| Параметр | Значение |
|
||
| --- | --- |
|
||
| Скорость serial-порта | `9600 baud` |
|
||
| Таблица символов | CP866, команда `ESC t 6` |
|
||
| Инициализация | `ESC @` |
|
||
| Очистка | `FF` (`0x0C`) |
|
||
| Координаты | `US $ x y`, нумерация от 1 |
|
||
| Яркость | `US X n`, где `n = 1..4` |
|
||
| Число строк | 2 |
|
||
| Допустимая ширина | `1..=255` символов |
|
||
|
||
Перед запуском убедитесь, что дисплей переведён DIP-переключателями в совместимый
|
||
режим. Точные настройки конкретной модели смотрите в документации из каталога
|
||
[`docs`](docs/).
|
||
|
||
## Подключение
|
||
|
||
Для локального проекта добавьте библиотеку как path-зависимость:
|
||
|
||
```toml
|
||
[dependencies]
|
||
m = { path = "../vfd" }
|
||
anyhow = "1"
|
||
```
|
||
|
||
Имя crate сейчас — `m`, поэтому импорт начинается с `m::`.
|
||
|
||
## Быстрый старт
|
||
|
||
```rust,no_run
|
||
use anyhow::Result;
|
||
use m::vfd::VfdConfig;
|
||
use m::worker::VfdWorker;
|
||
use std::time::Duration;
|
||
|
||
fn main() -> Result<()> {
|
||
let config = VfdConfig::new("/dev/cu.usbmodem101")
|
||
.with_width(20);
|
||
|
||
// Serial-порт открывается до запуска фонового потока,
|
||
// поэтому ошибка подключения вернётся из start().
|
||
let worker = VfdWorker::start(config)?;
|
||
let display = worker.handle();
|
||
|
||
display.clear();
|
||
display.set_brightness(2);
|
||
display.print_line_diff(1, "Привет!")?;
|
||
display.print_line_diff(2, "VFD готов")?;
|
||
|
||
// Команды выполняются асинхронно: не завершаем программу сразу после отправки.
|
||
std::thread::sleep(Duration::from_secs(2));
|
||
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
`VfdWorker` последовательно выполняет команды в отдельном потоке. При удалении worker
|
||
отправляет команду завершения и дожидается остановки потока. Сохраняйте `worker` в
|
||
области видимости, пока дисплей используется.
|
||
|
||
## Основной API
|
||
|
||
### Конфигурация
|
||
|
||
```rust,no_run
|
||
use m::vfd::VfdConfig;
|
||
use std::time::Duration;
|
||
|
||
let config = VfdConfig::new("/dev/ttyUSB0")
|
||
.with_width(20)
|
||
.with_timeout(Duration::from_millis(250));
|
||
```
|
||
|
||
Ширина автоматически ограничивается диапазоном `1..=255`, потому что координаты
|
||
протокола передаются одним байтом.
|
||
|
||
### Полная и дифференциальная запись
|
||
|
||
```rust,ignore
|
||
display.print_line(1, "Полная перезапись")?;
|
||
display.print_line_diff(2, "Изменились цифры: 42")?;
|
||
```
|
||
|
||
`print_line_diff` хранит последний кадр каждой строки и группирует соседние изменения.
|
||
Если текст не изменился, serial-команда не отправляется. Это полезно для часов и
|
||
дашбордов, которые обновляются часто.
|
||
|
||
### Печать по координатам
|
||
|
||
```rust,ignore
|
||
display.print_at(7, 1, "21.50")?;
|
||
display.print_at(16, 1, "1200")?;
|
||
```
|
||
|
||
Координаты начинаются с единицы. Допустимы строки `1` и `2`; текст, выходящий за правую
|
||
границу, обрезается по символам без повреждения UTF-8.
|
||
|
||
### Бегущая строка
|
||
|
||
```rust,ignore
|
||
use std::time::Duration;
|
||
|
||
display.set_marquee_text("Длинное сообщение для посетителя");
|
||
display.start_marquee(
|
||
2, // строка
|
||
8, // символов в секунду
|
||
Duration::from_millis(1500), // пауза после полного прохода
|
||
);
|
||
|
||
// При необходимости:
|
||
display.stop_marquee();
|
||
```
|
||
|
||
Обычные команды записи в строку, занятую marquee, игнорируются, чтобы два источника
|
||
не перезаписывали друг друга.
|
||
|
||
## Примеры
|
||
|
||
Все примеры принимают два общих аргумента:
|
||
|
||
1. serial-порт, по умолчанию `/dev/cu.usbmodem101`;
|
||
2. ширина дисплея, по умолчанию `20`.
|
||
|
||
Дополнительные аргументы зависят от примера:
|
||
|
||
| Пример | Что демонстрирует | Дополнительные аргументы |
|
||
| --- | --- | --- |
|
||
| `clock` | Дату, время, день недели и часть суток | `[brightness=2]` |
|
||
| `marquee` | Фоновую бегущую строку | `[cps=8] [pause_ms=1500] [brightness=2]` |
|
||
| `brightness` | Перебор уровней яркости `1..=4` | `[delay_ms=800]` |
|
||
| `position` | Движение символа по второй строке | `[delay_ms=80]` |
|
||
| `update_at` | Частичное обновление полей дашборда | `[delay_ms=200]` |
|
||
| `blink` | Мигание текста в заданной позиции | `[delay_ms=400] [x=1] [y=1] [text=BLINK]` |
|
||
|
||
Примеры запуска:
|
||
|
||
```bash
|
||
cargo run --example clock -- /dev/cu.usbmodem101 20 2
|
||
cargo run --example marquee -- /dev/cu.usbmodem101 20 8 1500 2
|
||
cargo run --example brightness -- /dev/cu.usbmodem101 20 800
|
||
cargo run --example position -- /dev/cu.usbmodem101 20 80
|
||
cargo run --example update_at -- /dev/cu.usbmodem101 20 200
|
||
cargo run --example blink -- /dev/cu.usbmodem101 20 400 1 2 BLINK
|
||
```
|
||
|
||
`update_at` использует фиксированную 20-символьную раскладку и завершится с понятной
|
||
ошибкой, если передать меньшую ширину.
|
||
|
||
Те же программы доступны через [Task](https://taskfile.dev/):
|
||
|
||
```bash
|
||
task vfd:clock
|
||
task vfd:marquee
|
||
task vfd:brightness
|
||
task vfd:position
|
||
task vfd:update_at
|
||
task vfd:blink
|
||
```
|
||
|
||
Значения можно переопределять переменными, например:
|
||
|
||
```bash
|
||
task vfd:marquee VFD_PORT=/dev/ttyUSB0 VFD_WIDTH=20 VFD_CPS=10
|
||
```
|
||
|
||
## Кодировка текста
|
||
|
||
Перед записью библиотека заменяет несколько типографских символов на совместимые
|
||
аналоги:
|
||
|
||
- `…` → `.`;
|
||
- `—` и `–` → `-`;
|
||
- `№` → `#`;
|
||
- типографские кавычки → обычные кавычки;
|
||
- табуляция → пробел.
|
||
|
||
Остальной текст кодируется с помощью `encoding_rs::IBM866`.
|
||
|
||
## Структура проекта
|
||
|
||
```text
|
||
src/vfd.rs низкоуровневые команды и CP866
|
||
src/worker.rs очередь команд, diff и marquee
|
||
examples/common/mod.rs общие CLI-утилиты примеров
|
||
examples/*.rs демонстрационные программы
|
||
docs/ руководства для дисплеев
|
||
taskfile.yml команды запуска примеров
|
||
```
|
||
|
||
## Разработка и проверка
|
||
|
||
```bash
|
||
cargo fmt --all -- --check
|
||
cargo clippy --all-targets --all-features --locked -- -D warnings
|
||
cargo test --all-targets --locked
|
||
cargo check --examples --locked
|
||
```
|
||
|
||
Автоматические тесты проверяют Unicode, CP866-санацию, ширину строк, обновление кэша,
|
||
группировку diff-диапазонов, таймер marquee и функции примера часов. Для полной проверки
|
||
нужен отдельный smoke-test с физическим дисплеем и фактическим serial-портом.
|
||
|
||
## Ограничения
|
||
|
||
- поддерживаются только две строки;
|
||
- скорость подключения фиксирована на `9600 baud`;
|
||
- очередь команд в worker не ограничена по размеру;
|
||
- аппаратные ошибки после запуска worker выводятся в stderr из фонового потока;
|
||
- пример `clock` использует системную команду `date` и рассчитан на Unix-подобную среду;
|
||
- библиотека не определяет serial-порт автоматически.
|