Compare commits

..

No commits in common. "91be01196fdbce6b427da8c92774718a92193be7" and "67e425c9466030449d516b2a6c946fc7e6a46032" have entirely different histories.

6 changed files with 72 additions and 153 deletions

View file

@ -1,17 +1,67 @@
use crate::display::{create_display_thread, EInkPanel};
use crate::dither::{DitherMethod, DitheredImage};
use crate::eink::Palette;
use axum::async_trait;
use axum::extract::{FromRequest, Multipart, State};
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use axum::{routing::post, Router};
use axum::{response::Response, routing::post, Router};
use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage};
use std::io::Cursor;
use std::str;
use std::str::FromStr;
use tracing::{info, instrument};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::JoinHandle;
use tracing::{error, info, instrument};
use crate::app::{ApiError, AppError, AppState};
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("missing image field")]
MissingImage,
}
#[derive(Clone)]
pub struct AppState {
display_channel: mpsc::Sender<Box<DitheredImage>>,
display_task: Arc<JoinHandle<()>>,
}
impl AppState {
#[must_use]
pub fn new(disp: Box<dyn EInkPanel + Send>) -> Self {
let (handle, tx) = create_display_thread(disp);
Self {
display_channel: tx,
display_task: Arc::new(handle),
}
}
}
// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
/// API routes for axum
/// Start with the basics: Send an image, crop it, dither, and upload.
@ -82,6 +132,7 @@ async fn set_image(
State(ctx): State<AppState>,
img_req: ImageRequest,
) -> Result<impl IntoResponse, AppError> {
// FIXME: resize image to 800x480 to match the eink panel.
info!("Got image");
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);

View file

@ -1,136 +0,0 @@
use crate::api;
use crate::display::create_display_thread;
use crate::dither::DitheredImage;
use axum::extract::{Path, State};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use include_dir::{include_dir, Dir, DirEntry};
use minijinja::Environment;
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::JoinHandle;
use tracing::{debug, warn};
use tracing::{error, info, instrument};
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("missing image field")]
MissingImage,
}
#[derive(Clone)]
pub struct AppState {
pub display_channel: mpsc::Sender<Box<DitheredImage>>,
pub display_task: Arc<JoinHandle<()>>,
pub templates: Arc<Environment<'static>>,
}
impl Default for AppState {
#[must_use]
fn default() -> Self {
let (handle, tx) = create_display_thread();
Self {
display_channel: tx,
display_task: Arc::new(handle),
templates: Arc::new(make_environment().expect("valid")),
}
}
}
/// Represents an error that can be returned to the requester by implementing ``IntoResponse``.
/// This wraps ``anyhow::Error`` so that it can accept any error object.
pub struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
#[instrument]
fn make_environment() -> Result<Environment<'static>, anyhow::Error> {
let mut env = Environment::new();
let mut entries = vec![&TEMPLATES_DIR];
while let Some(dir) = entries.pop() {
for entry in dir.entries() {
match entry {
DirEntry::Dir(d) => {
entries.push(d);
}
DirEntry::File(f) => {
debug!("adding {:?} to minijinja environment", f.path());
env.add_template(
f.path().to_str().expect("valid pathname"),
f.contents_utf8().expect("contents are valid"),
)?;
}
}
}
}
info!(
"loaded {} templates into environment",
env.templates().count()
);
Ok(env)
}
pub fn make_app_router() -> Router {
Router::new()
.route("/app/*path", get(app_handler))
.nest("/api", api::router())
.route("/assets", get(asset_handler))
.with_state(AppState::default())
}
#[instrument(skip(ctx))]
async fn app_handler(
State(ctx): State<AppState>,
Path(path): Path<String>,
) -> Result<impl IntoResponse, AppError> {
let template = ctx.templates.get_template(&path.to_string())?;
let content = template.render("")?;
let headers = [(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.to_string())];
Ok((StatusCode::OK, headers, content))
}
async fn asset_handler() -> Result<impl IntoResponse, AppError> {
Ok(StatusCode::OK)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment() -> anyhow::Result<()> {
make_environment()?;
Ok(())
}
}

View file

@ -51,7 +51,11 @@ impl Wrapper {
let mut delay = Delay {};
let panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?;
Ok(Self { spi, delay, panel })
Ok(Self {
spi,
delay,
panel,
})
}
pub fn test(&mut self) -> Result<()> {
self.panel.show_7block(&mut self.spi, &mut self.delay)?;
@ -105,8 +109,8 @@ pub fn get_display() -> Box<dyn EInkPanel + Send> {
#[must_use]
#[instrument(skip_all)]
pub fn create_display_thread(
mut display: Box<dyn EInkPanel + Send>,
) -> (thread::JoinHandle<()>, mpsc::Sender<Box<DitheredImage>>) {
let mut display = get_display();
let (tx, rx) = mpsc::channel::<Box<DitheredImage>>();
let handle = thread::spawn(move || {
let span = span!(Level::INFO, "display_thread");

View file

@ -216,10 +216,7 @@ impl<'a> Ditherer for ErrorDiffusion<'a> {
let srgb = <&[Srgb<u8>]>::from_components(&**img);
let (xsize, ysize) = img.dimensions();
// our destination buffer.
let mut temp_img: Vec<Lab> = srgb
.par_iter()
.map(|s| s.into_format().into_color())
.collect();
let mut temp_img: Vec<Lab> = srgb.par_iter().map(|s| s.into_format().into_color()).collect();
let lab_palette: Vec<Lab> = output.palette.iter().map(|c| Lab::from_color(*c)).collect();
// TODO: rework this to make more sense.

View file

@ -16,6 +16,8 @@ const DISPLAY_PALETTE: [Srgb; 7] = [
Srgb::new(0.757, 0.443, 0.165), // Orange
];
/// A more primitive palette that doesn't reflect reality. Here for posterity.
/// This is based on the datasheet, not on empirical testing
const SIMPLE_PALETTE: [Srgb; 7] = [
@ -28,9 +30,7 @@ const SIMPLE_PALETTE: [Srgb; 7] = [
Srgb::new(0.757, 0.443, 0.165), // Orange
];
#[derive(
strum::EnumString, strum::Display, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy,
)]
#[derive(strum::EnumString, strum::Display, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
pub enum Palette {
Default,
Simple,
@ -46,6 +46,8 @@ impl Palette {
}
}
/// Construct a dithered image for the EINK display using the default palette and correct
/// resolution.
#[must_use]

View file

@ -1,12 +1,12 @@
pub mod api;
pub mod app;
pub mod display;
pub mod dither;
pub mod eink;
pub mod app;
use serde::Deserialize;
use std::path::PathBuf;
use tower_http::trace::TraceLayer;
use std::path::PathBuf;
use crate::display::get_display;
use crate::dither::{DitherMethod, DitheredImage};
@ -84,9 +84,10 @@ async fn main() -> anyhow::Result<()> {
display.display(&eink_buf)?;
}
Command::Serve => {
let display = get_display();
let ctx = api::AppState::new(display);
let app = app::make_app_router()
.layer(TraceLayer::new_for_http());
let app = api::router().with_state(ctx).layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
info!("Listening on 0.0.0.0:3000");
axum::serve(listener, app).await?;