use std::io::Read;
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 zeromq::{Socket as _, SocketRecv as _, SocketSend as _, ZmqMessage};
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(args: LcdArgs, _ctx: Context) -> Result<()> {
use LcdAction::*;
match args.action.clone() {
Serve => serve(args).await,
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(&args.zmq_socket, screen).await
}
Clear { red, green, blue } => {
send(&args.zmq_socket, json::Screen::Clear([red, green, blue])).await
}
On => send(&args.zmq_socket, json::Screen::Light(true)).await,
Off => send(&args.zmq_socket, json::Screen::Light(false)).await,
}
}
#[instrument(level = "debug", skip(args))]
pub async fn serve(args: LcdArgs) -> Result<()> {
let mut socket = zeromq::RepSocket::new();
socket
.bind(&args.zmq_socket)
.await
.into_diagnostic()
.wrap_err(format!("zmq: bind({})", args.zmq_socket))?;
info!(
"ZMQ REP listening on {} for JSON messages",
args.zmq_socket
);
let mut lcd = Driver::new(args.into())?;
lcd.init()?;
lcd.probe_buffer_length()?;
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("ctrl-c received, exiting");
break;
}
request = socket.recv() => {
let request = request.into_diagnostic().wrap_err("zmq: recv")?;
let reply = match handle(request, &mut lcd) {
Ok(()) => ZmqMessage::from(""),
Err(err) => {
let err = format!("{err:?}");
error!("{err}");
ZmqMessage::from(err)
}
};
socket
.send(reply)
.await
.into_diagnostic()
.wrap_err("zmq: send")?;
}
}
}
Ok(())
}
#[instrument(level = "trace", skip(request, lcd))]
fn handle(request: ZmqMessage, lcd: &mut Driver) -> Result<()> {
let bytes = request.get(0).ok_or_else(|| miette!("zmq: empty message"))?;
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)?;
}
}
Ok(())
}
#[instrument(level = "debug")]
pub async fn send(addr: &str, screen: json::Screen) -> Result<()> {
let mut socket = zeromq::ReqSocket::new();
socket
.connect(addr)
.await
.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.into())
.await
.into_diagnostic()
.wrap_err("zmq: send")?;
let reply = socket.recv().await.into_diagnostic().wrap_err("zmq: recv")?;
let reply: String = reply
.try_into()
.map_err(|err| miette!("zmq: reply is not valid utf-8: {err}"))?;
if !reply.is_empty() {
println!("{reply}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn send_roundtrip() {
let mut server = zeromq::RepSocket::new();
let endpoint = server.bind("tcp://127.0.0.1:0").await.unwrap();
let server_task = tokio::spawn(async move {
let request = server.recv().await.unwrap();
let screen: json::Screen = serde_json::from_slice(request.get(0).unwrap()).unwrap();
assert!(matches!(screen, json::Screen::Light(true)));
server.send(ZmqMessage::from("")).await.unwrap();
});
send(&endpoint.to_string(), json::Screen::Light(true))
.await
.unwrap();
server_task.await.unwrap();
}
}