Rename crate to escpos-vfd and add tokio support

Rename crate to escpos-vfd and add tokio support

Introduce a configurable crate-ready API with presets, typed errors,
optional Tokio async support, and expanded documentation.
This commit is contained in:
Кобелев Андрей Андреевич
2026-08-16 11:24:20 +05:00
parent efc53e6ef5
commit 244d907cc2
25 changed files with 3823 additions and 782 deletions
+25 -16
View File
@@ -2,43 +2,52 @@
mod common;
use anyhow::Result;
use std::error::Error;
use m::worker::VfdWorker;
use escpos_vfd::VfdWorker;
fn main() -> Result<()> {
// args:
// 1) port (default: /dev/cu.usbmodem101)
// 2) width (default: 20)
// 3) delay_ms (default: 400)
// 4) x (default: 1) 1-based
// 5) y (default: 1) 1-based line
// 6) text (default: "BLINK")
let mut args = common::ExampleArgs::from_env();
fn main() -> Result<(), Box<dyn Error>> {
// Аргументы:
// 1) port - serial-порт дисплея;
// 2) width - ширина строки, нужна для проверки координат;
// 3) delay_ms - пауза между состояниями "видно/пусто";
// 4) x - колонка от единицы;
// 5) y - строка от единицы;
// 6) text - текст, который мигает.
let mut args = common::ExampleArgs::from_env()?;
let delay_ms: u64 = args.parse_or(400);
let x: u8 = args.parse_or(1);
let y: u8 = args.parse_or(1);
let text = args.string_or("BLINK");
let text_len = text.chars().count();
// Worker нужен, чтобы все команды шли в serial-порт последовательно. Даже в одном
// потоке пример получает те же semantics, что и приложение с несколькими producer-ами.
let worker = VfdWorker::start(args.config)?;
let vfd = worker.handle();
vfd.clear();
vfd.clear()?;
let _ = vfd.print_line_diff(1, "blink demo (print_at)");
// Первая строка остаётся статичной, мигает только выбранная область.
vfd.print_line_diff(1, "blink demo (print_at)")?;
if y != 1 {
let _ = vfd.print_at(1, y, ""); // просто чтобы “активировать” строку у некоторых дисплеев
// Пустая запись в начало строки полезна на некоторых дисплеях, которые лениво
// переключают видимую строку только после позиционирования курсора.
vfd.print_at(1, y, "")?;
}
// Стираем ровно столько символов, сколько было выведено. Так справа не остаются
// хвосты при следующем включении текста.
let blank = " ".repeat(text_len);
let mut on = false;
loop {
// Все ошибки (`InvalidCoordinate`, I/O, закрытый worker) сразу выходят из main
// через оператор `?`, что удобно для аппаратного smoke-test.
if on {
let _ = vfd.print_at(x, y, &blank);
vfd.print_at(x, y, &blank)?;
} else {
let _ = vfd.print_at(x, y, &text);
vfd.print_at(x, y, &text)?;
}
on = !on;
common::sleep_ms(delay_ms);
+22 -16
View File
@@ -1,36 +1,42 @@
//! Демонстрирует четыре уровня яркости и обновление строки без полного мерцания.
//! Демонстрирует четыре уровня яркости и обновление строки
mod common;
use anyhow::Result;
use std::error::Error;
use m::worker::VfdWorker;
use escpos_vfd::VfdWorker;
fn main() -> Result<()> {
// args:
// 1) port (default: /dev/cu.usbmodem101)
// 2) width (default: 20)
// 3) delay_ms (default: 800)
let mut args = common::ExampleArgs::from_env();
fn main() -> Result<(), Box<dyn Error>> {
// Аргументы:
// 1) port - serial-порт дисплея (по умолчанию /dev/cu.usbmodem101);
// 2) width - ширина дисплея в символах (по умолчанию 20);
// 3) delay_ms - пауза между уровнями яркости (по умолчанию 800).
let mut args = common::ExampleArgs::from_env()?;
let delay_ms: u64 = args.parse_or(800);
// Worker держит serial-порт в отдельном потоке. Важно сохранить `worker` в
// переменной: если он будет уничтожен, Drop остановит фоновую запись.
let worker = VfdWorker::start(args.config)?;
let vfd = worker.handle();
vfd.clear();
vfd.clear()?;
// шапка один раз (не обязательно, но удобно)
let _ = vfd.print_line_diff(1, "Яркость");
// Шапку пишем один раз. Дальше меняем только вторую строку через diff, чтобы
// дисплей не мерцал от полной перерисовки.
vfd.print_line_diff(1, "Яркость")?;
loop {
for level in 1u8..=4u8 {
vfd.set_brightness(level);
// Пресет текущего дисплея объявляет поддерживаемый диапазон яркости 1..=4.
// Если уровень вне диапазона, метод вернёт typed error.
vfd.set_brightness(level)?;
// обновляем строку медленно, через print_line_diff
// можно сделать “индикатор” уровня (*****)
// Индикатор обновляется с той же паузой, чтобы глазами было видно, какой
// уровень сейчас активен.
let bar = "*".repeat(level as usize);
let line2 = format!("Уровень: {} {}", level, bar);
let _ = vfd.print_line_diff(2, line2);
vfd.print_line_diff(2, line2)?;
common::sleep_ms(delay_ms);
}
+30 -21
View File
@@ -2,13 +2,14 @@
mod common;
use anyhow::Result;
use std::error::Error;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use m::vfd::fit_to_width;
use m::worker::VfdWorker;
use escpos_vfd::{VfdWorker, fit_to_width};
fn run_date(args: &[&str]) -> Option<String> {
// Используем системную `date`, чтобы пример оставался без дополнительных зависимостей.
// Если команда недоступна или вернула ошибку, ниже покажем безопасную заглушку.
let out = std::process::Command::new("date")
.args(args)
.output()
@@ -20,6 +21,7 @@ fn run_date(args: &[&str]) -> Option<String> {
}
fn parse_datetime_and_weekday(value: &str) -> Option<(String, u8)> {
// Формат одной строки: "DD.MM.YYYY HH:MM:SS|N", где N - номер дня недели 1..=7.
let (datetime, weekday) = value.split_once('|')?;
let weekday = weekday.parse::<u8>().ok()?;
(1..=7)
@@ -28,6 +30,8 @@ fn parse_datetime_and_weekday(value: &str) -> Option<(String, u8)> {
}
fn local_datetime_and_weekday() -> (String, u8) {
// Один вызов `date` важен: дата, время и день недели приходят из одного snapshot.
// Это убирает редкие рассинхронизации на границе полуночи.
run_date(&["+%d.%m.%Y %H:%M:%S|%u"])
.as_deref()
.and_then(parse_datetime_and_weekday)
@@ -70,8 +74,8 @@ fn time_of_day_ru(hour: u8) -> &'static str {
}
fn extract_hour(datetime: &str) -> u8 {
// ожидаем "DD.MM.YYYY HH:MM:SS"
// берём HH как 2 символа после пробела
// Ожидаем "DD.MM.YYYY HH:MM:SS" и берём HH как два символа после пробела.
// При неожиданном формате возвращаем 12, чтобы пример продолжал работать.
datetime
.split_whitespace()
.nth(1)
@@ -80,43 +84,48 @@ fn extract_hour(datetime: &str) -> u8 {
.unwrap_or(12)
}
fn main() -> Result<()> {
// args:
// 1) port (default: /dev/cu.usbmodem101)
// 2) width (default: 20)
// 3) brightness 1..4 (default: 2)
let mut args = common::ExampleArgs::from_env();
fn main() -> Result<(), Box<dyn Error>> {
// Аргументы:
// 1) port - serial-порт дисплея;
// 2) width - ширина строки, нужна для подгонки текста;
// 3) brightness - яркость из диапазона пресета 1..=4.
let mut args = common::ExampleArgs::from_env()?;
let brightness: u8 = args.parse_or(2);
let width = args.width;
let columns = args.columns;
// Worker остаётся жить до конца процесса. Handle можно клонировать, но сам worker
// владеет serial-портом и фоновым потоком записи.
let worker = VfdWorker::start(args.config)?;
let vfd = worker.handle();
vfd.clear();
vfd.set_brightness(brightness);
vfd.clear()?;
vfd.set_brightness(brightness)?;
loop {
let (dt, wd) = local_datetime_and_weekday();
let hour = extract_hour(&dt);
let tod = time_of_day_ru(hour);
// 1 строка: "28.01.2006 12:03:34"
let line1 = fit_to_width(&dt, width);
// 1 строка: "28.01.2006 12:03:34". `fit_to_width` обрезает по символам и
// дополняет пробелами, чтобы старый текст справа не оставался на дисплее.
let line1 = fit_to_width(&dt, columns);
// 2 строка: "Понедельник сейчас день"
// но если не влазит — "Пн сейчас день"
// 2 строка: сначала пробуем полное название дня недели, если не влазит -
// короткое. Это показывает, как приложение может адаптироваться к ширине VFD.
let full = format!("{} сейчас {}", weekday_ru_full(wd), tod);
let mut line2 = full;
if line2.chars().count() > width {
if line2.chars().count() > columns {
line2 = format!("{} сейчас {}", weekday_ru_short(wd), tod);
}
line2 = fit_to_width(&line2, width);
line2 = fit_to_width(&line2, columns);
vfd.print_line_diff(1, line1)?;
vfd.print_line_diff(2, line2)?;
// чтобы обновлялось ровно раз в секунду (плюс/минус), можно “привязать” к UNIX time
// Привязываем паузу к следующей границе секунды. Тогда часы не "уплывают" из-за
// времени, потраченного на форматирование и serial-запись.
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
+38 -11
View File
@@ -1,39 +1,52 @@
//! Общие утилиты командной строки для всех демонстрационных программ.
//! Общие утилиты командной строки для демонстрационных программ.
//!
//! Каждый пример компилируется как отдельный binary. Этот модуль убирает повторение:
//! первый аргумент всегда serial-порт, второй - ширина дисплея, остальные параметры
//! зависят от конкретного примера.
#![allow(
dead_code,
reason = "модуль компилируется отдельно для каждого примера, поэтому часть API используется в соседних примерах"
)]
use m::vfd::VfdConfig;
use escpos_vfd::{Preset, VfdConfig};
use std::error::Error;
use std::str::FromStr;
use std::vec::IntoIter;
const DEFAULT_PORT: &str = "/dev/cu.usbmodem101";
const DEFAULT_WIDTH: usize = 20;
/// Общие аргументы примера: serial-порт, ширина дисплея и оставшиеся параметры.
/// Общие аргументы примера: готовая конфигурация, ширина дисплея и оставшиеся параметры.
pub struct ExampleArgs {
/// Конфигурация VFD на основе пресета текущего дисплея.
pub config: VfdConfig,
pub width: usize,
/// Ширина дисплея в символах.
pub columns: usize,
remaining: IntoIter<String>,
}
impl ExampleArgs {
/// Читает общие аргументы из командной строки и применяет безопасные значения по умолчанию.
pub fn from_env() -> Self {
/// Читает общие аргументы из командной строки и применяет значения по умолчанию.
pub fn from_env() -> Result<Self, Box<dyn Error>> {
let mut values = std::env::args().skip(1);
// Примеры должны запускаться без длинной CLI-команды на авторском стенде, но
// первый аргумент позволяет сразу проверить другой USB/COM-порт.
let port_name = values.next().unwrap_or_else(|| DEFAULT_PORT.to_string());
let width = values
// Геометрия дисплея участвует в проверке координат и обрезке строк. Меняем
// только ширину, потому что демонстрации ниже рассчитаны на две строки.
let columns = values
.next()
.and_then(|value| value.parse().ok())
.unwrap_or(DEFAULT_WIDTH);
let config = VfdConfig::new(port_name).with_width(width);
let config = preset_config(port_name, columns)?;
Self {
width: config.width,
Ok(Self {
columns,
config,
remaining: values.collect::<Vec<_>>().into_iter(),
}
})
}
/// Читает следующий аргумент нужного типа или возвращает переданное значение по умолчанию.
@@ -57,3 +70,17 @@ impl ExampleArgs {
pub fn sleep_ms(delay_ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
/// Создаёт конфигурацию пресета текущего дисплея с переопределяемой шириной.
pub fn preset_config(
port_name: impl Into<String>,
columns: usize,
) -> Result<VfdConfig, Box<dyn Error>> {
let mut config = VfdConfig::preset(port_name, Preset::Epson20x2Cp866)?;
// Пресет сохраняет baud/code table/кодировку, а ширину даём менять из CLI, чтобы
// теми же примерами проверять 16x2, 20x2 и другие ESC/POS-совместимые VFD.
config.display.columns = columns;
config.validate()?;
Ok(config)
}
+49
View File
@@ -0,0 +1,49 @@
//! Синхронный старт с полностью ручными serial/display настройками.
use escpos_vfd::{DisplaySettings, SerialSettings, TextEncoding, Vfd, VfdConfig};
use serialport::{DataBits, FlowControl, Parity, StopBits};
use std::error::Error;
use std::time::Duration;
fn main() -> Result<(), Box<dyn Error>> {
// Ручной пример показывает, что библиотека не держит глобальных `FIXED_BAUD` или
// `TABLE_CYR`: каждый параметр задаёт приложение.
let port_name = std::env::args()
.nth(1)
.unwrap_or_else(|| "/dev/cu.usbmodem101".to_string());
// Все serial-параметры задаются явно, без скрытого FIXED_BAUD.
let mut serial = SerialSettings::new(port_name, 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);
// `encoding` кодирует Rust-строку в байты, а `code_table` отвечает только за
// аппаратную команду `ESC t n`. Эти настройки часто должны совпадать по смыслу,
// но это два разных слоя протокола.
let mut display = DisplaySettings::new(20, 2, TextEncoding::Cp866);
// Если дисплей уже настроен DIP-переключателями или использует другой протокол
// выбора таблицы, оставьте `None`, и библиотека не отправит `ESC t`.
display.code_table = Some(6);
display.reset_on_open = true;
display.brightness = Some(1..=4);
display.brightness_settle = Duration::from_millis(2);
// `VfdConfig::new` валидирует геометрию, baud rate и queue capacity до открытия
// serial-порта, поэтому ошибки настроек видны сразу.
let config = VfdConfig::new(serial, display)?;
let mut display = Vfd::open(config)?;
// Для обычного текста используйте типизированные методы; `write_raw` нужен
// только для команд, которых пока нет в публичном API.
display.clear()?;
display.print_line(1, "Ручной режим")?;
// Координаты в библиотеке 1-based: первая колонка - x=1, первая строка - y=1.
display.print_at(1, 2, "20x2, CP866")?;
Ok(())
}
+25 -17
View File
@@ -2,39 +2,47 @@
mod common;
use anyhow::Result;
use std::error::Error;
use std::time::Duration;
use m::worker::VfdWorker;
use escpos_vfd::VfdWorker;
fn main() -> Result<()> {
// args:
// 1) port (default: /dev/cu.usbmodem101)
// 2) width (default: 20)
// 3) cps (chars per second, default: 8)
// 4) end_pause_ms (default: 1500)
// 5) brightness 1..4 (default: 2)
let mut args = common::ExampleArgs::from_env();
fn main() -> Result<(), Box<dyn Error>> {
// Аргументы:
// 1) port - serial-порт дисплея;
// 2) width - ширина строки, по ней worker строит кадры marquee;
// 3) cps - скорость в символах в секунду;
// 4) end_pause_ms - пауза после полного прохода текста;
// 5) brightness - яркость из диапазона пресета 1..=4.
let mut args = common::ExampleArgs::from_env()?;
let cps: u32 = args.parse_or(8);
let end_pause_ms: u64 = args.parse_or(1500);
let brightness: u8 = args.parse_or(2);
// Marquee живёт внутри worker-потока: основной поток может спать, а worker будет
// просыпаться по собственному таймеру и отправлять следующий кадр.
let worker = VfdWorker::start(args.config)?;
let vfd = worker.handle();
vfd.clear();
vfd.set_brightness(brightness);
vfd.clear()?;
vfd.set_brightness(brightness)?;
let text = String::from(
"Однозначно, базовые сценарии поведения пользователей призывают нас к новым свершениям, которые, в свою очередь, должны быть объявлены нарушающими общечеловеческие нормы этики и морали. С другой стороны, понимание сути ресурсосберегающих технологий не даёт нам иного выбора, кроме определения экономической целесообразности принимаемых решений. Задача организации, в особенности же укрепление и развитие внутренней структуры обеспечивает актуальность соответствующих условий активизации. Предварительные выводы неутешительны: укрепление и развитие внутренней структуры однозначно фиксирует необходимость укрепления моральных ценностей. Высокий уровень вовлечения представителей целевой аудитории является четким доказательством простого факта: повышение уровня гражданского сознания представляет собой интересный эксперимент проверки поэтапного и последовательного развития общества.",
);
// “рыба” + бесконечный цикл
vfd.set_marquee_text(text);
vfd.start_marquee(2, cps, Duration::from_millis(end_pause_ms));
// можно подсветить вторую строку статикой
let _ = vfd.print_line_diff(1, "Бегущая строка");
// Длинный текст сначала сохраняется в состоянии worker-а. `start_marquee` выбирает
// строку, скорость и паузу; сама анимация дальше идёт без ручного цикла кадров.
vfd.set_marquee_text(text)?;
vfd.start_marquee(2, cps, Duration::from_millis(end_pause_ms))?;
// Первая строка остаётся статикой. Вторую строку теперь лучше не обновлять обычными
// командами, пока на ней активна marquee.
vfd.print_line_diff(1, "Бегущая строка")?;
loop {
// Держим процесс живым. Если `main` завершится, `worker` попадёт в Drop и
// остановит фоновую анимацию.
common::sleep_ms(3_600_000);
}
}
+25 -21
View File
@@ -2,47 +2,51 @@
mod common;
use anyhow::Result;
use std::error::Error;
use m::worker::VfdWorker;
use escpos_vfd::VfdWorker;
fn main() -> Result<()> {
// args:
// 1) port (default: /dev/cu.usbmodem101)
// 2) width (default: 20)
// 3) delay_ms (default: 80)
let mut args = common::ExampleArgs::from_env();
fn main() -> Result<(), Box<dyn Error>> {
// Аргументы:
// 1) port - serial-порт дисплея;
// 2) width - ширина строки, по ней ограничиваем движение курсора;
// 3) delay_ms - скорость движения символа.
let mut args = common::ExampleArgs::from_env()?;
let delay_ms: u64 = args.parse_or(80);
let width = args.width;
let columns = args.columns;
// Worker сериализует `print_at`: сначала стираем старую позицию, затем рисуем новую.
let worker = VfdWorker::start(args.config)?;
let vfd = worker.handle();
vfd.clear();
vfd.clear()?;
// Статика
let _ = vfd.print_line_diff(1, "print_at demo");
let _ = vfd.print_line_diff(2, "--------------------"); // будет обрезано по width
// Статичную шапку пишем через diff-метод. Вторая строка ниже станет рабочей областью.
vfd.print_line_diff(1, "print_at demo")?;
// Строка автоматически обрежется по настроенной ширине дисплея.
vfd.print_line_diff(2, "--------------------")?;
// “курсор бегает по 2-й строке
// "Курсор" бегает по 2-й строке. Координаты в API начинаются с 1, не с 0.
let y = 2u8;
let mut x: u8 = 1;
let mut dir: i8 = 1;
// чтобы не оставлять хвост помним прошлую позицию и стираем её пробелом
// Чтобы не оставлять хвост, помним прошлую позицию и стираем её пробелом.
let mut prev_x: u8 = x;
loop {
// стереть прошлую позицию
let _ = vfd.print_at(prev_x, y, " ");
// Стереть прошлую позицию.
vfd.print_at(prev_x, y, " ")?;
// нарисовать новую
let _ = vfd.print_at(x, y, ""); // можно заменить на "*" если надо
// Нарисовать новую. Если дисплей плохо показывает этот символ в выбранной
// таблице, замените его на ASCII "*".
vfd.print_at(x, y, "")?;
prev_x = x;
// шаг
// Шаг вправо/влево с отражением от границ настроенной ширины.
if dir > 0 {
if (x as usize) >= width {
if (x as usize) >= columns {
dir = -1;
} else {
x += 1;
+33
View File
@@ -0,0 +1,33 @@
//! Минимальный синхронный старт через пресет Epson20x2Cp866.
use escpos_vfd::{Preset, Vfd, VfdConfig};
use std::error::Error;
use std::thread;
use std::time::Duration;
fn main() -> Result<(), Box<dyn Error>> {
// Первый аргумент - имя serial-порта. Значение по умолчанию удобно для локального
// macOS-стенда; на Linux чаще будет `/dev/ttyUSB0`, на Windows - `COM3`.
let port_name = std::env::args()
.nth(1)
.unwrap_or_else(|| "/dev/cu.usbmodem101".to_string());
// Пресет содержит все параметры текущего дисплея: serial 9600 8N1,
// геометрию 20x2, CP866, `ESC @` и `ESC t 6`.
let config = VfdConfig::preset(port_name, Preset::Epson20x2Cp866)?;
let mut display = Vfd::open(config)?;
// Низкоуровневый `Vfd` пишет команды сразу в serial-порт в текущем потоке. Это
// самый простой путь для CLI-утилит и smoke-test на железе.
display.clear()?;
display.set_brightness(2)?;
// `print_line` дополняет строку пробелами до ширины дисплея, поэтому старый текст
// справа не остаётся на экране.
display.print_line(1, "Пресет CP866")?;
display.print_line(2, "escpos-vfd")?;
// Даём физическому дисплею время показать результат до завершения процесса.
thread::sleep(Duration::from_secs(2));
Ok(())
}
+34
View File
@@ -0,0 +1,34 @@
//! Асинхронный worker на Tokio с bounded queue и явным shutdown.
mod common;
use escpos_vfd::tokio::AsyncVfdWorker;
use std::error::Error;
use std::time::Duration;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
// В Cargo.toml у примера указан `required-features = ["tokio"]`. Поэтому sync-only
// пользователи не подтягивают async-зависимости, а сам файл остаётся обычным
// понятным Tokio-примером без cfg-обёрток.
let config = common::ExampleArgs::from_env()?.config;
// Async worker открывает serial-порт через `tokio-serial` и запускает одну task,
// которая последовательно пишет команды в устройство.
let worker = AsyncVfdWorker::start(config).await?;
let display = worker.handle();
// Каждый await завершается после записи команды worker-ом или возвращает ошибку I/O.
// Если очередь заполнена, `send` внутри handle даст backpressure через `.await`.
display.clear().await?;
display.set_brightness(2).await?;
display.print_line_diff(1, "Tokio worker").await?;
display.print_line_diff(2, "bounded queue").await?;
// Держим текст на физическом дисплее пару секунд, затем делаем graceful shutdown:
// ранее принятые команды завершатся, task остановится, serial transport вернётся.
tokio::time::sleep(Duration::from_secs(2)).await;
worker.shutdown().await?;
Ok(())
}
+33 -25
View File
@@ -2,12 +2,13 @@
mod common;
use anyhow::Result;
use std::error::Error;
use m::worker::VfdWorker;
use escpos_vfd::VfdWorker;
fn pad_left(s: &str, width: usize) -> String {
// простая подгонка ширины, чтобы при уменьшении числа не оставались хвосты
// Подгоняем ширину поля, чтобы при уменьшении числа не оставались "хвосты" от
// предыдущего значения. Например, после 1000 должно появиться " 999", а не "9990".
if s.chars().count() >= width {
s.chars().take(width).collect()
} else {
@@ -15,28 +16,34 @@ fn pad_left(s: &str, width: usize) -> String {
}
}
fn main() -> Result<()> {
// args:
// 1) port (default: /dev/cu.usbmodem101)
// 2) width (default: 20)
// 3) delay_ms (default: 200)
let mut args = common::ExampleArgs::from_env();
fn main() -> Result<(), Box<dyn Error>> {
// Аргументы:
// 1) port - serial-порт дисплея;
// 2) width - ширина строки;
// 3) delay_ms - пауза между изменениями данных.
let mut args = common::ExampleArgs::from_env()?;
let delay_ms: u64 = args.parse_or(200);
let width = args.width;
if width < 20 {
anyhow::bail!("update_at requires a display width of at least 20 columns");
let columns = args.columns;
// Координаты ниже подобраны под 20 символов. Для более узких дисплеев лучше
// сделать другой layout, поэтому пример явно сообщает об ограничении.
if columns < 20 {
return Err("update_at requires a display width of at least 20 columns".into());
}
// Worker сохраняет кэш строк. Благодаря этому `print_at` и `print_line_diff`
// согласованно обновляют только изменившиеся области.
let worker = VfdWorker::start(args.config)?;
let vfd = worker.handle();
vfd.clear();
vfd.clear()?;
// Рисуем шаблон один раз (как UI)
// Рисуем "шаблон" один раз. Дальше меняем только числовые поля внутри шаблона.
// 12345678901234567890
// Temp: __.__C RPM:____
// Load: ___% Uptime:____
let _ = vfd.print_line_diff(1, "Temp: 00.00C RPM:0000");
let _ = vfd.print_line_diff(2, "Load: 000% Up:0000s");
vfd.print_line_diff(1, "Temp: 00.00C RPM:0000")?;
vfd.print_line_diff(2, "Load: 000% Up:0000s")?;
// Координаты (1-based):
// "Temp: 00.00C ..." -> числа начинаются с x=7, длина 5 (00.00)
@@ -64,7 +71,7 @@ fn main() -> Result<()> {
let mut si = 0usize;
loop {
// чуть шевелим данные
// Чуть "шевелим" данные, чтобы на дисплее были видны частичные обновления.
t += 0.03;
if t > 29.99 {
t = 21.50;
@@ -74,21 +81,22 @@ fn main() -> Result<()> {
load = (load + 3) % 100;
uptime = uptime.wrapping_add(1);
// Важно: обновляем только куски строки в фиксированных местах
// Важно: обновляем только куски строки в фиксированных местах. Это меньше
// нагружает serial-линию и обычно выглядит спокойнее, чем полная перерисовка.
let temp_s = pad_left(&format!("{:.2}", t), temp_w);
let rpm_s = pad_left(&format!("{}", rpm), rpm_w);
let load_s = pad_left(&format!("{}", load), load_w);
let up_s = pad_left(&format!("{}", uptime % 10000), up_w);
let _ = vfd.print_at(temp_x, 1, temp_s);
let _ = vfd.print_at(rpm_x, 1, rpm_s);
vfd.print_at(temp_x, 1, temp_s)?;
vfd.print_at(rpm_x, 1, rpm_s)?;
let _ = vfd.print_at(load_x, 2, load_s);
let _ = vfd.print_at(up_x, 2, up_s);
vfd.print_at(load_x, 2, load_s)?;
vfd.print_at(up_x, 2, up_s)?;
// “живой индикатор справа (если ширина позволяет)
if width >= 20 {
let _ = vfd.print_at(20, 2, spinner[si]);
// ивой" индикатор справа показывает, что цикл продолжает выполняться.
if columns >= 20 {
vfd.print_at(20, 2, spinner[si])?;
si = (si + 1) % spinner.len();
}