Compare commits
2 commits
67e425c946
...
91be01196f
Author | SHA1 | Date | |
---|---|---|---|
saji | 91be01196f | ||
saji | c50f8cb955 |
57
src/api.rs
57
src/api.rs
|
@ -1,67 +1,17 @@
|
||||||
use crate::display::{create_display_thread, EInkPanel};
|
|
||||||
use crate::dither::{DitherMethod, DitheredImage};
|
use crate::dither::{DitherMethod, DitheredImage};
|
||||||
use crate::eink::Palette;
|
use crate::eink::Palette;
|
||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use axum::extract::{FromRequest, Multipart, State};
|
use axum::extract::{FromRequest, Multipart, State};
|
||||||
use axum::http::{header, StatusCode};
|
use axum::http::{header, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{response::Response, routing::post, Router};
|
use axum::{routing::post, Router};
|
||||||
use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage};
|
use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::mpsc;
|
use tracing::{info, instrument};
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread::JoinHandle;
|
|
||||||
use tracing::{error, info, instrument};
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
use crate::app::{ApiError, AppError, AppState};
|
||||||
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
|
/// API routes for axum
|
||||||
/// Start with the basics: Send an image, crop it, dither, and upload.
|
/// Start with the basics: Send an image, crop it, dither, and upload.
|
||||||
|
@ -132,7 +82,6 @@ async fn set_image(
|
||||||
State(ctx): State<AppState>,
|
State(ctx): State<AppState>,
|
||||||
img_req: ImageRequest,
|
img_req: ImageRequest,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
// FIXME: resize image to 800x480 to match the eink panel.
|
|
||||||
info!("Got image");
|
info!("Got image");
|
||||||
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
|
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
|
||||||
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
|
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
|
||||||
|
|
136
src/app.rs
Normal file
136
src/app.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,11 +51,7 @@ impl Wrapper {
|
||||||
let mut delay = Delay {};
|
let mut delay = Delay {};
|
||||||
let panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?;
|
let panel = Epd7in3f::new(&mut spi, busy_pin, dc_pin, rst_pin, &mut delay, None)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { spi, delay, panel })
|
||||||
spi,
|
|
||||||
delay,
|
|
||||||
panel,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
pub fn test(&mut self) -> Result<()> {
|
pub fn test(&mut self) -> Result<()> {
|
||||||
self.panel.show_7block(&mut self.spi, &mut self.delay)?;
|
self.panel.show_7block(&mut self.spi, &mut self.delay)?;
|
||||||
|
@ -109,8 +105,8 @@ pub fn get_display() -> Box<dyn EInkPanel + Send> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn create_display_thread(
|
pub fn create_display_thread(
|
||||||
mut display: Box<dyn EInkPanel + Send>,
|
|
||||||
) -> (thread::JoinHandle<()>, mpsc::Sender<Box<DitheredImage>>) {
|
) -> (thread::JoinHandle<()>, mpsc::Sender<Box<DitheredImage>>) {
|
||||||
|
let mut display = get_display();
|
||||||
let (tx, rx) = mpsc::channel::<Box<DitheredImage>>();
|
let (tx, rx) = mpsc::channel::<Box<DitheredImage>>();
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
let span = span!(Level::INFO, "display_thread");
|
let span = span!(Level::INFO, "display_thread");
|
||||||
|
|
|
@ -216,7 +216,10 @@ impl<'a> Ditherer for ErrorDiffusion<'a> {
|
||||||
let srgb = <&[Srgb<u8>]>::from_components(&**img);
|
let srgb = <&[Srgb<u8>]>::from_components(&**img);
|
||||||
let (xsize, ysize) = img.dimensions();
|
let (xsize, ysize) = img.dimensions();
|
||||||
// our destination buffer.
|
// 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();
|
let lab_palette: Vec<Lab> = output.palette.iter().map(|c| Lab::from_color(*c)).collect();
|
||||||
|
|
||||||
// TODO: rework this to make more sense.
|
// TODO: rework this to make more sense.
|
||||||
|
|
10
src/eink.rs
10
src/eink.rs
|
@ -16,8 +16,6 @@ const DISPLAY_PALETTE: [Srgb; 7] = [
|
||||||
Srgb::new(0.757, 0.443, 0.165), // Orange
|
Srgb::new(0.757, 0.443, 0.165), // Orange
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// A more primitive palette that doesn't reflect reality. Here for posterity.
|
/// A more primitive palette that doesn't reflect reality. Here for posterity.
|
||||||
/// This is based on the datasheet, not on empirical testing
|
/// This is based on the datasheet, not on empirical testing
|
||||||
const SIMPLE_PALETTE: [Srgb; 7] = [
|
const SIMPLE_PALETTE: [Srgb; 7] = [
|
||||||
|
@ -30,7 +28,9 @@ const SIMPLE_PALETTE: [Srgb; 7] = [
|
||||||
Srgb::new(0.757, 0.443, 0.165), // Orange
|
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 {
|
pub enum Palette {
|
||||||
Default,
|
Default,
|
||||||
Simple,
|
Simple,
|
||||||
|
@ -46,11 +46,9 @@ impl Palette {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Construct a dithered image for the EINK display using the default palette and correct
|
/// Construct a dithered image for the EINK display using the default palette and correct
|
||||||
/// resolution.
|
/// resolution.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new_image() -> DitheredImage {
|
pub fn new_image() -> DitheredImage {
|
||||||
DitheredImage::new(800,480, DISPLAY_PALETTE.to_vec())
|
DitheredImage::new(800, 480, DISPLAY_PALETTE.to_vec())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
pub mod app;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod dither;
|
pub mod dither;
|
||||||
pub mod eink;
|
pub mod eink;
|
||||||
pub mod app;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tower_http::trace::TraceLayer;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
use crate::display::get_display;
|
use crate::display::get_display;
|
||||||
use crate::dither::{DitherMethod, DitheredImage};
|
use crate::dither::{DitherMethod, DitheredImage};
|
||||||
|
@ -84,10 +84,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
display.display(&eink_buf)?;
|
display.display(&eink_buf)?;
|
||||||
}
|
}
|
||||||
Command::Serve => {
|
Command::Serve => {
|
||||||
let display = get_display();
|
|
||||||
let ctx = api::AppState::new(display);
|
|
||||||
|
|
||||||
let app = api::router().with_state(ctx).layer(TraceLayer::new_for_http());
|
let app = app::make_app_router()
|
||||||
|
.layer(TraceLayer::new_for_http());
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
||||||
info!("Listening on 0.0.0.0:3000");
|
info!("Listening on 0.0.0.0:3000");
|
||||||
axum::serve(listener, app).await?;
|
axum::serve(listener, app).await?;
|
||||||
|
|
Loading…
Reference in a new issue