145 lines
4.1 KiB
Rust
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));
|
|
}
|
|
});
|
|
}
|