cuttle/src/main.rs
Joe Ardent 7ecb07eec3 Displays either a static qr code or a stream of raptor-encoded ones.
Kinda needs a client now to really test.
2023-08-03 13:40:10 -07:00

145 lines
4.1 KiB
Rust

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<OsString>,
#[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<String>,
}
impl Cli {
pub fn text(&self) -> &Vec<String> {
&self.text
}
}
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let cli = Cli::parse();
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(" ")
};
stream_file(file, tx, cli.rate);
Flasher {
heading,
content,
rate: cli.rate,
}
} else {
let heading = "text message".to_string();
let bytes: Vec<u8> = 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(
"Cuttle: optical insanity",
options,
Box::new(move |_cc| Box::new(flasher)),
)
}
struct Flasher {
pub heading: String,
pub content: Content,
pub rate: u64,
}
enum Content {
Static(&'static [u8]),
Streaming(std::sync::mpsc::Receiver<Vec<u8>>),
}
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| {
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<Vec<u8>>, 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::<Vec<_>>();
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));
}
});
}