use std::{
io::Read,
ops::ControlFlow,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use clap::{Parser, Subcommand};
use embedded_graphics::Drawable;
use miette::{miette, IntoDiagnostic, Result, WrapErr};
use rpi_st7789v2_driver::{DriverArgs, Driver};
use tracing::{error, info, instrument, trace};
use super::ItiArgs;
use crate::actions::Context;
pub mod json;
#[derive(Debug, Clone, Parser)]
pub struct LcdArgs {
#[arg(long, default_value = "0")]
pub spi: u8,
#[arg(long, default_value = "18")]
pub backlight: u8,
#[arg(long, default_value = "27")]
pub reset: u8,
#[arg(long, default_value = "25")]
pub dc: u8,
#[arg(long, default_value = "0")]
pub ce: u8,
#[arg(long, default_value = "20000000")]
pub frequency: u32,
#[arg(default_value = "tcp://[::1]:2009")]
pub zmq_socket: String,
#[command(subcommand)]
pub action: LcdAction,
}
impl From<LcdArgs> for DriverArgs {
fn from(args: LcdArgs) -> Self {
DriverArgs {
spi: args.spi,
backlight: args.backlight,
reset: args.reset,
dc: args.dc,
ce: args.ce,
frequency: args.frequency,
}
}
}
#[derive(Debug, Clone, Subcommand)]
pub enum LcdAction {
Serve,
Send {
message: Option<String>,
},
Clear {
#[arg(default_value = "0")]
red: u8,
#[arg(default_value = "0")]
green: u8,
#[arg(default_value = "0")]
blue: u8,
},
On,
Off,
}
pub async fn run(ctx: Context<ItiArgs, LcdArgs>) -> Result<()> {
use LcdAction::*;
match ctx.args_sub.action.clone() {
Serve => serve(ctx),
Send { message } => {
let screen = serde_json::from_str(&message.unwrap_or_else(|| {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf).expect("stdin: ");
buf
}))
.into_diagnostic()
.wrap_err("json: from_str")?;
send(&ctx.args_sub.zmq_socket, screen)
}
Clear { red, green, blue } => send(&ctx.args_sub.zmq_socket, json::Screen::Clear([red, green, blue])),
On => send(&ctx.args_sub.zmq_socket, json::Screen::Light(true)),
Off => send(&ctx.args_sub.zmq_socket, json::Screen::Light(false)),
}
}
#[instrument(level = "debug", skip(ctx))]
pub fn serve(ctx: Context<ItiArgs, LcdArgs>) -> Result<()> {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
})
.into_diagnostic()
.wrap_err("ctrlc: set_handler")?;
let z = zmq::Context::new();
let socket = z
.socket(zmq::REP)
.into_diagnostic()
.wrap_err("zmq: socket(REP)")?;
socket
.set_ipv6(true)
.into_diagnostic()
.wrap_err("zmq: set_ipv6")?;
socket
.bind(&ctx.args_sub.zmq_socket)
.into_diagnostic()
.wrap_err(format!("zmq: bind({})", ctx.args_sub.zmq_socket))?;
info!(
"ZMQ REP listening on {} for JSON messages",
ctx.args_sub.zmq_socket
);
let mut lcd = Driver::new(ctx.args_sub.into())?;
lcd.init()?;
lcd.probe_buffer_length()?;
loop {
match loop_inner(running.clone(), &socket, &mut lcd) {
Ok(ControlFlow::Continue(_)) => continue,
Ok(ControlFlow::Break(_)) => break,
Err(err) => {
let err = format!("{err:?}");
error!("{err}");
socket.send(&err, 0).ok();
continue;
}
}
}
Ok(())
}
#[instrument(level = "trace", skip(socket, lcd))]
fn loop_inner(
running: Arc<AtomicBool>,
socket: &zmq::Socket,
lcd: &mut Driver,
) -> Result<ControlFlow<()>> {
let mut polls = [socket.as_poll_item(zmq::POLLIN)];
let polled = zmq::poll(&mut polls, 1000)
.into_diagnostic()
.wrap_err("zmq: poll")?;
if running.load(Ordering::SeqCst) == false {
info!("ctrl-c received, exiting");
return Ok(ControlFlow::Break(()));
}
if polled == 0 || !polls[0].is_readable() {
trace!("zmq: no messages (poll timed out)");
return Ok(ControlFlow::Continue(()));
}
let bytes = socket
.recv_bytes(0)
.into_diagnostic()
.wrap_err("zmq: recv")?;
let screen: json::Screen = serde_json::from_slice(&bytes)
.into_diagnostic()
.wrap_err("json: parse")?;
trace!(?screen, "received screen control message");
use json::Screen::*;
match screen {
Light(true) => {
info!("turning screen on");
lcd.display(true)?;
lcd.backlight(true);
lcd.wake()?;
}
Light(false) => {
info!("turning screen off");
lcd.display(false)?;
lcd.backlight(false);
lcd.sleep()?;
}
otherwise => {
info!("updating screen {otherwise:?}");
otherwise.draw(lcd)?;
}
}
socket
.send(zmq::Message::new(), 0)
.into_diagnostic()
.wrap_err("zmq: send")?;
Ok(ControlFlow::Continue(()))
}
#[instrument(level = "debug")]
pub fn send(addr: &str, screen: json::Screen) -> Result<()> {
let z = zmq::Context::new();
let socket = z
.socket(zmq::REQ)
.into_diagnostic()
.wrap_err("zmq: socket(REQ)")?;
socket
.set_ipv6(true)
.into_diagnostic()
.wrap_err("zmq: set_ipv6")?;
socket
.connect(addr)
.into_diagnostic()
.wrap_err(format!("zmq: connect({})", addr))?;
let bytes = serde_json::to_vec(&screen)
.into_diagnostic()
.wrap_err("json: to_vec")?;
socket
.send(&bytes, 0)
.into_diagnostic()
.wrap_err("zmq: send")?;
let reply = socket
.recv_string(0)
.into_diagnostic()
.wrap_err("zmq: recv")?
.map_err(|bytes| miette!("reply is not valid utf-8, received {} bytes", bytes.len()))
.wrap_err("zmq: recv_string")?;
if !reply.is_empty() {
println!("{reply}");
}
Ok(())
}