# 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-порт автоматически.