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.
160 lines
5.9 KiB
Rust
160 lines
5.9 KiB
Rust
//! Показывает локальные дату, время, день недели и часть суток с обновлением раз в секунду.
|
|
|
|
mod common;
|
|
|
|
use std::error::Error;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
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()
|
|
.ok()?;
|
|
if !out.status.success() {
|
|
return None;
|
|
}
|
|
Some(String::from_utf8_lossy(&out.stdout).trim().to_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)
|
|
.contains(&weekday)
|
|
.then(|| (datetime.to_string(), weekday))
|
|
}
|
|
|
|
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)
|
|
.unwrap_or_else(|| ("??.??.???? ??:??:??".to_string(), 1))
|
|
}
|
|
|
|
fn weekday_ru_full(n: u8) -> &'static str {
|
|
match n {
|
|
1 => "Понедельник",
|
|
2 => "Вторник",
|
|
3 => "Среда",
|
|
4 => "Четверг",
|
|
5 => "Пятница",
|
|
6 => "Суббота",
|
|
7 => "Воскресенье",
|
|
_ => "Понедельник",
|
|
}
|
|
}
|
|
|
|
fn weekday_ru_short(n: u8) -> &'static str {
|
|
match n {
|
|
1 => "Пн",
|
|
2 => "Вт",
|
|
3 => "Ср",
|
|
4 => "Чт",
|
|
5 => "Пт",
|
|
6 => "Сб",
|
|
7 => "Вс",
|
|
_ => "Пн",
|
|
}
|
|
}
|
|
|
|
fn time_of_day_ru(hour: u8) -> &'static str {
|
|
match hour {
|
|
5..=10 => "утро",
|
|
11..=16 => "день",
|
|
17..=22 => "вечер",
|
|
_ => "ночь",
|
|
}
|
|
}
|
|
|
|
fn extract_hour(datetime: &str) -> u8 {
|
|
// Ожидаем "DD.MM.YYYY HH:MM:SS" и берём HH как два символа после пробела.
|
|
// При неожиданном формате возвращаем 12, чтобы пример продолжал работать.
|
|
datetime
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.and_then(|t| t.get(0..2))
|
|
.and_then(|hh| hh.parse::<u8>().ok())
|
|
.unwrap_or(12)
|
|
}
|
|
|
|
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 columns = args.columns;
|
|
|
|
// Worker остаётся жить до конца процесса. Handle можно клонировать, но сам worker
|
|
// владеет serial-портом и фоновым потоком записи.
|
|
let worker = VfdWorker::start(args.config)?;
|
|
let vfd = worker.handle();
|
|
|
|
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". `fit_to_width` обрезает по символам и
|
|
// дополняет пробелами, чтобы старый текст справа не оставался на дисплее.
|
|
let line1 = fit_to_width(&dt, columns);
|
|
|
|
// 2 строка: сначала пробуем полное название дня недели, если не влазит -
|
|
// короткое. Это показывает, как приложение может адаптироваться к ширине VFD.
|
|
let full = format!("{} сейчас {}", weekday_ru_full(wd), tod);
|
|
let mut line2 = full;
|
|
|
|
if line2.chars().count() > columns {
|
|
line2 = format!("{} сейчас {}", weekday_ru_short(wd), tod);
|
|
}
|
|
|
|
line2 = fit_to_width(&line2, columns);
|
|
|
|
vfd.print_line_diff(1, line1)?;
|
|
vfd.print_line_diff(2, line2)?;
|
|
|
|
// Привязываем паузу к следующей границе секунды. Тогда часы не "уплывают" из-за
|
|
// времени, потраченного на форматирование и serial-запись.
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis();
|
|
let ms_to_next = 1000 - (now % 1000) as u64;
|
|
std::thread::sleep(Duration::from_millis(ms_to_next));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_date_and_weekday_from_one_process_result() {
|
|
assert_eq!(
|
|
parse_datetime_and_weekday("15.08.2026 23:59:58|6"),
|
|
Some(("15.08.2026 23:59:58".to_string(), 6))
|
|
);
|
|
assert_eq!(parse_datetime_and_weekday("invalid"), None);
|
|
assert_eq!(parse_datetime_and_weekday("15.08.2026 23:59:58|8"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_hours_and_extracts_them_safely() {
|
|
assert_eq!(extract_hour("15.08.2026 05:00:00"), 5);
|
|
assert_eq!(extract_hour("invalid"), 12);
|
|
assert_eq!(time_of_day_ru(5), "утро");
|
|
assert_eq!(time_of_day_ru(23), "ночь");
|
|
}
|
|
}
|