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:
+25
-16
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user