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::eink::Palette;
|
||||
use axum::async_trait;
|
||||
use axum::extract::{FromRequest, Multipart, State};
|
||||
use axum::http::{header, StatusCode};
|
||||
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 std::io::Cursor;
|
||||
use std::str;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
use tracing::{error, info, instrument};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
use crate::app::{ApiError, AppError, AppState};
|
||||
|
||||
/// API routes for axum
|
||||
/// Start with the basics: Send an image, crop it, dither, and upload.
|
||||
|
@ -132,7 +82,6 @@ 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);
|
||||
|
|
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 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)?;
|
||||
|
@ -109,8 +105,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");
|
||||
|
|
|
@ -216,7 +216,10 @@ 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.
|
||||
|
|
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
|
||||
];
|
||||
|
||||
|
||||
|
||||
/// 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] = [
|
||||
|
@ -30,7 +28,9 @@ 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,11 +46,9 @@ impl Palette {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Construct a dithered image for the EINK display using the default palette and correct
|
||||
/// resolution.
|
||||
#[must_use]
|
||||
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 app;
|
||||
pub mod display;
|
||||
pub mod dither;
|
||||
pub mod eink;
|
||||
pub mod app;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use std::path::PathBuf;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::display::get_display;
|
||||
use crate::dither::{DitherMethod, DitheredImage};
|
||||
|
@ -84,10 +84,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
display.display(&eink_buf)?;
|
||||
}
|
||||
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?;
|
||||
info!("Listening on 0.0.0.0:3000");
|
||||
axum::serve(listener, app).await?;
|
||||
|
|
Loading…
Reference in a new issue