escpos-vfd 0.3.0

ESC/POS-compatible VFD customer display driver with sync and optional Tokio APIs
Documentation
//! Показывает локальные дату, время, день недели и часть суток с обновлением раз в секунду.

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(4);
    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), "ночь");
    }
}