diff --git a/Cargo.toml b/Cargo.toml index 87e675c..0b54e54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,15 +3,17 @@ name = "cuttle" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["desktop"] +desktop = ["dep:eframe", "dep:egui_extras"] [dependencies] clap = { version = "4.3.19", features = ["derive", "env"] } -eframe = { version = "0.22", default-features = false, features = ["default_fonts", "wgpu", "tts", "accesskit"] } -egui_extras = { version = "0.22", default-features = false, features = ["chrono", "image"] } +eframe = { version = "0.22", default-features = false, optional = true, features = ["default_fonts", "wgpu", "tts", "accesskit"] } +egui_extras = { version = "0.22", default-features = false, optional = true, features = ["chrono", "image"] } env_logger = "*" fast_qr = { version = "0.9", default-features = false, features = ["image"] } png = "0.17.6" # pinning this to resolve conflict between eframe and fast_qr with the image crate -rand = { version = "0.8", default-features = false, features = ["std"] } +rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } raptorq = "1.7" rkyv = { version = "0.7.42", features = ["validation"] } diff --git a/src/lib.rs b/src/lib.rs index f04733d..6a1e590 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,79 +1,105 @@ -use std::{fmt::Debug, time::Duration}; +use std::{ + fmt::Debug, + time::{Duration, Instant}, +}; +#[cfg(feature = "desktop")] use eframe::egui; +#[cfg(feature = "desktop")] use egui_extras::RetainedImage; use fast_qr::convert::image::ImageBuilder; use rand::seq::SliceRandom; use raptorq::{Encoder, ObjectTransmissionInformation}; use rkyv::{Archive, Deserialize, Serialize}; -pub const STREAMING_MTU: u16 = 776; +pub const STREAMING_MTU: u16 = 2326; -pub type Receiver = std::sync::mpsc::Receiver>; -pub type Sender = std::sync::mpsc::SyncSender>; +pub type Receiver = std::sync::mpsc::Receiver<(usize, Vec)>; +pub type Sender = std::sync::mpsc::SyncSender<(usize, Vec)>; + +/// The application state +#[derive(Debug)] +pub struct Flasher { + pub heading: String, + pub content: Content, + pub sleep: Duration, + pub last: Instant, + pub idx: usize, +} + +#[derive(Debug)] +pub enum Content { + Static(&'static [u8]), + Streaming(Receiver), +} #[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[repr(C)] pub struct TxConfig { pub len: u64, pub mtu: u16, pub description: String, } -pub fn mk_img(bytes: &[u8]) -> RetainedImage { - let qr = fast_qr::QRBuilder::new(bytes).build().unwrap(); - let bytes = ImageBuilder::default() - .fit_width(1100) - .to_pixmap(&qr) - .encode_png() - .unwrap(); - - RetainedImage::from_image_bytes("generated qr code", &bytes).unwrap() -} - -pub fn stream_bytes(bytes: Vec, tx: Sender, rate: u64) { - let sleep = (1000.0 / rate as f64) as u64; - +pub fn stream_bytes(bytes: Vec, tx: Sender, desc: String) { std::thread::spawn(move || { let rng = &mut rand::thread_rng(); - let config = - ObjectTransmissionInformation::with_defaults(bytes.len() as u64, STREAMING_MTU); + + let len = bytes.len() as u64; + let txconfig = TxConfig { + len, + mtu: STREAMING_MTU, + description: desc.to_string(), + }; + let txconfig = + rkyv::to_bytes::<_, 256>(&txconfig).expect("tried to serialize the txconfig"); + + let config = ObjectTransmissionInformation::with_defaults(len, STREAMING_MTU); let encoder = Encoder::new(&bytes, config); let mut packets = encoder .get_encoded_packets(10) .iter() .map(|p| p.serialize()) .collect::>(); + + let txcfgs = vec![txconfig.to_vec(); packets.len() / 10]; + packets.extend(txcfgs); + packets.shuffle(rng); - for (_counter, packet) in packets.iter().cycle().enumerate() { - tx.send(packet.clone()).unwrap(); - std::thread::sleep(Duration::from_millis(sleep)); + for (counter, packet) in packets.iter().cycle().enumerate() { + tx.send((counter, packet.clone())).unwrap(); } }); } -pub struct Flasher { - pub heading: String, - pub content: Content, - pub rate: u64, -} - -pub enum Content { - Static(&'static [u8]), - Streaming(Receiver), +pub fn mk_img(bytes: &[u8]) -> Vec { + let qr = fast_qr::QRBuilder::new(bytes) + .ecl(fast_qr::ECL::M) + .build() + .unwrap(); + + ImageBuilder::default() + .fit_width(1100) + .to_pixmap(&qr) + .encode_png() + .unwrap() } +#[cfg(feature = "desktop")] impl eframe::App for Flasher { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - let sleep = 1000.0 / self.rate as f64; - ctx.request_repaint_after(Duration::from_millis(sleep as u64)); + ctx.request_repaint_after(self.sleep); egui::CentralPanel::default().show(ctx, |ui| { ui.heading(&self.heading); let img = match self.content { - Content::Static(bytes) => mk_img(bytes), + Content::Static(bytes) => { + RetainedImage::from_image_bytes("generated qr code", &mk_img(bytes)).unwrap() + } Content::Streaming(ref rx) => { - if let Ok(bytes) = rx.try_recv() { - mk_img(&bytes) + if let Ok((_counter, ref bytes)) = rx.try_recv() { + RetainedImage::from_image_bytes("generated qr code", &mk_img(bytes)) + .unwrap() } else { return; } diff --git a/src/main.rs b/src/main.rs index 0e8465f..9d22780 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ -use std::ffi::OsString; +use std::{ + ffi::OsString, + time::{Duration, Instant}, +}; use clap::Parser; use cuttle::{stream_bytes, Content, Flasher}; @@ -10,8 +13,9 @@ struct Cli { #[clap(long, short, help = "File to expose")] pub file: Option, - #[clap(long, short, help = "Frames per second", default_value_t = 10)] - pub rate: u64, + #[clap(long, help = "Frames per second", default_value_t = 10)] + pub fps: u64, + #[clap( help = "all remaining arguments treated as a string; this string is the whole message if `-f` is not given, otherwise it's an optional description of the file" )] @@ -41,12 +45,17 @@ fn main() -> Result<(), eframe::Error> { "text message".to_string() }; - let content: Content = get_content(&cli); + let content: Content = get_content(&cli, &description); + let sleep = 1000.0 / cli.fps as f64; + let sleep = Duration::from_millis(sleep as u64); + let last = Instant::now(); let flasher = Flasher { heading: description, content, - rate: cli.rate, + sleep, + last, + idx: 0, }; let options = eframe::NativeOptions { @@ -63,7 +72,7 @@ fn main() -> Result<(), eframe::Error> { ) } -fn get_content(cli: &Cli) -> Content { +fn get_content(cli: &Cli, desc: &str) -> Content { let bytes = if let Some(ref file) = cli.file { std::fs::read(file).unwrap_or_else(|e| panic!("tried to open {file:?}, got {e:?}")) } else { @@ -75,7 +84,7 @@ fn get_content(cli: &Cli) -> Content { Content::Static(bytes) } else { let (tx, rx) = std::sync::mpsc::sync_channel(2); - stream_bytes(bytes, tx, cli.rate); + stream_bytes(bytes, tx, desc.to_string()); Content::Streaming(rx) } }