use crate::codec::{EpsonCodec, changed_runs, fit_to_width, replace_cached_range, sanitize_text};
use crate::config::{DisplaySettings, VfdConfig};
use crate::error::{ConfigError, Result, VfdError};
use ::tokio::io::{AsyncWrite, AsyncWriteExt};
use ::tokio::sync::{mpsc, oneshot};
use ::tokio::task::JoinHandle;
use ::tokio::time::{Duration, Instant, sleep, sleep_until};
use serialport::SerialPortBuilder;
use tokio_serial::{SerialPortBuilderExt, SerialStream};
type Ack = oneshot::Sender<Result<()>>;
enum Cmd {
Clear {
ack: Ack,
},
PrintLine {
line: u8,
text: String,
ack: Ack,
},
PrintLineDiff {
line: u8,
text: String,
ack: Ack,
},
PrintAt {
x: u8,
y: u8,
text: String,
ack: Ack,
},
WriteRaw {
bytes: Vec<u8>,
ack: Ack,
},
SetMarqueeText {
text: String,
ack: Ack,
},
StartMarquee {
line: u8,
cps: u32,
end_pause: Duration,
ack: Ack,
},
StopMarquee {
ack: Ack,
},
SetBrightness {
level: u8,
ack: Ack,
},
Shutdown {
ack: Ack,
},
}
pub struct AsyncVfd<T: AsyncWrite + Unpin = SerialStream> {
transport: T,
codec: EpsonCodec,
}
impl AsyncVfd<SerialStream> {
pub async fn open(cfg: VfdConfig) -> Result<Self> {
cfg.validate()?;
let serial = cfg.serial;
let display = cfg.display;
let mut builder: SerialPortBuilder = tokio_serial::new(&serial.port_name, serial.baud_rate)
.data_bits(serial.data_bits)
.parity(serial.parity)
.stop_bits(serial.stop_bits)
.flow_control(serial.flow_control)
.timeout(serial.timeout);
#[cfg(unix)]
{
builder = builder.exclusive(serial.exclusive);
}
let port = builder.open_native_async()?;
Self::from_transport(port, display).await
}
}
impl<T: AsyncWrite + Unpin> AsyncVfd<T> {
pub async fn from_transport(mut transport: T, display: DisplaySettings) -> Result<Self> {
display.validate()?;
let codec = EpsonCodec::new(display);
let init = codec.init();
if !init.is_empty() {
transport.write_all(&init).await?;
}
Ok(Self { transport, codec })
}
pub fn display(&self) -> &DisplaySettings {
self.codec.display()
}
pub fn columns(&self) -> usize {
self.display().columns
}
pub fn rows(&self) -> usize {
self.display().rows
}
pub async fn clear(&mut self) -> Result<()> {
self.transport.write_all(&self.codec.clear()).await?;
Ok(())
}
pub async fn print_line(&mut self, line: u8, text: &str) -> Result<()> {
self.codec.validate_line(line)?;
self.goto_xy(1, line).await?;
let fitted = self.codec.fit_line(text);
self.write_text(&fitted).await
}
pub async fn print_frame(&mut self, line: u8, frame: &str) -> Result<()> {
self.codec.validate_line(line)?;
self.goto_xy(1, line).await?;
let fitted = fit_to_width(frame, self.columns());
self.write_text(&fitted).await
}
pub async fn print_at(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
let text = sanitize_text(text);
self.print_at_prepared(x, y, &text).await
}
async fn print_at_prepared(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
self.codec.validate_xy(x, y)?;
let remaining = self.columns() - usize::from(x) + 1;
let text: String = text.chars().take(remaining).collect();
self.goto_xy(x, y).await?;
self.write_text(&text).await
}
pub async fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
self.transport.write_all(bytes).await?;
Ok(())
}
pub async fn set_brightness(&mut self, level: u8) -> Result<()> {
let cmd = self.codec.brightness(level)?;
self.transport.write_all(&cmd).await?;
self.transport.flush().await?;
let settle = self.display().brightness_settle;
if !settle.is_zero() {
sleep(settle).await;
}
Ok(())
}
pub async fn flush(&mut self) -> Result<()> {
self.transport.flush().await?;
Ok(())
}
pub fn into_inner(self) -> T {
self.transport
}
async fn goto_xy(&mut self, x: u8, y: u8) -> Result<()> {
let cmd = self.codec.goto_xy(x, y)?;
self.transport.write_all(&cmd).await?;
Ok(())
}
async fn write_text(&mut self, s: &str) -> Result<()> {
let bytes = self.codec.encode_text(s);
self.transport.write_all(&bytes).await?;
Ok(())
}
}
#[derive(Clone)]
pub struct AsyncVfdHandle {
tx: mpsc::Sender<Cmd>,
}
impl AsyncVfdHandle {
pub async fn clear(&self) -> Result<()> {
self.call(|ack| Cmd::Clear { ack }).await
}
pub async fn set_brightness(&self, level: u8) -> Result<()> {
self.call(|ack| Cmd::SetBrightness { level, ack }).await
}
pub async fn print_line(&self, line: u8, text: impl Into<String>) -> Result<()> {
let text = text.into();
self.call(|ack| Cmd::PrintLine { line, text, ack }).await
}
pub async fn print_line_diff(&self, line: u8, text: impl Into<String>) -> Result<()> {
let text = text.into();
self.call(|ack| Cmd::PrintLineDiff { line, text, ack })
.await
}
pub async fn print_at(&self, x: u8, y: u8, text: impl Into<String>) -> Result<()> {
let text = text.into();
self.call(|ack| Cmd::PrintAt { x, y, text, ack }).await
}
pub async fn write_raw(&self, bytes: impl Into<Vec<u8>>) -> Result<()> {
let bytes = bytes.into();
self.call(|ack| Cmd::WriteRaw { bytes, ack }).await
}
pub async fn set_marquee_text(&self, text: impl Into<String>) -> Result<()> {
let text = text.into();
self.call(|ack| Cmd::SetMarqueeText { text, ack }).await
}
pub async fn start_marquee(&self, line: u8, cps: u32, end_pause: Duration) -> Result<()> {
self.call(|ack| Cmd::StartMarquee {
line,
cps,
end_pause,
ack,
})
.await
}
pub async fn stop_marquee(&self) -> Result<()> {
self.call(|ack| Cmd::StopMarquee { ack }).await
}
pub async fn shutdown(&self) -> Result<()> {
self.call(|ack| Cmd::Shutdown { ack }).await
}
async fn call(&self, build: impl FnOnce(Ack) -> Cmd) -> Result<()> {
let (ack_tx, ack_rx) = oneshot::channel();
self.tx
.send(build(ack_tx))
.await
.map_err(|_| VfdError::QueueClosed)?;
ack_rx.await.map_err(|_| VfdError::WorkerStopped)?
}
}
pub struct AsyncVfdWorker<T: AsyncWrite + Unpin + Send + 'static = SerialStream> {
handle: AsyncVfdHandle,
join: Option<JoinHandle<Result<AsyncVfd<T>>>>,
}
impl AsyncVfdWorker<SerialStream> {
pub async fn start(cfg: VfdConfig) -> Result<Self> {
cfg.validate()?;
let capacity = cfg.queue_capacity;
let vfd = AsyncVfd::open(cfg).await?;
Self::from_vfd(vfd, capacity)
}
}
impl<T: AsyncWrite + Unpin + Send + 'static> AsyncVfdWorker<T> {
pub fn from_vfd(vfd: AsyncVfd<T>, queue_capacity: usize) -> Result<Self> {
if queue_capacity == 0 {
return Err(ConfigError::ZeroQueueCapacity.into());
}
let (tx, rx) = mpsc::channel(queue_capacity);
let handle = AsyncVfdHandle { tx };
let join = ::tokio::spawn(async move { writer_loop(vfd, rx).await });
Ok(Self {
handle,
join: Some(join),
})
}
pub async fn from_transport(
transport: T,
display: DisplaySettings,
queue_capacity: usize,
) -> Result<Self> {
if queue_capacity == 0 {
return Err(ConfigError::ZeroQueueCapacity.into());
}
let vfd = AsyncVfd::from_transport(transport, display).await?;
Self::from_vfd(vfd, queue_capacity)
}
pub fn handle(&self) -> AsyncVfdHandle {
self.handle.clone()
}
pub async fn shutdown(mut self) -> Result<AsyncVfd<T>> {
let shutdown_result = self.handle.shutdown().await;
let worker_result = self
.join
.take()
.expect("join handle exists")
.await
.map_err(|_| VfdError::WorkerCancelled)?;
let vfd = worker_result?;
shutdown_result?;
Ok(vfd)
}
}
impl<T: AsyncWrite + Unpin + Send + 'static> Drop for AsyncVfdWorker<T> {
fn drop(&mut self) {
if let Some(join) = self.join.take() {
join.abort();
}
}
}
#[derive(Debug, Clone)]
struct MarqueeState {
active: bool,
line: u8,
cps: u32,
end_pause: Duration,
text: String,
stream: Vec<char>,
offset: usize,
paused_until: Option<Instant>,
next_step: 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,
paused_until: None,
next_step: None,
}
}
fn rebuild_stream(&mut self, width: usize) {
let text = sanitize_text(&self.text);
self.stream.clear();
self.stream.reserve(width * 2 + text.chars().count());
self.stream.extend(std::iter::repeat_n(' ', width));
self.stream.extend(text.chars());
self.stream.extend(std::iter::repeat_n(' ', width));
self.offset = 0;
self.paused_until = None;
self.next_step = Some(Instant::now() + self.step_interval());
}
fn step_interval(&self) -> Duration {
let cps = u64::from(self.cps.max(1));
Duration::from_nanos((1_000_000_000 / cps).max(1))
}
fn next_deadline(&self) -> Option<Instant> {
if !self.active {
return None;
}
self.paused_until.or(self.next_step)
}
}
async fn writer_loop<T: AsyncWrite + Unpin>(
mut vfd: AsyncVfd<T>,
mut rx: mpsc::Receiver<Cmd>,
) -> Result<AsyncVfd<T>> {
vfd.clear().await?;
let rows = vfd.rows();
let mut marquee = MarqueeState::new();
let mut last_lines = vec![String::new(); rows];
loop {
let event = match marquee.next_deadline() {
Some(deadline) => {
::tokio::select! {
cmd = rx.recv() => match cmd {
Some(cmd) => WorkerEvent::Command(cmd),
None => WorkerEvent::Closed,
},
_ = sleep_until(deadline) => WorkerEvent::Timer,
}
}
None => match rx.recv().await {
Some(cmd) => WorkerEvent::Command(cmd),
None => WorkerEvent::Closed,
},
};
match event {
WorkerEvent::Command(cmd) => {
if handle_command(cmd, &mut vfd, &mut marquee, &mut last_lines).await? {
break;
}
}
WorkerEvent::Timer => {
render_marquee(&mut vfd, &mut marquee, &mut last_lines).await?;
}
WorkerEvent::Closed => break,
}
}
Ok(vfd)
}
enum WorkerEvent {
Command(Cmd),
Timer,
Closed,
}
async fn handle_command<T: AsyncWrite + Unpin>(
cmd: Cmd,
vfd: &mut AsyncVfd<T>,
marquee: &mut MarqueeState,
last_lines: &mut [String],
) -> Result<bool> {
let width = vfd.columns();
let rows = vfd.rows();
match cmd {
Cmd::Clear { ack } => {
let result = vfd.clear().await;
if result.is_ok() {
last_lines.fill(String::new());
}
send_ack(ack, result);
}
Cmd::SetBrightness { level, ack } => send_ack(ack, vfd.set_brightness(level).await),
Cmd::PrintLine { line, text, ack } => {
let result = vfd.print_line(line, &text).await;
if result.is_ok() {
last_lines[(line - 1) as usize] = fit_to_width(&sanitize_text(&text), width);
}
send_ack(ack, result);
}
Cmd::PrintLineDiff { line, text, ack } => {
let result = print_line_diff(vfd, marquee, last_lines, line, &text).await;
send_ack(ack, result);
}
Cmd::PrintAt { x, y, text, ack } => {
let result = print_at_cached(vfd, marquee, last_lines, x, y, &text).await;
send_ack(ack, result);
}
Cmd::WriteRaw { bytes, ack } => send_ack(ack, vfd.write_raw(&bytes).await),
Cmd::SetMarqueeText { text, ack } => {
marquee.text = text;
if marquee.active {
marquee.rebuild_stream(width);
}
send_ack(ack, Ok(()));
}
Cmd::StartMarquee {
line,
cps,
end_pause,
ack,
} => {
let result = if line == 0 || usize::from(line) > rows {
Err(VfdError::InvalidLine { line, rows })
} else {
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);
Ok(())
};
send_ack(ack, result);
}
Cmd::StopMarquee { ack } => {
if marquee.active && usize::from(marquee.line) <= last_lines.len() {
last_lines[(marquee.line - 1) as usize].clear();
}
marquee.active = false;
marquee.paused_until = None;
marquee.next_step = None;
send_ack(ack, Ok(()));
}
Cmd::Shutdown { ack } => {
send_ack(ack, Ok(()));
return Ok(true);
}
}
Ok(false)
}
async fn print_line_diff<T: AsyncWrite + Unpin>(
vfd: &mut AsyncVfd<T>,
marquee: &MarqueeState,
last_lines: &mut [String],
line: u8,
text: &str,
) -> Result<()> {
if line == 0 || usize::from(line) > vfd.rows() {
return Err(VfdError::InvalidLine {
line,
rows: vfd.rows(),
});
}
if marquee.active && marquee.line == line {
return Ok(());
}
let next = fit_to_width(&sanitize_text(text), vfd.columns());
let idx = (line - 1) as usize;
if last_lines[idx] == next {
return Ok(());
}
if last_lines[idx].is_empty() {
vfd.print_line(line, &next).await?;
last_lines[idx] = next;
return Ok(());
}
for (x, text) in changed_runs(&last_lines[idx], &next) {
vfd.print_at_prepared(x, line, &text).await?;
}
last_lines[idx] = next;
Ok(())
}
async fn print_at_cached<T: AsyncWrite + Unpin>(
vfd: &mut AsyncVfd<T>,
marquee: &MarqueeState,
last_lines: &mut [String],
x: u8,
y: u8,
text: &str,
) -> Result<()> {
if x == 0 || y == 0 || usize::from(x) > vfd.columns() || usize::from(y) > vfd.rows() {
return Err(VfdError::InvalidCoordinate {
x,
y,
columns: vfd.columns(),
rows: vfd.rows(),
});
}
if marquee.active && marquee.line == y {
return Ok(());
}
let remaining = vfd.columns() - usize::from(x) + 1;
let text: String = sanitize_text(text).chars().take(remaining).collect();
vfd.print_at_prepared(x, y, &text).await?;
replace_cached_range(&mut last_lines[(y - 1) as usize], x, &text, vfd.columns());
Ok(())
}
async fn render_marquee<T: AsyncWrite + Unpin>(
vfd: &mut AsyncVfd<T>,
marquee: &mut MarqueeState,
last_lines: &mut [String],
) -> Result<()> {
if !marquee.active {
return Ok(());
}
let now = Instant::now();
if let Some(until) = marquee.paused_until {
if now < until {
return Ok(());
}
marquee.paused_until = None;
marquee.next_step = Some(now + marquee.step_interval());
return Ok(());
}
if marquee.next_step.is_some_and(|deadline| now < deadline) {
return Ok(());
}
let width = vfd.columns();
if marquee.stream.len() < width {
marquee.rebuild_stream(width);
}
let max_off = marquee.stream.len().saturating_sub(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();
vfd.print_at_prepared(1, marquee.line, &frame).await?;
if usize::from(marquee.line) <= last_lines.len() {
last_lines[(marquee.line - 1) as usize] = frame;
}
if marquee.offset >= max_off {
marquee.offset = 0;
marquee.paused_until = Some(now + marquee.end_pause);
marquee.next_step = None;
} else {
marquee.offset += 1;
marquee.next_step = Some(now + marquee.step_interval());
}
Ok(())
}
fn send_ack(ack: Ack, result: Result<()>) {
let _ = ack.send(result);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{DisplaySettings, TextEncoding};
use std::io;
use std::pin::Pin;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::task::{Context, Poll};
struct AsyncFailsAfterWrites {
writes_left: usize,
failed: Arc<AtomicBool>,
}
impl AsyncWrite for AsyncFailsAfterWrites {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
if self.writes_left == 0 {
self.failed.store(true, Ordering::SeqCst);
return Poll::Ready(Err(io::Error::other("forced write failure")));
}
self.writes_left -= 1;
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn async_driver_writes_same_core_bytes() {
let display = DisplaySettings::new(6, 2, TextEncoding::Ascii);
let mut vfd = AsyncVfd::from_transport(Vec::<u8>::new(), display)
.await
.unwrap();
vfd.print_line(2, "abc").await.unwrap();
vfd.write_raw(&[0xAA]).await.unwrap();
assert_eq!(
vfd.into_inner(),
vec![
0x1B, 0x40, 0x1F, 0x24, 1, 2, b'a', b'b', b'c', b' ', b' ', b' ', 0xAA
]
);
}
#[tokio::test(start_paused = true)]
async fn async_worker_ack_and_marquee_scheduler() {
let display = DisplaySettings::new(5, 2, TextEncoding::Ascii);
let worker = AsyncVfdWorker::from_transport(Vec::<u8>::new(), display, 2)
.await
.unwrap();
let handle = worker.handle();
handle.set_marquee_text("abc").await.unwrap();
handle
.start_marquee(2, 10, Duration::from_millis(100))
.await
.unwrap();
::tokio::time::advance(Duration::from_millis(100)).await;
::tokio::task::yield_now().await;
handle.stop_marquee().await.unwrap();
let vfd = worker.shutdown().await.unwrap();
assert!(
vfd.into_inner()
.windows(4)
.any(|window| window == [0x1F, 0x24, 1, 2])
);
}
#[tokio::test]
async fn async_worker_rejects_zero_queue_capacity() {
let display = DisplaySettings::new(5, 2, TextEncoding::Ascii);
assert!(matches!(
AsyncVfdWorker::from_transport(Vec::<u8>::new(), display, 0).await,
Err(VfdError::Config(ConfigError::ZeroQueueCapacity))
));
}
#[tokio::test]
async fn async_worker_print_at_validates_coordinates_before_marquee_skip() {
let display = DisplaySettings::new(5, 2, TextEncoding::Ascii);
let worker = AsyncVfdWorker::from_transport(Vec::<u8>::new(), display, 2)
.await
.unwrap();
let handle = worker.handle();
handle
.start_marquee(2, 10, Duration::from_millis(100))
.await
.unwrap();
assert!(matches!(
handle.print_at(0, 2, "bad").await,
Err(VfdError::InvalidCoordinate { x: 0, y: 2, .. })
));
assert!(matches!(
handle.print_at(6, 2, "bad").await,
Err(VfdError::InvalidCoordinate { x: 6, y: 2, .. })
));
handle.print_at(1, 2, "skipped").await.unwrap();
worker.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_worker_shutdown_prefers_startup_io_error_over_closed_queue() {
let display = DisplaySettings::new(5, 2, TextEncoding::Ascii);
let failed = Arc::new(AtomicBool::new(false));
let transport = AsyncFailsAfterWrites {
writes_left: 1,
failed: Arc::clone(&failed),
};
let worker = AsyncVfdWorker::from_transport(transport, display, 2)
.await
.unwrap();
while !failed.load(Ordering::SeqCst) {
::tokio::task::yield_now().await;
}
assert!(matches!(worker.shutdown().await, Err(VfdError::Io(_))));
}
#[tokio::test(start_paused = true)]
async fn async_worker_exits_when_channel_closes_with_active_marquee() {
let display = DisplaySettings::new(5, 2, TextEncoding::Ascii);
let worker = AsyncVfdWorker::from_transport(Vec::<u8>::new(), display, 2)
.await
.unwrap();
let handle = worker.handle();
handle.set_marquee_text("abc").await.unwrap();
handle
.start_marquee(2, 10, Duration::from_millis(100))
.await
.unwrap();
drop(handle);
let mut worker = worker;
let join = worker.join.take().expect("join handle exists");
drop(worker);
let vfd = ::tokio::time::timeout(Duration::from_millis(1), async {
join.await.map_err(|_| VfdError::WorkerCancelled)?
})
.await
.expect("closed worker channel should terminate task")
.unwrap();
assert_eq!(vfd.rows(), 2);
}
}