From 7ecb07eec3ef9f4f8f00695bb38ee00158b05341 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Thu, 3 Aug 2023 13:40:10 -0700 Subject: [PATCH] Displays either a static qr code or a stream of raptor-encoded ones. Kinda needs a client now to really test. --- Cargo.lock | 29 +++++++------ Cargo.toml | 6 +-- README.md | 37 ++++++++-------- src/main.rs | 121 ++++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 136 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd5bbd5..65b4c95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,6 +901,21 @@ dependencies = [ "typenum", ] +[[package]] +name = "cuttle" +version = "0.1.0" +dependencies = [ + "clap", + "eframe", + "egui_extras", + "env_logger", + "fast_qr", + "png", + "rand", + "raptorq", + "rkyv", +] + [[package]] name = "d3d12" version = "0.6.0" @@ -1443,20 +1458,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "godiva" -version = "0.1.0" -dependencies = [ - "clap", - "eframe", - "egui_extras", - "env_logger", - "fast_qr", - "png", - "raptorq", - "rkyv", -] - [[package]] name = "gpu-alloc" version = "0.5.4" diff --git a/Cargo.toml b/Cargo.toml index 01cd608..87e675c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "godiva" +name = "cuttle" version = "0.1.0" edition = "2021" @@ -11,7 +11,7 @@ eframe = { version = "0.22", default-features = false, features = ["default_font egui_extras = { version = "0.22", default-features = false, 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"] } raptorq = "1.7" -# pinning this to resolve conflict between eframe and fast_qr with the image crate -png = "0.17.6" rkyv = { version = "0.7.42", features = ["validation"] } diff --git a/README.md b/README.md index 55fdd45..65bdba0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Godiva, a flasher with a message +# Cuttle, a flasher with a message -Godiva is a desktop application for transfering data from one computer to another by displaying a [possibly -endless] stream of [QR encoded](https://en.wikipedia.org/wiki/QR_code) [Raptor -codes](https://en.wikipedia.org/wiki/Raptor_code), and then on the receiving side, a camera to read -the QR codes and turn them back into the original content. +Cuttle is a desktop application for transfering data from one computer to another by displaying a +[possibly endless] stream of [QR encoded](https://en.wikipedia.org/wiki/QR_code) [Raptor +codes](https://en.wikipedia.org/wiki/Raptor_code), so that a camera-equipped different computer can +read the QR codes and turn them back into the original content. -## what? +## Tell me more! Raptor codes are a type of [fountain code](https://en.wikipedia.org/wiki/Fountain_code); fountain codes are a way to encode data as a stream of error-correcting pieces of that data; as long as you @@ -16,21 +16,24 @@ and not care if anyone is receiving, and receivers don't have to worry about mis needed; they're guaranteed to get everything they need as long as they listen long enough. I think that's pretty cool! -So the idea here is to give it a file (or short string of text) you want transferred to a different -machine (probably a mobile phone running a little app I'll be writing), and it will start producing -raptor codes for that file. Each piece of raptor code then gets encoded as a QR code, and then +So the idea here is to give it a file (or short string of text) that you want transferred to a +different computer (like a mobile phone running a little companion app), and it will start producing +Raptor codes for that file. Each piece of Raptor code then gets encoded as a QR code, and then displayed for a period of time on the screen. The user then holds up the receiving computer's camera in order to receive the QR encoded raptor codes, and, *voila*, the file has been transferred! -# status +# Current status -I literally just started this today, and currently it will produce a single QR code for a given -input string, and display that. Honestly, this is already genuinely useful to me; sometimes I want -to look at a url on my phone, and the camera app will let me look at a QR code and interpret text as -a link. But I hope to have a quick and dirty mobile app soon that will let me test streaming codes. +The desktop transmitting application is nearly done; already it can convert text input into QR codes +(either a single static one if the data is small enough, otherwise it will stream a never ending +loop of them), and the mobile app has been started. Even without the mobile app, it's still +already useful for transmitting short text strings to the phone, since the Android camera app will +decode any detected QR codes. + +The mobile app has been started, and I hope to have it decoding QR streams soon! # about the name -[Lady Godiva](https://en.wikipedia.org/wiki/Lady_Godiva), in legend, rode naked through her town in -order to send a message to her husband about the plight of the downtrodden under his -landlordship. +[Cuttlefish](https://en.wikipedia.org/wiki/Cuttlefish) are small but very intelligent cephalopods +whose bodies are covered in a 15hz multi-color, multi-polarity, pixel-mapped display that they use +to visually communicate with other cuttlefish, as well as to dazzle predators and prey. diff --git a/src/main.rs b/src/main.rs index 8040240..79f341f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,25 @@ -use std::ffi::OsString; +use std::{ffi::OsString, time::Duration}; use clap::Parser; use eframe::egui; use egui_extras::RetainedImage; use fast_qr::convert::image::ImageBuilder; +use rand::seq::SliceRandom; +use raptorq::{Encoder, ObjectTransmissionInformation}; + +pub const MTU: u16 = 776; #[derive(Parser, Debug)] #[clap(author, version, trailing_var_arg = true)] struct Cli { #[clap(long, short, help = "File to expose")] pub file: Option, - #[clap(help = "all remaining arguments treated as a string; ignored if `-f` is given")] + + #[clap(long, short, help = "Frames per second", default_value_t = 10)] + pub rate: 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" + )] text: Vec, } @@ -25,46 +34,112 @@ fn main() -> Result<(), eframe::Error> { let cli = Cli::parse(); - let content = if let Some(file) = cli.file { - file.to_string_lossy().bytes().collect() - } else { - cli.text().join(" ").bytes().collect() - }; + let flasher = if let Some(ref file) = cli.file { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let content = Content::Streaming(rx); + let heading = if cli.text.is_empty() { + file.to_string_lossy().to_string() + } else { + cli.text().join(" ") + }; - //println!("content: {content}"); + stream_file(file, tx, cli.rate); + + Flasher { + heading, + content, + rate: cli.rate, + } + } else { + let heading = "text message".to_string(); + let bytes: Vec = cli.text().join(" ").bytes().collect(); + let bytes = bytes.leak(); + let content = Content::Static(bytes); + + Flasher { + heading, + content, + rate: cli.rate, + } + }; let options = eframe::NativeOptions { initial_window_size: Some(egui::vec2(1200.0, 1200.0)), + active: true, + vsync: true, ..Default::default() }; eframe::run_native( - "Show an image with eframe/egui", + "Cuttle: optical insanity", options, - Box::new(|_cc| Box::new(Flasher { content })), + Box::new(move |_cc| Box::new(flasher)), ) } struct Flasher { - pub content: Vec, + pub heading: String, + pub content: Content, + pub rate: u64, +} + +enum Content { + Static(&'static [u8]), + Streaming(std::sync::mpsc::Receiver>), } 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)); egui::CentralPanel::default().show(ctx, |ui| { - let heading_text = String::from_utf8(self.content.clone()).unwrap(); - ui.heading(heading_text); - let qr = fast_qr::QRBuilder::new(self.content.clone()) - .build() - .unwrap(); - let bytes = ImageBuilder::default() - .fit_width(1100) - .to_pixmap(&qr) - .encode_png() - .unwrap(); - - let img = RetainedImage::from_image_bytes("generated qr code", &bytes).unwrap(); + ui.heading(&self.heading); + let img = match self.content { + Content::Static(bytes) => mk_img(bytes), + Content::Streaming(ref rx) => { + if let Ok(bytes) = rx.try_recv() { + mk_img(&bytes) + } else { + return; + } + } + }; img.show(ui); }); } } + +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() +} + +fn stream_file(file: &OsString, tx: std::sync::mpsc::SyncSender>, rate: u64) { + let bytes = std::fs::read(file).unwrap_or_else(|e| panic!("tried to open {file:?}, got {e:?}")); + + let sleep = (1000.0 / rate as f64) as u64; + + std::thread::spawn(move || { + let rng = &mut rand::thread_rng(); + let config = ObjectTransmissionInformation::with_defaults(bytes.len() as u64, MTU); + let encoder = Encoder::new(&bytes, config); + let mut packets = encoder + .get_encoded_packets(10) + .iter() + .map(|p| p.serialize()) + .collect::>(); + packets.shuffle(rng); + dbg!(&packets.len()); + + for (_counter, packet) in packets.iter().cycle().enumerate() { + tx.send(packet.clone()).unwrap(); + std::thread::sleep(Duration::from_millis(sleep)); + } + }); +}