feat: add periodic image saving
This commit is contained in:
parent
6da19152ca
commit
0502f3dacb
6 changed files with 1085 additions and 6 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
.direnv/
|
.direnv/
|
||||||
|
recordings/
|
||||||
|
|
|
||||||
1030
Cargo.lock
generated
1030
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,8 @@ edition = "2021"
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
atoi_radix10 = "0.0.1"
|
atoi_radix10 = "0.0.1"
|
||||||
bytes = "1.6.0"
|
bytes = "1.6.0"
|
||||||
|
chrono = "0.4.38"
|
||||||
|
image = "0.25.2"
|
||||||
tokio = { version = "1.38", features = ["full"] }
|
tokio = { version = "1.38", features = ["full"] }
|
||||||
tokio-test = "*"
|
tokio-test = "*"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,8 @@ To set a pixel using RGB, use ({SET_PX_RGB_BIN:02X}) (u8 canvas) (x as u16_le) (
|
||||||
writer.write_all(help_text.as_bytes()).await
|
writer.write_all(help_text.as_bytes()).await
|
||||||
}
|
}
|
||||||
Response::Size(x, y) => {
|
Response::Size(x, y) => {
|
||||||
writer.write_u16_le(x).await?;
|
writer.write_u16(x).await?;
|
||||||
writer.write_u16_le(y).await
|
writer.write_u16(y).await
|
||||||
}
|
}
|
||||||
Response::GetPixel(_, _, c) => {
|
Response::GetPixel(_, _, c) => {
|
||||||
writer.write_u8(c[0]).await?;
|
writer.write_u8(c[0]).await?;
|
||||||
|
|
|
||||||
17
src/grid.rs
17
src/grid.rs
|
|
@ -1,5 +1,7 @@
|
||||||
use std::cell::SyncUnsafeCell;
|
use std::cell::SyncUnsafeCell;
|
||||||
|
|
||||||
|
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Pixel, Rgb, Rgba};
|
||||||
|
|
||||||
use crate::Coordinate;
|
use crate::Coordinate;
|
||||||
|
|
||||||
pub trait Grid<I, V> {
|
pub trait Grid<I, V> {
|
||||||
|
|
@ -63,6 +65,21 @@ impl<T> Grid<Coordinate, T> for Flut<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GenericImageView for Flut<u32> {
|
||||||
|
type Pixel = Rgb<u8>;
|
||||||
|
|
||||||
|
fn dimensions(&self) -> (u32, u32) {
|
||||||
|
let (x, y) = self.get_size();
|
||||||
|
(x as u32, y as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel {
|
||||||
|
let pixel = self.get_unchecked(x as u16, y as u16);
|
||||||
|
let [r, g, b, _a] = pixel.to_be_bytes();
|
||||||
|
Rgb::from([r, g, b])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::needless_return)]
|
#[allow(clippy::needless_return)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
37
src/main.rs
37
src/main.rs
|
|
@ -7,17 +7,24 @@ mod grid;
|
||||||
mod text_protocol;
|
mod text_protocol;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
alloc::System,
|
||||||
|
fmt::Debug,
|
||||||
|
fs::{create_dir_all, File},
|
||||||
io::{self, Error, ErrorKind},
|
io::{self, Error, ErrorKind},
|
||||||
|
path::Path,
|
||||||
sync::{atomic::AtomicU64, Arc},
|
sync::{atomic::AtomicU64, Arc},
|
||||||
time::Duration,
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use binary_protocol::BinaryParser;
|
use binary_protocol::BinaryParser;
|
||||||
|
use chrono::Local;
|
||||||
use grid::{Flut, Grid};
|
use grid::{Flut, Grid};
|
||||||
|
use image::{codecs::jpeg::JpegEncoder, save_buffer, DynamicImage, GenericImageView, SubImage};
|
||||||
use text_protocol::TextParser;
|
use text_protocol::TextParser;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
|
time::{interval, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
@ -261,6 +268,30 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn save_image_frames(grids: Arc<[grid::Flut<u32>]>) -> io::Result<()> {
|
||||||
|
let base_dir = Path::new("./recordings");
|
||||||
|
let mut timer = interval(Duration::from_secs(5));
|
||||||
|
create_dir_all(base_dir)?;
|
||||||
|
loop {
|
||||||
|
timer.tick().await;
|
||||||
|
for grid in grids.as_ref() {
|
||||||
|
let p = base_dir.join(format!("{}", Local::now().format("%Y-%m-%d %H:%M:%S")));
|
||||||
|
println!("timer ticked, grid writing to {:?}", p);
|
||||||
|
let mut file_writer = File::create(p)?;
|
||||||
|
|
||||||
|
let encoder = JpegEncoder::new_with_quality(&mut file_writer, 50);
|
||||||
|
grid.view(0, 0, grid.width(), grid.height()).to_image();
|
||||||
|
|
||||||
|
let sub_image = SubImage::new(grid, 0, 0, grid.width(), grid.height());
|
||||||
|
let image = sub_image.to_image();
|
||||||
|
match image.write_with_encoder(encoder) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => eprintln!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_flut(flut_listener: TcpListener, grids: Arc<[grid::Flut<u32>]>) -> io::Result<()> {
|
async fn handle_flut(flut_listener: TcpListener, grids: Arc<[grid::Flut<u32>]>) -> io::Result<()> {
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -293,8 +324,10 @@ async fn main() {
|
||||||
let handles = vec![
|
let handles = vec![
|
||||||
// log the amount of changed pixels each second
|
// log the amount of changed pixels each second
|
||||||
(tokio::spawn(listen_handle())),
|
(tokio::spawn(listen_handle())),
|
||||||
|
// save frames every 5 seconds
|
||||||
|
(tokio::spawn(save_image_frames(grids.clone()))),
|
||||||
// accept and handle flut connections
|
// accept and handle flut connections
|
||||||
(tokio::spawn(handle_flut(flut_listener, grids))),
|
(tokio::spawn(handle_flut(flut_listener, grids.clone()))),
|
||||||
];
|
];
|
||||||
|
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue