From 89c3f748dd9946c072ccbf388a070e17ee133db4 Mon Sep 17 00:00:00 2001 From: Danny Hadley Date: Wed, 1 Nov 2023 14:26:37 -0400 Subject: [PATCH] more image stuff --- .gitignore | 2 +- .../lib/redis-events/redis-events.hpp | 14 +- src/beetle-pio/src/xiao-rendering.hpp | 6 +- src/beetle-srv/src/api/jobs.rs | 107 +++--- src/beetle-srv/src/rendering/mod.rs | 6 +- src/beetle-srv/src/rendering/renderer.rs | 2 +- src/beetle-ui/src/Route/Device.elm | 2 +- tools/beetle-mock/src/arguments.rs | 11 + tools/beetle-mock/src/id.rs | 98 ++++++ tools/beetle-mock/src/main.rs | 318 ++--------------- tools/beetle-mock/src/redis_reader.rs | 331 ++++++++++++++++++ .../beetle-pio-image-tester/fixtures/dog.png | Bin 0 -> 54329 bytes .../fixtures/gray-tester.png | Bin 0 -> 6882 bytes .../fixtures/square.png | Bin 0 -> 236 bytes tools/beetle-pio-image-tester/src/main.cpp | 15 +- 15 files changed, 562 insertions(+), 350 deletions(-) create mode 100644 tools/beetle-mock/src/arguments.rs create mode 100644 tools/beetle-mock/src/id.rs create mode 100644 tools/beetle-mock/src/redis_reader.rs create mode 100644 tools/beetle-pio-image-tester/fixtures/dog.png create mode 100644 tools/beetle-pio-image-tester/fixtures/gray-tester.png create mode 100644 tools/beetle-pio-image-tester/fixtures/square.png 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 0000000000000000000000000000000000000000..850afb43c697e33dc8e73d721a87ef3ca07e3dd0 GIT binary patch literal 54329 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 O0000zYT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7a4b971a209ed219cf9c0a62558a150de32b4dc5 GIT binary patch literal 6882 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fc6f81a5dc30f4b67264a4fdc84ecaf6c7d37ce8 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}%RF5iLo%G- z&OFF@z(C;eJG1i{H+AkFRM2qydt2b-1gEmJdm=Tj9qZBpmF0LHJngMR1?MmSEUIvFp1Ah%Rvs1o(kM%7 iwUvTx+4rK7pD_PuEqgpuE}{|WVg^rFKbLh*2~7Y@G+Al@ literal 0 HcmV?d00001 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,