diff --git a/.gitignore b/.gitignore index 22669e2..a2b14e8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ orient-beetle-ci.key env.toml local-env.toml .storage -*.png +/*.png diff --git a/src/beetle-pio/lib/redis-events/redis-events.hpp b/src/beetle-pio/lib/redis-events/redis-events.hpp index ce3f4c7..38f28dc 100644 --- a/src/beetle-pio/lib/redis-events/redis-events.hpp +++ b/src/beetle-pio/lib/redis-events/redis-events.hpp @@ -149,6 +149,7 @@ class Events final { // If we ready an array response but the length is -1 or 0, // we're no longer expecting any messages if (array_read.size == -1 || array_read.size == 0) { + log_i("empty array received while waiting for message pop"); c.state = NotReceiving{true}; return std::make_pair(c, std::nullopt); } @@ -184,6 +185,16 @@ class Events final { } } + if (std::holds_alternative(c.state)) { + ReceivingPop *popping = std::get_if(&c.state); + uint32_t time_diff = time - popping->timeout_start; + + if (time_diff > 1000) { + log_i("timeout!"); + popping->timeout_start = time; + } + } + return std::make_pair(c, std::nullopt); } @@ -270,7 +281,7 @@ class Events final { context->device_id_len, context->device_id, context->device_id_len, context->device_id); context->client.print(context->outbound); - log_i("wrote auth: '%s'", context->outbound); + log_d("wrote auth: '%s'", context->outbound); c.authorization_stage = AuthorizationStage::AuthorizationAttempted; return std::make_pair(c, IdentificationReceived{}); } @@ -298,6 +309,7 @@ class Events final { struct ReceivingPop final { int32_t payload_count = 0; int32_t payload_position = 0; + uint32_t timeout_start = 0; }; struct NotReceiving final { diff --git a/src/beetle-pio/src/xiao-rendering.hpp b/src/beetle-pio/src/xiao-rendering.hpp index 5c3b38c..aa2b51d 100644 --- a/src/beetle-pio/src/xiao-rendering.hpp +++ b/src/beetle-pio/src/xiao-rendering.hpp @@ -41,11 +41,11 @@ void draw_row(PNGDRAW *draw_context) { float l = lum(r, g, b); uint16_t color = GxEPD_WHITE; - if (l < lum(0x7b, 0x7d, 0x7b)) { + if (l < 64) { color = GxEPD_BLACK; - } else if (l < lum(0xc5, 0xc2, 0xc5)) { + } else if (l < 160) { color = GxEPD_DARKGREY; - } else if (l < lum(0xaa, 0xaa, 0xaa)) { + } else if (l < 223) { color = GxEPD_LIGHTGREY; } diff --git a/src/beetle-srv/src/api/jobs.rs b/src/beetle-srv/src/api/jobs.rs index 9050f22..d9c00a3 100644 --- a/src/beetle-srv/src/api/jobs.rs +++ b/src/beetle-srv/src/api/jobs.rs @@ -91,65 +91,70 @@ pub async fn queue(mut request: tide::Request) -> tide::R .content_type() .ok_or_else(|| tide::Error::from_str(422, "missing content-type"))?; - // TODO[image-upload]: we will circle back to this and make it more graceful: - // 1. handle the parameterization of `device_id` better - // 2. add support for multiple image types & verify the mime "essense" is correct. - if content_type.essence() == "image/jpeg" && request.param("device_id").is_ok() { - let device_id = request.param("device_id").unwrap().to_string(); - - // TODO: borrow scoping... - { - let worker = request.state(); - if worker.user_access(&user.oid, &device_id).await?.is_none() { - return Err(tide::Error::from_str(404, "not-found")); + match content_type.essence() { + image_kind @ "image/jpeg" | image_kind @ "image/png" => { + let device_id = request + .param("device_id")? + // .ok_or_else(|| tide::Error::from_str(422, "bad-id")) + .to_string(); + + // TODO: borrow scoping... + { + let worker = request.state(); + if worker.user_access(&user.oid, &device_id).await?.is_none() { + return Err(tide::Error::from_str(404, "not-found")); + } } - } - - let size = request - .len() - .ok_or_else(|| tide::Error::from_str(422, "missing image upload size"))?; - - if size > 800000usize { - return Err(tide::Error::from_str(422, "image too large")); - } - log::debug!("has image upload for device queue '{device_id}' of {size} bytes"); - let mut bytes = request.take_body(); - let mut storage_dest = std::path::PathBuf::new(); - storage_dest.push(&request.state().web_configuration.temp_file_storage); - async_std::fs::create_dir_all(&storage_dest).await.map_err(|error| { - log::error!("unable to ensure temporary file storage dir exists - {error}"); - tide::Error::from_str(500, "bad") - })?; - let file_name = uuid::Uuid::new_v4().to_string(); + let size = request + .len() + .ok_or_else(|| tide::Error::from_str(422, "missing image upload size"))?; - storage_dest.push(&file_name); - storage_dest.set_extension("jpg"); + if size > 800000usize { + return Err(tide::Error::from_str(422, "image too large")); + } - log::info!("writing temporary file to '{storage_dest:?}"); - let mut file = async_std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(&storage_dest) - .await - .map_err(|error| { - log::error!("unable to create temporary file for upload - {error}"); + log::debug!("has image upload for device queue '{device_id}' of {size} bytes"); + let mut bytes = request.take_body(); + let mut storage_dest = std::path::PathBuf::new(); + storage_dest.push(&request.state().web_configuration.temp_file_storage); + async_std::fs::create_dir_all(&storage_dest).await.map_err(|error| { + log::error!("unable to ensure temporary file storage dir exists - {error}"); + tide::Error::from_str(500, "bad") + })?; + let file_name = uuid::Uuid::new_v4().to_string(); + + storage_dest.push(&file_name); + storage_dest.set_extension(if image_kind == "image/jpeg" { "jpg" } else { "png" }); + + log::info!("writing temporary file to '{storage_dest:?}"); + let mut file = async_std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(&storage_dest) + .await + .map_err(|error| { + log::error!("unable to create temporary file for upload - {error}"); + tide::Error::from_str(500, "bad") + })?; + + async_std::io::copy(&mut bytes, &mut file).await.map_err(|error| { + log::error!("unable to copy file upload - {error}"); tide::Error::from_str(500, "bad") })?; - async_std::io::copy(&mut bytes, &mut file).await.map_err(|error| { - log::error!("unable to copy file upload - {error}"); - tide::Error::from_str(500, "bad") - })?; - - let job = registrar::RegistrarJobKind::Renders(registrar::jobs::RegistrarRenderKinds::SendImage { - location: storage_dest.to_string_lossy().to_string(), - device_id, - }); - let worker = request.state(); - let id = worker.queue_job_kind(job).await?; + let job = registrar::RegistrarJobKind::Renders(registrar::jobs::RegistrarRenderKinds::SendImage { + location: storage_dest.to_string_lossy().to_string(), + device_id, + }); + let worker = request.state(); + let id = worker.queue_job_kind(job).await?; - return tide::Body::from_json(&QueueResponse { id }).map(|body| tide::Response::builder(200).body(body).build()); + return tide::Body::from_json(&QueueResponse { id }).map(|body| tide::Response::builder(200).body(body).build()); + } + other => { + log::warn!("strange content type - '{other}'"); + } } let queue_payload = request.body_json::().await.map_err(|error| { diff --git a/src/beetle-srv/src/rendering/mod.rs b/src/beetle-srv/src/rendering/mod.rs index cb63e53..8693cdf 100644 --- a/src/beetle-srv/src/rendering/mod.rs +++ b/src/beetle-srv/src/rendering/mod.rs @@ -105,11 +105,7 @@ where .map_err(|error| io::Error::new(io::ErrorKind::Other, error.to_string()))?; raw_image = raw_image.resize(dimensions.0, dimensions.1, image::imageops::FilterType::CatmullRom); - image = raw_image - .grayscale() - .as_luma8() - .cloned() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "unable to create grayscale"))?; + image = raw_image.grayscale().to_luma8(); log::info!("grayscale of '{}' successful", location.as_ref()); } diff --git a/src/beetle-srv/src/rendering/renderer.rs b/src/beetle-srv/src/rendering/renderer.rs index 915ed62..f28c172 100644 --- a/src/beetle-srv/src/rendering/renderer.rs +++ b/src/beetle-srv/src/rendering/renderer.rs @@ -119,7 +119,7 @@ impl Worker { let queue_error = match self.send_layout(&mut c, &queue_id, queued_render.layout.clone()).await { Ok(_) => None, Err(error) => { - log::warn!("uanble to send layout - {error:}"); + log::warn!("unable to send layout - {error:}"); Some(format!("{error:?}")) } }; diff --git a/src/beetle-ui/src/Route/Device.elm b/src/beetle-ui/src/Route/Device.elm index ac3c62f..1bfd56f 100644 --- a/src/beetle-ui/src/Route/Device.elm +++ b/src/beetle-ui/src/Route/Device.elm @@ -197,7 +197,7 @@ view model env = let fileInputNode = Html.input - [ ATT.accept ".jpg,.jpeg" + [ ATT.accept ".jpg,.jpeg,.png" , ATT.type_ "file" , EV.on "change" (D.map ReceivedFiles filesDecoder) ] diff --git a/tools/beetle-mock/src/arguments.rs b/tools/beetle-mock/src/arguments.rs new file mode 100644 index 0000000..9d8b7ba --- /dev/null +++ b/tools/beetle-mock/src/arguments.rs @@ -0,0 +1,11 @@ +use clap::Parser; + +#[derive(Parser)] +#[command(author, version = option_env!("BEETLE_VERSION").unwrap_or_else(|| "dev"), about, long_about = None)] +pub struct CommandLineArguments { + #[clap(short, long, default_value = "env.toml")] + pub config: String, + + #[clap(short, long, default_value = ".storage")] + pub storage: String, +} diff --git a/tools/beetle-mock/src/id.rs b/tools/beetle-mock/src/id.rs new file mode 100644 index 0000000..98e5e8a --- /dev/null +++ b/tools/beetle-mock/src/id.rs @@ -0,0 +1,98 @@ +use super::arguments::CommandLineArguments; +use std::io; + +pub async fn get_device_id( + args: &CommandLineArguments, + config: &beetle::registrar::Configuration, + mut connection: &mut beetle::redis::RedisConnection, +) -> io::Result { + let mut id_storage_path = std::path::PathBuf::from(&args.storage); + id_storage_path.push(".device_id"); + + let (id_user, id_password) = config + .registrar + .id_consumer_username + .as_ref() + .zip(config.registrar.id_consumer_password.as_ref()) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "Configuration is missing registrar burn-in credentials", + ) + })?; + + match async_std::fs::metadata(&id_storage_path).await { + Err(error) if error.kind() == io::ErrorKind::NotFound => { + let burnin_auth_response = match kramer::execute( + &mut connection, + kramer::Command::<&str, &str>::Auth(kramer::AuthCredentials::User((id_user.as_str(), id_password.as_str()))), + ) + .await + { + Ok(kramer::Response::Item(kramer::ResponseValue::String(inner))) if inner == "OK" => inner, + other => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("unable to authenticate with redis - {other:?} (as {id_user})"), + )) + } + }; + + log::info!("initial handshake completed {burnin_auth_response:?}, taking a device id"); + + let mock_device_id = match kramer::execute( + &mut connection, + kramer::Command::<&str, &str>::Lists(kramer::ListCommand::Pop( + kramer::Side::Left, + beetle::constants::REGISTRAR_AVAILABLE, + None, + )), + ) + .await + { + Ok(kramer::Response::Item(kramer::ResponseValue::String(id))) => id, + other => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("unable to pull id - {other:?}"), + )) + } + }; + + log::info!("device id taken - {mock_device_id:?}"); + + match kramer::execute( + &mut connection, + kramer::Command::<&str, &str>::Auth(kramer::AuthCredentials::User((&mock_device_id, &mock_device_id))), + ) + .await + { + Ok(kramer::Response::Item(kramer::ResponseValue::String(inner))) if inner == "OK" => (), + other => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("unable to authenticate with redis - {other:?}"), + )) + } + } + + log::info!("preparing '{}' for device id storage", args.storage); + async_std::fs::create_dir_all(&args.storage).await?; + async_std::fs::write(&id_storage_path, &mock_device_id).await?; + + Ok(mock_device_id) + } + + Ok(meta) if meta.is_file() => { + log::info!("found existing device id at '{:?}'", id_storage_path); + let loaded_id = async_std::fs::read_to_string(&id_storage_path).await?; + log::info!("loaded device id - '{loaded_id}'"); + + Ok(loaded_id) + } + other @ Ok(_) | other @ Err(_) => Err(io::Error::new( + io::ErrorKind::Other, + format!("unable to handle device id storage lookup - {other:?}"), + )), + } +} diff --git a/tools/beetle-mock/src/main.rs b/tools/beetle-mock/src/main.rs index 47f8f8f..8b1a76a 100644 --- a/tools/beetle-mock/src/main.rs +++ b/tools/beetle-mock/src/main.rs @@ -8,94 +8,14 @@ use clap::Parser; use iced::Application; use std::io; -#[derive(Parser)] -#[command(author, version = option_env!("BEETLE_VERSION").unwrap_or_else(|| "dev"), about, long_about = None)] -struct CommandLineArguments { - #[clap(short, long, default_value = "env.toml")] - config: String, - - #[clap(short, long, default_value = ".storage")] - storage: String, -} - -#[derive(Debug, PartialEq)] -enum BulkStringLocation { - Sizing(usize), - Reading(usize, String, bool), -} - -#[derive(Debug, Default, PartialEq)] -enum MessageState { - #[default] - Initial, - - ArraySize(usize, bool), - - BulkString(BulkStringLocation, Option<(Vec, usize)>), - - Error(String), -} - -impl MessageState { - fn take(self, token: char) -> Self { - match (self, token) { - (Self::Initial, '*') => Self::ArraySize(0, false), - (Self::Initial, '$') => Self::BulkString(BulkStringLocation::Sizing(0), None), - - (Self::ArraySize(accumulator, _), '\r') => Self::ArraySize(accumulator, true), - (Self::ArraySize(accumulator, _), '\n') => { - Self::BulkString(BulkStringLocation::Sizing(0), Some((Vec::new(), accumulator))) - } +mod arguments; +use arguments::CommandLineArguments; - (Self::ArraySize(accumulator, false), token) => Self::ArraySize( - (accumulator * 10) + token.to_digit(10).unwrap_or_default() as usize, - false, - ), +mod id; +use id::get_device_id; - (Self::BulkString(BulkStringLocation::Sizing(_), Some((vec, array_size))), '$') => { - Self::BulkString(BulkStringLocation::Sizing(0), Some((vec, array_size))) - } - (Self::BulkString(BulkStringLocation::Sizing(size), Some((vec, array_size))), '\r') => { - Self::BulkString(BulkStringLocation::Sizing(size), Some((vec, array_size))) - } - - // Terminate Bulk String Sizing - (Self::BulkString(BulkStringLocation::Sizing(size), Some((vec, array_size))), '\n') => Self::BulkString( - BulkStringLocation::Reading(size, String::new(), false), - Some((vec, array_size)), - ), - - (Self::BulkString(BulkStringLocation::Sizing(size), Some((vec, array_size))), token) => { - let new_size = (size * 10) + token.to_digit(10).unwrap_or_default() as usize; - Self::BulkString(BulkStringLocation::Sizing(new_size), Some((vec, array_size))) - } - - // Start Bulk String Terminate - (Self::BulkString(BulkStringLocation::Reading(size, mut buffer, false), Some((vec, array_size))), '\r') => { - buffer.push(token); - Self::BulkString(BulkStringLocation::Reading(size, buffer, true), Some((vec, array_size))) - } - - // Start Bulk String Terminate - (Self::BulkString(BulkStringLocation::Reading(_, mut buffer, true), Some((mut vec, array_size))), '\n') => { - vec.push(buffer.drain(0..buffer.len() - 1).collect()); - Self::BulkString(BulkStringLocation::Sizing(0), Some((vec, array_size))) - } - - (Self::BulkString(BulkStringLocation::Reading(size, mut buffer, _), Some((vec, array_size))), token) => { - buffer.push(token); - Self::BulkString( - BulkStringLocation::Reading(size, buffer, false), - Some((vec, array_size)), - ) - } - - (Self::Initial, token) => Self::Error(format!("Invalid starting token '{token}'")), - (Self::Error(e), _) => Self::Error(e), - (_, token) => Self::Error(format!("Invalid token '{token}'")), - } - } -} +mod redis_reader; +use redis_reader::{MessageState, RedisResponse}; fn save_image(args: &CommandLineArguments, image_buffer: &Vec) -> io::Result<()> { log::debug!("attempting to save image buffer of {} byte(s)", image_buffer.len()); @@ -122,102 +42,6 @@ fn save_image(args: &CommandLineArguments, image_buffer: &Vec) -> io::Result Ok(()) } -async fn get_device_id( - args: &CommandLineArguments, - config: &beetle::registrar::Configuration, - mut connection: &mut beetle::redis::RedisConnection, -) -> io::Result { - let mut id_storage_path = std::path::PathBuf::from(&args.storage); - id_storage_path.push(".device_id"); - - let (id_user, id_password) = config - .registrar - .id_consumer_username - .as_ref() - .zip(config.registrar.id_consumer_password.as_ref()) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::Other, - "Configuration is missing registrar burn-in credentials", - ) - })?; - - match async_std::fs::metadata(&id_storage_path).await { - Err(error) if error.kind() == io::ErrorKind::NotFound => { - let burnin_auth_response = match kramer::execute( - &mut connection, - kramer::Command::<&str, &str>::Auth(kramer::AuthCredentials::User((id_user.as_str(), id_password.as_str()))), - ) - .await - { - Ok(kramer::Response::Item(kramer::ResponseValue::String(inner))) if inner == "OK" => inner, - other => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("unable to authenticate with redis - {other:?} (as {id_user})"), - )) - } - }; - - log::info!("initial handshake completed {burnin_auth_response:?}, taking a device id"); - - let mock_device_id = match kramer::execute( - &mut connection, - kramer::Command::<&str, &str>::Lists(kramer::ListCommand::Pop( - kramer::Side::Left, - beetle::constants::REGISTRAR_AVAILABLE, - None, - )), - ) - .await - { - Ok(kramer::Response::Item(kramer::ResponseValue::String(id))) => id, - other => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("unable to pull id - {other:?}"), - )) - } - }; - - log::info!("device id taken - {mock_device_id:?}"); - - match kramer::execute( - &mut connection, - kramer::Command::<&str, &str>::Auth(kramer::AuthCredentials::User((&mock_device_id, &mock_device_id))), - ) - .await - { - Ok(kramer::Response::Item(kramer::ResponseValue::String(inner))) if inner == "OK" => (), - other => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("unable to authenticate with redis - {other:?}"), - )) - } - } - - log::info!("preparing '{}' for device id storage", args.storage); - async_std::fs::create_dir_all(&args.storage).await?; - async_std::fs::write(&id_storage_path, &mock_device_id).await?; - - Ok(mock_device_id) - } - - Ok(meta) if meta.is_file() => { - log::info!("found existing device id at '{:?}'", id_storage_path); - let loaded_id = async_std::fs::read_to_string(&id_storage_path).await?; - log::info!("loaded device id - '{loaded_id}'"); - - Ok(loaded_id) - } - other @ Ok(_) | other @ Err(_) => Err(io::Error::new( - io::ErrorKind::Other, - format!("unable to handle device id storage lookup - {other:?}"), - )), - } -} - async fn run_background(args: CommandLineArguments, s: async_std::channel::Sender>) -> io::Result<()> { let contents = async_std::fs::read_to_string(&args.config).await?; let config = toml::from_str::(&contents).map_err(|error| { @@ -260,83 +84,36 @@ async fn run_background(args: CommandLineArguments, s: async_std::channel::Sende ) .await?; - let mut image_buffer: Vec = Vec::with_capacity(1024 * 10); + let mut image_buffer: Vec = Vec::with_capacity(1024 * 104); let mut parser = MessageState::default(); log::info!("pop written, waiting for response"); + let mut read_count = 0; // TODO: this does not seem very structurally sound; the goal is to read from the redis // connection, attempting to parse our pop messages as a payload of image data. 'response_read: loop { + read_count += 1; let mut frame_buffer = [0u8; 1024 * 80]; - match async_std::io::timeout( + let response = match async_std::io::timeout( std::time::Duration::from_secs(6), async_std::io::ReadExt::read(&mut connection, &mut frame_buffer), ) .await { Ok(read_amount) => { - log::info!("has {read_amount} bytes"); - let parsed = std::str::from_utf8(&frame_buffer[0..read_amount]); + log::info!("read {read_amount} bytes on read#{read_count}"); - if read_amount == 5 && matches!(parsed, Ok("*-1\r\n")) { - log::info!("empty read from redis, moving on immediately"); - break 'response_read; - } - - // Try to parse _something_ - this will normally be the `*2\r\n...` bit that contains our - // array size followed by two entries for the key + actual image data. - let (message_header, header_size) = match parsed { - Ok(inner) => { - log::info!("parsed whole buffer as utf-8 - '{inner}'"); - (Ok(inner), read_amount) - } - Err(error) if error.valid_up_to() > 0 => { - log::warn!("parially read buffer as utf8 - {error:?}"); - let header_size = error.valid_up_to(); - (std::str::from_utf8(&frame_buffer[0..header_size]), header_size) - } - Err(other) => { - log::warn!("unable to parse anything in frame buffer - {other:?}"); - (Err(other), 0) - } - }; - - if let Ok(header) = message_header { - log::info!("parser started with {parser:?}"); - parser = header.chars().fold(parser, |p, c| p.take(c)); - } + parser = frame_buffer[0..read_amount] + .iter() + .fold(parser, |current_parser, token| current_parser.take(*token)); - log::info!("parser concluded with {parser:?}"); - - match &parser { - // If we finished parsing the `message_header` as a bulk string that is yet to be read, - // attempt to push into our actual image buffer that range of the slice starting from - // where the header ended, to where the image is expected to end. - MessageState::BulkString(BulkStringLocation::Reading(remainder, _, _), _) => { - if header_size > 0 { - let terminal = header_size + remainder; - log::info!( - "image located @ {header_size} -> {terminal}, currently have {} bytes", - read_amount, - ); - - if frame_buffer.len() < terminal { - log::warn!("confused - header: '{header_size}' remainder: '{remainder}'"); - } else { - image_buffer.extend_from_slice(&frame_buffer[header_size..terminal]); - } - - break 'response_read; - } - } - MessageState::BulkString(BulkStringLocation::Sizing(0), Some((messages, arr_size))) - if *arr_size == messages.len() => - { - log::info!("found command = {messages:?}"); - } - other => log::warn!("parser finished with unexpected result - {other:?}"), + if let MessageState::Complete(ref terminal) = parser { + terminal + } else { + log::info!("message reading concluded incomplete parser, preparing for read {read_count}"); + continue; } } @@ -349,7 +126,24 @@ async fn run_background(args: CommandLineArguments, s: async_std::channel::Sende log::warn!("unknown error while reading - {error}"); return Err(error); } + }; + + match response { + RedisResponse::Array(ref items) if items.len() == 2 => { + let queue_name = String::from_utf8(items.get(0).unwrap().clone()); + log::info!("has image from {queue_name:?}"); + let buffer = items.get(1).unwrap(); + image_buffer.extend_from_slice(buffer.as_slice()); + } + RedisResponse::Array(_) => { + log::trace!("empty array response"); + } + other => { + log::warn!("unhandled redis response - {other:?}"); + } } + + break; } if !image_buffer.is_empty() { @@ -362,7 +156,7 @@ async fn run_background(args: CommandLineArguments, s: async_std::channel::Sende .map_err(|error| io::Error::new(io::ErrorKind::Other, error.to_string()))?; } - log::info!("writing message '{mock_device_id}' for keep-alive",); + log::info!("writing message '{mock_device_id}' for keep-alive"); let response = kramer::execute( &mut connection, @@ -476,41 +270,3 @@ fn main() -> io::Result<()> { settings.window.resizable = false; BeetleMock::run(settings).map_err(|error| io::Error::new(io::ErrorKind::Other, error.to_string())) } - -#[cfg(test)] -mod tests { - use super::{BulkStringLocation, MessageState}; - - #[test] - fn test_array_message_single() { - let mut parser = MessageState::default(); - for token in "*1\r\n$2\r\nhi\r\n".chars() { - parser = parser.take(token); - } - assert_eq!( - parser, - MessageState::BulkString(BulkStringLocation::Sizing(0), Some((vec!["hi".to_string()], 1))) - ); - } - - #[test] - fn test_array_message_many() { - let mut parser = MessageState::default(); - let mut buffer = "*11\r\n".to_string(); - let mut expected = Vec::new(); - - for _ in 0..11 { - buffer.push_str("$2\r\nhi\r\n"); - expected.push("hi".to_string()); - } - - for token in buffer.chars() { - parser = parser.take(token); - } - - assert_eq!( - parser, - MessageState::BulkString(BulkStringLocation::Sizing(0), Some((expected, 11))) - ); - } -} diff --git a/tools/beetle-mock/src/redis_reader.rs b/tools/beetle-mock/src/redis_reader.rs new file mode 100644 index 0000000..4bff800 --- /dev/null +++ b/tools/beetle-mock/src/redis_reader.rs @@ -0,0 +1,331 @@ +use std::io; + +pub enum SizeResult { + Complete(i32), + Incomplete(io::Result), +} + +#[derive(Debug, PartialEq)] +pub struct SizeCollector { + pub size: i32, + pub terminating: bool, + pub sign: i32, +} + +impl Default for SizeCollector { + fn default() -> Self { + Self { + size: 0, + terminating: false, + sign: 1, + } + } +} + +impl SizeCollector { + fn take(mut self, token: u8) -> SizeResult { + if token == b'-' { + return SizeResult::Incomplete(Ok(Self { + size: 0, + terminating: false, + sign: -1, + })); + } + + if token == b'\r' { + return SizeResult::Incomplete(Ok(Self { + size: self.size, + terminating: true, + sign: self.sign, + })); + } + + if self.terminating && token == b'\n' { + return SizeResult::Complete(self.size * self.sign); + } + + self.size = (self.size * 10) + (token - b'0') as i32; + + SizeResult::Incomplete(Ok(self)) + } +} + +#[derive(Debug, PartialEq)] +pub enum StringCollector { + Sizing(SizeCollector), + + Collecting { + /// The length of the string. + total_count: i32, + current_count: i32, + items: Vec, + }, +} + +pub enum StringCollectorResult { + Finished(Vec), + Incomplete(io::Result), +} + +impl StringCollector { + pub fn take(self, token: u8) -> StringCollectorResult { + match self { + Self::Sizing(collector) => { + if token == b'$' { + return StringCollectorResult::Incomplete(Ok(Self::Sizing(SizeCollector::default()))); + } + match collector.take(token) { + SizeResult::Complete(size) => StringCollectorResult::Incomplete(Ok(Self::Collecting { + total_count: size, + current_count: 0, + items: vec![], + })), + SizeResult::Incomplete(Ok(collector)) => StringCollectorResult::Incomplete(Ok(Self::Sizing(collector))), + SizeResult::Incomplete(Err(e)) => StringCollectorResult::Incomplete(Err(e)), + } + } + Self::Collecting { + current_count, + total_count, + mut items, + } => { + if current_count < total_count { + items.push(token); + } + + if current_count == total_count + 1 { + return StringCollectorResult::Finished(items); + } + + StringCollectorResult::Incomplete(Ok(Self::Collecting { + current_count: current_count + 1, + total_count, + items, + })) + } + } + } +} + +#[derive(Debug, PartialEq)] +pub enum ArrayMessage { + Sizing(SizeCollector), + + Collecting { + /// The length of the array. + total_count: i32, + + current_count: i32, + + head: StringCollector, + + /// Our list of accumulated items. + items: Vec>, + }, +} + +impl Default for ArrayMessage { + fn default() -> Self { + Self::Sizing(SizeCollector::default()) + } +} + +pub enum ArrayCollectionResult { + Finished(Vec>), + Incomplete(io::Result), +} + +impl ArrayMessage { + pub fn take(self, token: u8) -> ArrayCollectionResult { + match self { + Self::Sizing(collector) => match collector.take(token) { + SizeResult::Complete(size) => { + if size < 1 { + return ArrayCollectionResult::Finished(vec![]); + } + ArrayCollectionResult::Incomplete(Ok(Self::Collecting { + total_count: size, + current_count: 0, + head: StringCollector::Sizing(SizeCollector::default()), + items: vec![], + })) + } + SizeResult::Incomplete(Ok(collector)) => ArrayCollectionResult::Incomplete(Ok(Self::Sizing(collector))), + SizeResult::Incomplete(Err(e)) => ArrayCollectionResult::Incomplete(Err(e)), + }, + Self::Collecting { + total_count, + current_count, + mut items, + head, + } => match head.take(token) { + StringCollectorResult::Finished(buffer) => { + items.push(buffer); + + if current_count + 1 == total_count { + return ArrayCollectionResult::Finished(items); + } + + ArrayCollectionResult::Incomplete(Ok(Self::Collecting { + total_count, + current_count: current_count + 1, + items, + head: StringCollector::Sizing(SizeCollector::default()), + })) + } + StringCollectorResult::Incomplete(Ok(next)) => ArrayCollectionResult::Incomplete(Ok(Self::Collecting { + total_count, + current_count, + items, + head: next, + })), + StringCollectorResult::Incomplete(Err(next)) => ArrayCollectionResult::Incomplete(Err(next)), + }, + } + } +} + +#[derive(Debug, Default)] +pub enum MessageState { + #[default] + Initial, + + Array(ArrayMessage), + + String(StringCollector), + + Int(SizeCollector), + + Complete(RedisResponse), + + Failed(io::Error), +} + +impl MessageState { + pub fn take(self, token: u8) -> Self { + match (self, token) { + (Self::Initial, b'*') => Self::Array(ArrayMessage::default()), + (Self::Initial, b':') => Self::Int(SizeCollector::default()), + (Self::Initial, b'-') => Self::Initial, + (Self::Initial, b'$') => Self::String(StringCollector::Sizing(SizeCollector::default())), + + (Self::Complete(response), _) => Self::Complete(response), + + (Self::Int(collector), other) => match collector.take(other) { + SizeResult::Incomplete(Ok(ar)) => Self::Int(ar), + SizeResult::Incomplete(Err(e)) => Self::Failed(e), + SizeResult::Complete(amount) => Self::Complete(RedisResponse::Int(amount)), + }, + + (Self::String(string), other) => match string.take(other) { + StringCollectorResult::Incomplete(Ok(ar)) => Self::String(ar), + StringCollectorResult::Incomplete(Err(e)) => Self::Failed(e), + StringCollectorResult::Finished(buffer) => Self::Complete(RedisResponse::String(buffer)), + }, + + (Self::Array(array), other) => match array.take(other) { + ArrayCollectionResult::Incomplete(Ok(ar)) => Self::Array(ar), + ArrayCollectionResult::Incomplete(Err(e)) => Self::Failed(e), + ArrayCollectionResult::Finished(buffers) => Self::Complete(RedisResponse::Array(buffers)), + }, + + (Self::Initial, _) => Self::Failed(io::Error::new(io::ErrorKind::Other, "invalid start")), + (Self::Failed(e), _) => Self::Failed(e), + } + } +} + +#[derive(Debug, PartialEq)] +pub enum RedisResponse { + Empty, + Failed(String), + Array(Vec>), + String(Vec), + Int(i32), +} + +impl FromIterator for RedisResponse { + fn from_iter(i: I) -> Self + where + I: IntoIterator, + { + let mut state = MessageState::default(); + + for tok in i.into_iter() { + state = state.take(tok); + + if let MessageState::Complete(response) = state { + return response; + } + + if let MessageState::Failed(error) = state { + return RedisResponse::Failed(error.to_string()); + } + } + + Self::Empty + } +} + +#[cfg(test)] +mod tests { + use super::RedisResponse; + + #[test] + fn test_array_empty() { + let input = "*-1\r\n"; + let result = input.as_bytes().iter().copied().collect::(); + assert_eq!(result, RedisResponse::Array(vec![])); + } + + #[test] + fn test_int() { + let input = ":123\r\n"; + let result = input.as_bytes().iter().copied().collect::(); + assert_eq!(result, RedisResponse::Int(123)); + } + + #[test] + fn test_bulk_str() { + let input = "$2\r\nhi\r\n"; + let result = input.as_bytes().iter().copied().collect::(); + assert_eq!( + result, + RedisResponse::String("hi".as_bytes().iter().copied().collect::>()) + ) + } + + #[test] + fn test_array_message_many() { + let mut many = "*10\r\n".to_string(); + for _ in 0..10 { + many.push_str("$2\r\nhi\r\n"); + } + let result = many.as_bytes().iter().copied().collect::(); + assert_eq!( + result, + RedisResponse::Array(vec![ + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + b"hi".to_vec(), + ]) + ) + } + + #[test] + fn test_array_message_single() { + let response = "*1\r\n$2\r\nhi\r\n" + .as_bytes() + .iter() + .copied() + .collect::(); + assert_eq!(response, RedisResponse::Array(vec![b"hi".to_vec()])); + } +} diff --git a/tools/beetle-pio-image-tester/fixtures/dog.png b/tools/beetle-pio-image-tester/fixtures/dog.png new file mode 100644 index 0000000..850afb4 Binary files /dev/null and b/tools/beetle-pio-image-tester/fixtures/dog.png differ diff --git a/tools/beetle-pio-image-tester/fixtures/gray-tester.png b/tools/beetle-pio-image-tester/fixtures/gray-tester.png new file mode 100644 index 0000000..7a4b971 Binary files /dev/null and b/tools/beetle-pio-image-tester/fixtures/gray-tester.png differ diff --git a/tools/beetle-pio-image-tester/fixtures/square.png b/tools/beetle-pio-image-tester/fixtures/square.png new file mode 100644 index 0000000..fc6f81a Binary files /dev/null and b/tools/beetle-pio-image-tester/fixtures/square.png differ diff --git a/tools/beetle-pio-image-tester/src/main.cpp b/tools/beetle-pio-image-tester/src/main.cpp index 0fef483..64cf089 100644 --- a/tools/beetle-pio-image-tester/src/main.cpp +++ b/tools/beetle-pio-image-tester/src/main.cpp @@ -40,12 +40,16 @@ void draw_row(PNGDRAW *draw_context) { float l = lum(r, g, b); + if (i == 0) { + log_i("row=%d, l=%f", draw_context->y, l); + } + uint16_t color = GxEPD_WHITE; - if (l < lum(0x7b, 0x7d, 0x7b)) { + if (l < 64) { color = GxEPD_BLACK; - } else if (l < lum(0xc5, 0xc2, 0xc5)) { + } else if (l < 160) { color = GxEPD_DARKGREY; - } else if (l < lum(0xaa, 0xaa, 0xaa)) { + } else if (l < 223) { color = GxEPD_LIGHTGREY; } @@ -94,9 +98,8 @@ void setup(void) { } while (display.nextPage()); auto rc = - // png.openRAM((uint8_t *)square_start, square_end - square_start, - // draw_row); - png.openRAM((uint8_t *)dog_start, dog_end - dog_start, draw_row); + png.openRAM((uint8_t *)square_start, square_end - square_start, draw_row); + // png.openRAM((uint8_t *)dog_start, dog_end - dog_start, draw_row); if (rc == PNG_SUCCESS) { auto width = png.getWidth(), height = png.getHeight(), bpp = png.getBpp(); log_i("image specs: (%d x %d) | %d bpp | alpha? %d | type %d", width,