init
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
.DS_Store
|
||||||
286
Cargo.lock
generated
Normal file
286
Cargo.lock
generated
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-kit-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"mach2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.180"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libudev"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libudev-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libudev-sys"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "m"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"encoding_rs",
|
||||||
|
"serialport",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mach2"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serialport"
|
||||||
|
version = "4.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21f60a586160667241d7702c420fc223939fb3c0bb8d3fac84f78768e8970dee"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"cfg-if",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"io-kit-sys",
|
||||||
|
"libudev",
|
||||||
|
"mach2",
|
||||||
|
"nix",
|
||||||
|
"quote",
|
||||||
|
"scopeguard",
|
||||||
|
"unescaper",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unescaper"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "m"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serialport = "4"
|
||||||
|
anyhow = "1"
|
||||||
|
encoding_rs = "0.8"
|
||||||
53
examples/brightness.rs
Normal file
53
examples/brightness.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use m::vfd::VfdConfig;
|
||||||
|
use m::worker::VfdWorker;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
// args:
|
||||||
|
// 1) port (default: /dev/cu.usbmodem101)
|
||||||
|
// 2) width (default: 20)
|
||||||
|
// 3) delay_ms (default: 800)
|
||||||
|
let port_name = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or("/dev/cu.usbmodem101".into());
|
||||||
|
|
||||||
|
let width: usize = std::env::args()
|
||||||
|
.nth(2)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("20")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(20);
|
||||||
|
|
||||||
|
let delay_ms: u64 = std::env::args()
|
||||||
|
.nth(3)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("800")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(800);
|
||||||
|
|
||||||
|
let cfg = VfdConfig::new(port_name).with_width(width);
|
||||||
|
let worker = VfdWorker::start(cfg)?;
|
||||||
|
let vfd = worker.handle();
|
||||||
|
|
||||||
|
vfd.clear();
|
||||||
|
|
||||||
|
// шапка один раз (не обязательно, но удобно)
|
||||||
|
let _ = vfd.print_line_diff(1, "Яркость");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
for level in 1u8..=4u8 {
|
||||||
|
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);
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_millis(delay_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
examples/clock.rs
Normal file
150
examples/clock.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// examples/clock_ru.rs
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use m::vfd::VfdConfig;
|
||||||
|
use m::worker::VfdWorker;
|
||||||
|
|
||||||
|
fn run_date(args: &[&str]) -> Option<String> {
|
||||||
|
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 local_datetime_ddmmyyyy_hhmmss() -> String {
|
||||||
|
// 28.01.2006 12:03:34
|
||||||
|
run_date(&["+%d.%m.%Y %H:%M:%S"]).unwrap_or_else(|| "??.??.???? ??:??:??".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_weekday_num_1_7() -> u8 {
|
||||||
|
// 1..7 (Mon..Sun)
|
||||||
|
run_date(&["+%u"])
|
||||||
|
.and_then(|s| s.parse::<u8>().ok())
|
||||||
|
.unwrap_or(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 как 2 символа после пробела
|
||||||
|
datetime
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|t| t.get(0..2))
|
||||||
|
.and_then(|hh| hh.parse::<u8>().ok())
|
||||||
|
.unwrap_or(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fit_to_width(s: &str, width: usize) -> String {
|
||||||
|
// простое “обрезать по символам”, чтобы кириллицу не порвать
|
||||||
|
let mut out = String::new();
|
||||||
|
for (i, ch) in s.chars().enumerate() {
|
||||||
|
if i >= width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.push(ch);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
// args:
|
||||||
|
// 1) port (default: /dev/cu.usbmodem101)
|
||||||
|
// 2) width (default: 20)
|
||||||
|
// 3) brightness 1..4 (default: 2)
|
||||||
|
let port_name = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or("/dev/cu.usbmodem101".into());
|
||||||
|
|
||||||
|
let width: usize = std::env::args()
|
||||||
|
.nth(2)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("20")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(20);
|
||||||
|
|
||||||
|
let brightness: u8 = std::env::args()
|
||||||
|
.nth(3)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("2")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(2);
|
||||||
|
|
||||||
|
let cfg = VfdConfig::new(port_name).with_width(width);
|
||||||
|
let worker = VfdWorker::start(cfg)?;
|
||||||
|
let vfd = worker.handle();
|
||||||
|
|
||||||
|
vfd.clear();
|
||||||
|
vfd.set_brightness(brightness);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let dt = local_datetime_ddmmyyyy_hhmmss();
|
||||||
|
let wd = local_weekday_num_1_7();
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 2 строка: "Понедельник сейчас день"
|
||||||
|
// но если не влазит — "Пн сейчас день"
|
||||||
|
let full = format!("{} сейчас {}", weekday_ru_full(wd), tod);
|
||||||
|
let mut line2 = full;
|
||||||
|
|
||||||
|
if line2.chars().count() > width {
|
||||||
|
line2 = format!("{} сейчас {}", weekday_ru_short(wd), tod);
|
||||||
|
}
|
||||||
|
|
||||||
|
line2 = fit_to_width(&line2, width);
|
||||||
|
|
||||||
|
vfd.print_line_diff(1, line1)?;
|
||||||
|
vfd.print_line_diff(2, line2)?;
|
||||||
|
|
||||||
|
// чтобы обновлялось ровно раз в секунду (плюс/минус), можно “привязать” к UNIX time
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
66
examples/marquee.rs
Normal file
66
examples/marquee.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use m::vfd::VfdConfig;
|
||||||
|
use m::worker::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 port_name = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or("/dev/cu.usbmodem101".into());
|
||||||
|
|
||||||
|
let width: usize = std::env::args()
|
||||||
|
.nth(2)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("20")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(20);
|
||||||
|
|
||||||
|
let cps: u32 = std::env::args()
|
||||||
|
.nth(3)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("8")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(8);
|
||||||
|
|
||||||
|
let end_pause_ms: u64 = std::env::args()
|
||||||
|
.nth(4)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("1500")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(1500);
|
||||||
|
|
||||||
|
let brightness: u8 = std::env::args()
|
||||||
|
.nth(5)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("2")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(2);
|
||||||
|
|
||||||
|
let cfg = VfdConfig::new(port_name).with_width(width);
|
||||||
|
let worker = VfdWorker::start(cfg)?;
|
||||||
|
let vfd = worker.handle();
|
||||||
|
|
||||||
|
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, "Бегущая строка");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
std::thread::sleep(Duration::from_secs(3600));
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod vfd;
|
||||||
|
pub mod worker;
|
||||||
45
src/main.rs
Normal file
45
src/main.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use m::vfd::VfdConfig;
|
||||||
|
use m::worker::VfdWorker;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let port_name = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or("/dev/cu.usbmodem101".into());
|
||||||
|
let width: usize = std::env::args()
|
||||||
|
.nth(2)
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("20")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(20);
|
||||||
|
|
||||||
|
let cfg = VfdConfig::new(port_name).with_width(width);
|
||||||
|
let worker = VfdWorker::start(cfg)?;
|
||||||
|
let vfd = worker.handle();
|
||||||
|
|
||||||
|
// === бизнес-логика (демо) ===
|
||||||
|
vfd.clear();
|
||||||
|
vfd.set_brightness(0);
|
||||||
|
|
||||||
|
vfd.set_marquee_text(" Вставай самурай, у нас город в огне! Пора на работу!");
|
||||||
|
vfd.start_marquee(1, 8, Duration::from_millis(1500));
|
||||||
|
|
||||||
|
{
|
||||||
|
std::thread::sleep(Duration::from_millis(800));
|
||||||
|
vfd.set_brightness(1);
|
||||||
|
std::thread::sleep(Duration::from_millis(800));
|
||||||
|
vfd.set_brightness(2);
|
||||||
|
std::thread::sleep(Duration::from_millis(800));
|
||||||
|
vfd.set_brightness(3);
|
||||||
|
std::thread::sleep(Duration::from_millis(800));
|
||||||
|
vfd.set_brightness(4);
|
||||||
|
}
|
||||||
|
// останавливаем
|
||||||
|
std::thread::sleep(Duration::from_secs(30));
|
||||||
|
vfd.stop_marquee();
|
||||||
|
|
||||||
|
// graceful shutdown (или просто выйти — Drop у VfdWorker сделает shutdown+join)
|
||||||
|
vfd.shutdown();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
144
src/vfd.rs
Normal file
144
src/vfd.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use encoding_rs::IBM866;
|
||||||
|
use serialport::SerialPort;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const TABLE_CYR: u8 = 6; // PD-2600: your Cyrillic table (ESC t 6)
|
||||||
|
const FIXED_BAUD: u32 = 9600;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VfdConfig {
|
||||||
|
pub port_name: String,
|
||||||
|
pub width: usize,
|
||||||
|
pub timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfdConfig {
|
||||||
|
pub fn new(port_name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
port_name: port_name.into(),
|
||||||
|
width: 20,
|
||||||
|
timeout: Duration::from_millis(100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_width(mut self, width: usize) -> Self {
|
||||||
|
self.width = width.max(1);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Vfd {
|
||||||
|
port: Box<dyn SerialPort>,
|
||||||
|
pub width: usize,
|
||||||
|
blank_line: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vfd {
|
||||||
|
pub fn open(cfg: VfdConfig) -> Result<Self> {
|
||||||
|
let mut port = serialport::new(cfg.port_name, FIXED_BAUD)
|
||||||
|
.timeout(cfg.timeout)
|
||||||
|
.open()?;
|
||||||
|
|
||||||
|
// ESC @
|
||||||
|
port.write_all(&[0x1B, 0x40])?;
|
||||||
|
// ESC t 6
|
||||||
|
port.write_all(&[0x1B, 0x74, TABLE_CYR])?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
port,
|
||||||
|
width: cfg.width,
|
||||||
|
blank_line: " ".repeat(cfg.width),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) -> std::io::Result<()> {
|
||||||
|
self.port.write_all(&[0x0C])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_line(&mut self, line: u8, text: &str) -> std::io::Result<()> {
|
||||||
|
if !(1..=2).contains(&line) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// overwrite the whole line to avoid leftovers
|
||||||
|
self.goto_xy(1, line)?;
|
||||||
|
|
||||||
|
// FIX: take owned copy so self isn't immutably borrowed during &mut self call
|
||||||
|
let blank = self.blank_line.clone();
|
||||||
|
self.write_cp866(&blank)?;
|
||||||
|
|
||||||
|
self.goto_xy(1, line)?;
|
||||||
|
let fitted = fit_to_width(&sanitize_for_cp866(text), self.width);
|
||||||
|
self.write_cp866(&fitted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print already-prepared fixed-width frame at (1, line) without clearing.
|
||||||
|
/// Use this for marquee to avoid double-writes.
|
||||||
|
pub fn print_frame(&mut self, line: u8, frame: &str) -> std::io::Result<()> {
|
||||||
|
if !(1..=2).contains(&line) {
|
||||||
|
// silently ignore invalid lines (or return an error if you want)
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.goto_xy(1, line)?;
|
||||||
|
|
||||||
|
// frame should already be width-sized; if not, fit it
|
||||||
|
let fitted = fit_to_width(frame, self.width);
|
||||||
|
self.write_cp866(&fitted)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn print_at(&mut self, x: u8, y: u8, text: &str) -> std::io::Result<()> {
|
||||||
|
self.goto_xy(x, y)?;
|
||||||
|
self.write_cp866(&sanitize_for_cp866(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_xy(&mut self, x: u8, y: u8) -> std::io::Result<()> {
|
||||||
|
// US $ x y
|
||||||
|
self.port.write_all(&[0x1F, 0x24, x, y])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_cp866(&mut self, s: &str) -> std::io::Result<()> {
|
||||||
|
let (bytes, _, _) = IBM866.encode(s);
|
||||||
|
self.port.write_all(&bytes)
|
||||||
|
}
|
||||||
|
/// Epson customer display: US X n (brightness), n=1..4
|
||||||
|
pub fn set_brightness(&mut self, n: u8) -> Result<()> {
|
||||||
|
let n = n.clamp(1, 4);
|
||||||
|
let cmd = [0x1F, 0x58, n]; // US 'X' n
|
||||||
|
self.port.write_all(&cmd)?;
|
||||||
|
self.port.flush()?;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitize_for_cp866(s: &str) -> String {
|
||||||
|
s.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'…' => '.', //
|
||||||
|
'—' | '–' => '-', //
|
||||||
|
'№' => '#', //
|
||||||
|
'\t' => ' ',
|
||||||
|
'“' | '”' => '"',
|
||||||
|
'‘' | '’' => '\'',
|
||||||
|
_ => c,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fit_to_width(s: &str, width: usize) -> String {
|
||||||
|
let mut out: String = s.chars().take(width).collect();
|
||||||
|
let len = out.chars().count();
|
||||||
|
if len < width {
|
||||||
|
out.push_str(&" ".repeat(width - len));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
405
src/worker.rs
Normal file
405
src/worker.rs
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
use crate::vfd::{Vfd, VfdConfig, sanitize_for_cp866};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc::{self, Receiver, Sender},
|
||||||
|
};
|
||||||
|
use std::thread::{self, JoinHandle};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Cmd {
|
||||||
|
Clear,
|
||||||
|
PrintLine {
|
||||||
|
line: u8,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
PrintLineDiff {
|
||||||
|
line: u8,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
PrintAt {
|
||||||
|
x: u8,
|
||||||
|
y: u8,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
// marquee control
|
||||||
|
SetMarqueeText {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
StartMarquee {
|
||||||
|
line: u8,
|
||||||
|
cps: u32,
|
||||||
|
end_pause: Duration,
|
||||||
|
},
|
||||||
|
|
||||||
|
StopMarquee,
|
||||||
|
|
||||||
|
SetBrightness {
|
||||||
|
level: u8, // 1..4
|
||||||
|
},
|
||||||
|
|
||||||
|
// stop worker
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VfdHandle {
|
||||||
|
tx: Sender<Cmd>,
|
||||||
|
stop: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfdHandle {
|
||||||
|
pub fn clear(&self) {
|
||||||
|
let _ = self.tx.send(Cmd::Clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_brightness(&self, level: u8) {
|
||||||
|
let _ = self.tx.send(Cmd::SetBrightness { level });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn print_line(&self, line: u8, text: impl Into<String>) -> anyhow::Result<()> {
|
||||||
|
self.tx.send(Cmd::PrintLine {
|
||||||
|
line,
|
||||||
|
text: text.into(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_line_diff(&self, line: u8, text: impl Into<String>) -> anyhow::Result<()> {
|
||||||
|
self.tx.send(Cmd::PrintLineDiff {
|
||||||
|
line,
|
||||||
|
text: text.into(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_at(&self, x: u8, y: u8, text: impl Into<String>) -> anyhow::Result<()> {
|
||||||
|
self.tx.send(Cmd::PrintAt {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
text: text.into(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_marquee_text(&self, text: impl Into<String>) {
|
||||||
|
let _ = self.tx.send(Cmd::SetMarqueeText { text: text.into() });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_marquee(&self, line: u8, cps: u32, end_pause: Duration) {
|
||||||
|
let _ = self.tx.send(Cmd::StartMarquee {
|
||||||
|
line,
|
||||||
|
cps,
|
||||||
|
end_pause,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_marquee(&self) {
|
||||||
|
let _ = self.tx.send(Cmd::StopMarquee);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
self.stop.store(true, Ordering::Relaxed);
|
||||||
|
let _ = self.tx.send(Cmd::Shutdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VfdWorker {
|
||||||
|
handle: VfdHandle,
|
||||||
|
join: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfdWorker {
|
||||||
|
pub fn start(cfg: VfdConfig) -> Result<Self> {
|
||||||
|
let (tx, rx) = mpsc::channel::<Cmd>();
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
let handle = VfdHandle {
|
||||||
|
tx: tx.clone(),
|
||||||
|
stop: stop.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let join = thread::spawn(move || {
|
||||||
|
if let Err(e) = writer_loop(cfg, rx, stop) {
|
||||||
|
eprintln!("[vfd] writer loop error: {e:#}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
handle,
|
||||||
|
join: Some(join),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(&self) -> VfdHandle {
|
||||||
|
self.handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VfdWorker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// try to stop gracefully
|
||||||
|
self.handle.shutdown();
|
||||||
|
if let Some(j) = self.join.take() {
|
||||||
|
let _ = j.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct MarqueeState {
|
||||||
|
active: bool,
|
||||||
|
line: u8,
|
||||||
|
cps: u32,
|
||||||
|
end_pause: Duration,
|
||||||
|
text: String,
|
||||||
|
|
||||||
|
// runtime
|
||||||
|
stream: Vec<char>,
|
||||||
|
offset: usize,
|
||||||
|
last_step: Instant,
|
||||||
|
paused_until: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarqueeState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
line: 1,
|
||||||
|
cps: 5,
|
||||||
|
end_pause: Duration::from_millis(1500),
|
||||||
|
text: String::new(),
|
||||||
|
stream: Vec::new(),
|
||||||
|
offset: 0,
|
||||||
|
last_step: Instant::now(),
|
||||||
|
paused_until: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_stream(&mut self, width: usize) {
|
||||||
|
let text = sanitize_for_cp866(&self.text);
|
||||||
|
self.stream.clear();
|
||||||
|
self.stream.extend(std::iter::repeat(' ').take(width));
|
||||||
|
self.stream.extend(text.chars());
|
||||||
|
self.stream.extend(std::iter::repeat(' ').take(width));
|
||||||
|
self.offset = 0;
|
||||||
|
self.paused_until = None;
|
||||||
|
self.last_step = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_interval(&self) -> Duration {
|
||||||
|
let cps = self.cps.max(1);
|
||||||
|
Duration::from_millis((1000 / cps) as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writer_loop(cfg: VfdConfig, rx: Receiver<Cmd>, stop: Arc<AtomicBool>) -> Result<()> {
|
||||||
|
let mut vfd = Vfd::open(cfg)?;
|
||||||
|
// optional initial clear:
|
||||||
|
let _ = vfd.clear();
|
||||||
|
|
||||||
|
let mut marquee = MarqueeState::new();
|
||||||
|
let mut last_lines = [String::new(), String::new()]; // line 1..2, fixed-width
|
||||||
|
let width = vfd.width;
|
||||||
|
|
||||||
|
let normalize_line =
|
||||||
|
|text: &str| -> String { crate::vfd::fit_to_width(&sanitize_for_cp866(text), width) };
|
||||||
|
|
||||||
|
let tick = Duration::from_millis(20); // internal scheduler tick
|
||||||
|
|
||||||
|
while !stop.load(Ordering::Relaxed) {
|
||||||
|
// 1) Drain commands (non-blocking)
|
||||||
|
loop {
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(cmd) => match cmd {
|
||||||
|
Cmd::Clear => {
|
||||||
|
let _ = vfd.clear();
|
||||||
|
last_lines = [String::new(), String::new()];
|
||||||
|
}
|
||||||
|
Cmd::SetBrightness { level } => {
|
||||||
|
let _ = vfd.set_brightness(level);
|
||||||
|
}
|
||||||
|
Cmd::PrintLine { line, text } => {
|
||||||
|
let _ = vfd.print_line(line, &text);
|
||||||
|
|
||||||
|
if (1..=2).contains(&line) {
|
||||||
|
let idx = (line - 1) as usize;
|
||||||
|
last_lines[idx] = normalize_line(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::PrintLineDiff { line, text } => {
|
||||||
|
// конфликт с marquee
|
||||||
|
if !(1..=2).contains(&line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if marquee.active && marquee.line == line {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = normalize_line(&text);
|
||||||
|
let idx = (line - 1) as usize;
|
||||||
|
|
||||||
|
// если строка не поменялась — ничего не делаем
|
||||||
|
if last_lines[idx] == next {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// первый кадр (или после clear) — лучше один раз вывести целиком
|
||||||
|
if last_lines[idx].is_empty() {
|
||||||
|
let _ = vfd.print_line(line, &next);
|
||||||
|
last_lines[idx] = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
|
||||||
|
for (i, (a, b)) in last_lines[idx].chars().zip(next.chars()).enumerate() {
|
||||||
|
if i >= width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if a != b {
|
||||||
|
let x = (i as u8) + 1;
|
||||||
|
let s = b.encode_utf8(&mut buf);
|
||||||
|
let _ = vfd.print_at(x, line, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_lines[idx] = next;
|
||||||
|
}
|
||||||
|
Cmd::PrintAt { x, y, text } => {
|
||||||
|
// базовая валидация координат
|
||||||
|
if !(1..=width as u8).contains(&x) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(1..=2).contains(&y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// если marquee активен и пишет в эту строку — игнорируем, иначе будет “драка”
|
||||||
|
if marquee.active && marquee.line == y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = sanitize_for_cp866(&text);
|
||||||
|
let _ = vfd.print_at(x, y, &s);
|
||||||
|
|
||||||
|
// Обновляем кеш после изменения
|
||||||
|
if (1..=2).contains(&y) {
|
||||||
|
let idx = (y - 1) as usize;
|
||||||
|
|
||||||
|
// гарантируем fixed-width в кэше
|
||||||
|
if last_lines[idx].is_empty() {
|
||||||
|
last_lines[idx] = " ".repeat(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// заменяем символ в позиции x (1-based) на первый символ s (после sanitize)
|
||||||
|
// (если строка пустая — просто игнор)
|
||||||
|
if let Some(ch) = s.chars().next() {
|
||||||
|
let pos = (x - 1) as usize;
|
||||||
|
let mut new_line = String::with_capacity(width);
|
||||||
|
|
||||||
|
for (i, cur) in last_lines[idx].chars().enumerate() {
|
||||||
|
if i == pos {
|
||||||
|
new_line.push(ch);
|
||||||
|
} else {
|
||||||
|
new_line.push(cur);
|
||||||
|
}
|
||||||
|
if new_line.chars().count() >= width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// если вдруг кэш был короче width — добиваем пробелами
|
||||||
|
last_lines[idx] = crate::vfd::fit_to_width(&new_line, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::SetMarqueeText { text } => {
|
||||||
|
marquee.text = text;
|
||||||
|
if marquee.active {
|
||||||
|
marquee.rebuild_stream(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cmd::StartMarquee {
|
||||||
|
line,
|
||||||
|
cps,
|
||||||
|
end_pause,
|
||||||
|
} => {
|
||||||
|
let line = if (1..=2).contains(&line) { line } else { 1 };
|
||||||
|
|
||||||
|
last_lines[(line - 1) as usize].clear();
|
||||||
|
marquee.active = true;
|
||||||
|
marquee.line = line;
|
||||||
|
marquee.cps = cps.max(1);
|
||||||
|
marquee.end_pause = end_pause;
|
||||||
|
marquee.rebuild_stream(width);
|
||||||
|
}
|
||||||
|
Cmd::StopMarquee => {
|
||||||
|
if (1..=2).contains(&marquee.line) {
|
||||||
|
last_lines[(marquee.line - 1) as usize].clear();
|
||||||
|
}
|
||||||
|
marquee.active = false;
|
||||||
|
marquee.paused_until = None;
|
||||||
|
}
|
||||||
|
Cmd::Shutdown => {
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Empty) => break,
|
||||||
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
|
// All senders dropped => exit thread cleanly
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Render marquee if active
|
||||||
|
if marquee.active {
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if let Some(until) = marquee.paused_until {
|
||||||
|
if now >= until {
|
||||||
|
marquee.paused_until = None;
|
||||||
|
marquee.last_step = now;
|
||||||
|
}
|
||||||
|
} else if now.duration_since(marquee.last_step) >= marquee.step_interval() {
|
||||||
|
marquee.last_step = now;
|
||||||
|
|
||||||
|
if marquee.stream.len() >= width {
|
||||||
|
let max_off = marquee.stream.len() - width;
|
||||||
|
|
||||||
|
let start = marquee.offset.min(max_off);
|
||||||
|
let end = (start + width).min(marquee.stream.len());
|
||||||
|
|
||||||
|
let frame: String = marquee.stream[start..end].iter().collect();
|
||||||
|
let _ = vfd.print_frame(marquee.line, &frame);
|
||||||
|
|
||||||
|
if (1..=2).contains(&marquee.line) {
|
||||||
|
last_lines[(marquee.line - 1) as usize] =
|
||||||
|
crate::vfd::fit_to_width(&frame, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if marquee.offset >= max_off {
|
||||||
|
marquee.offset = 0;
|
||||||
|
marquee.paused_until = Some(now + marquee.end_pause);
|
||||||
|
} else {
|
||||||
|
marquee.offset += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
29
taskfile.yml
Normal file
29
taskfile.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
vars:
|
||||||
|
VFD_PORT: /dev/cu.usbmodem101
|
||||||
|
VFD_WIDTH: "20"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
vfd:clock:
|
||||||
|
desc: Run VFD clock example
|
||||||
|
cmds:
|
||||||
|
- cargo run --example clock -- {{.VFD_PORT}} {{.VFD_WIDTH}} {{.VFD_CPS | default "8"}}
|
||||||
|
vars:
|
||||||
|
VFD_CPS: "8"
|
||||||
|
|
||||||
|
vfd:marquee:
|
||||||
|
desc: Run VFD marquee example
|
||||||
|
cmds:
|
||||||
|
- cargo run --example marquee -- {{.VFD_PORT}} {{.VFD_WIDTH}} {{.VFD_CPS | default "8"}} {{.VFD_END_PAUSE_MS | default "1500"}} {{.VFD_BRIGHTNESS | default "2"}}
|
||||||
|
vars:
|
||||||
|
VFD_CPS: "8"
|
||||||
|
VFD_END_PAUSE_MS: "1500"
|
||||||
|
VFD_BRIGHTNESS: "2"
|
||||||
|
|
||||||
|
vfd:brightness:
|
||||||
|
desc: Run VFD brightness example
|
||||||
|
cmds:
|
||||||
|
- cargo run --example brightness -- {{.VFD_PORT}} {{.VFD_WIDTH}} {{.VFD_DELAY_MS | default "800"}}
|
||||||
|
vars:
|
||||||
|
VFD_DELAY_MS: "800"
|
||||||
Reference in New Issue
Block a user