From acdf8945582b6d0a277ce79a25cd6a6c8da881dd Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 10 May 2025 13:15:49 -0400 Subject: [PATCH 1/5] keyfork-qrcode: add threaded handler --- crates/qrcode/keyfork-qrcode/src/lib.rs | 148 ++++++++++++++++++------ 1 file changed, 112 insertions(+), 36 deletions(-) diff --git a/crates/qrcode/keyfork-qrcode/src/lib.rs b/crates/qrcode/keyfork-qrcode/src/lib.rs index 08aa972..2599019 100644 --- a/crates/qrcode/keyfork-qrcode/src/lib.rs +++ b/crates/qrcode/keyfork-qrcode/src/lib.rs @@ -1,11 +1,15 @@ //! Encoding and decoding QR codes. +#![allow(clippy::expect_fun_call)] + use keyfork_bug as bug; +use bug::POISONED_MUTEX; use image::{ImageBuffer, ImageReader, Luma}; use std::{ io::{Cursor, Write}, process::{Command, Stdio}, + sync::{mpsc::channel, Arc, Condvar, Mutex}, time::{Duration, Instant}, }; use v4l::{ @@ -15,6 +19,8 @@ use v4l::{ Device, FourCC, }; +type Image = ImageBuffer, Vec>; + /// A QR code could not be generated. #[derive(thiserror::Error, Debug)] pub enum QRGenerationError { @@ -102,12 +108,12 @@ pub fn qrencode( const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format"; trait Scanner { - fn scan_image(&mut self, image: ImageBuffer, Vec>) -> Option; + fn scan_image(&mut self, image: Image) -> Option; } #[cfg(feature = "decode-backend-zbar")] mod zbar { - use super::{ImageBuffer, Luma, Scanner}; + use super::{Image, Scanner}; pub struct Zbar { scanner: keyfork_zbar::image_scanner::ImageScanner, @@ -129,29 +135,25 @@ mod zbar { } impl Scanner for Zbar { - fn scan_image( - &mut self, - image: ImageBuffer, Vec>, - ) -> Option { + fn scan_image(&mut self, image: Image) -> Option { let image = keyfork_zbar::image::Image::from(image); - self.scanner.scan_image(&image).into_iter().next().map(|symbol| { - String::from_utf8_lossy(symbol.data()).into() - }) + self.scanner + .scan_image(&image) + .into_iter() + .next() + .map(|symbol| String::from_utf8_lossy(symbol.data()).into()) } } } #[cfg(feature = "decode-backend-rqrr")] mod rqrr { - use super::{ImageBuffer, Luma, Scanner}; + use super::{Image, Scanner}; pub struct Rqrr; impl Scanner for Rqrr { - fn scan_image( - &mut self, - image: ImageBuffer, Vec>, - ) -> Option { + fn scan_image(&mut self, image: Image) -> Option { let mut image = rqrr::PreparedImage::prepare(image); for grid in image.detect_grids() { if let Ok((_, content)) = grid.decode() { @@ -170,12 +172,23 @@ fn dbg_elapsed(count: u64, instant: Instant) { eprintln!("framerate: {count}/{elapsed} = {framerate}"); } +#[derive(Debug)] +struct ScanQueue { + shutdown: bool, + images: Vec, +} + /// Continuously scan the `index`-th camera for a QR code. /// /// # Errors /// /// The function may return an error if the hardware is unable to scan video or if an image could /// not be decoded. +/// +/// # Panics +/// +/// The function may panic if a mutex is poisoned by a thread panicking, which should +/// only happen during a mutex, or if it can't send a message over the mpsc channel. pub fn scan_camera(timeout: Duration, index: usize) -> Result, QRCodeScanError> { let device = Device::new(index)?; let mut fmt = device @@ -185,33 +198,96 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR device.set_format(&fmt)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let start = Instant::now(); - cfg_if::cfg_if! { - if #[cfg(feature = "decode-backend-zbar")] { - let mut scanner = zbar::Zbar::default(); - } else if #[cfg(feature = "decode-backend-rqrr")] { - let mut scanner = rqrr::Rqrr; - } else { - unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected") - } - }; #[allow(unused)] let mut count = 0; - while Instant::now().duration_since(start) < timeout { - count += 1; - let (buffer, _) = stream.next()?; - let image = ImageReader::new(Cursor::new(buffer)) - .with_guessed_format()? - .decode()? - .to_luma8(); - if let Some(content) = scanner.scan_image(image) { - // dbg_elapsed(count, start); - return Ok(Some(content)); + let thread_count = 4; + + std::thread::scope(|scope| { + let scan_queue = ScanQueue { + shutdown: false, + images: vec![], + }; + + let arced = Arc::new((Mutex::new(scan_queue), Condvar::new())); + let (tx, rx) = channel(); + + for _ in 0..thread_count { + let tx = tx.clone(); + let arced = arced.clone(); + + scope.spawn(move || { + cfg_if::cfg_if! { + if #[cfg(feature = "decode-backend-zbar")] { + let mut scanner = zbar::Zbar::default(); + } else if #[cfg(feature = "decode-backend-rqrr")] { + let mut scanner = rqrr::Rqrr; + } else { + unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected") + } + }; + + let (queue_mutex, condvar) = &*arced; + loop { + // NOTE: Carrying the `queue` variable through the loop, so we can + // pass it through without re-locking, means that we don't drop the + // lock on the mutex. Therefore, we unlock, then immediately + // re-lock when we pass the value to wait_while(). + // + // By holding onto the queue until we pass it back to the Condvar, + // and checking shutdown, we ensure that there's no way we miss the + // shutdown being set before we release the guard on the queue. + let queue = queue_mutex.lock().expect(bug::bug!(POISONED_MUTEX)); + if queue.shutdown { + break; + } + let mut queue = condvar + .wait_while(queue, |queue| { + queue.images.is_empty() || !queue.shutdown + }) + .expect(bug::bug!(POISONED_MUTEX)); + if let Some(image) = queue.images.pop() { + // drop the queue here since this is what's going to take time + drop(queue); + if let Some(content) = scanner.scan_image(image) { + tx.send(content).expect( + bug::bug!("A scanned image could not be sent") + ); + } + } + } + }); } - } - // dbg_elapsed(count, start); + while Instant::now().duration_since(start) < timeout { + if let Ok(content) = rx.try_recv() { + arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; + arced.1.notify_all(); + return Ok(Some(content)); + } - Ok(None) + count += 1; + let (buffer, _) = stream.next()?; + let image = ImageReader::new(Cursor::new(buffer)) + .with_guessed_format()? + .decode()? + .to_luma8(); + + arced + .0 + .lock() + .expect(bug::bug!(POISONED_MUTEX)) + .images + .push(image); + arced.1.notify_one(); + } + + // dbg_elapsed(count, start); + + arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; + arced.1.notify_all(); + + Ok(None) + }) } -- 2.40.1 From 0737ca690730d919c6a882ea6a9d1562c3ba6c12 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 10 May 2025 13:47:46 -0400 Subject: [PATCH 2/5] keyfork-qrcode: add debug printing --- crates/qrcode/keyfork-qrcode/src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/qrcode/keyfork-qrcode/src/lib.rs b/crates/qrcode/keyfork-qrcode/src/lib.rs index 2599019..b08810e 100644 --- a/crates/qrcode/keyfork-qrcode/src/lib.rs +++ b/crates/qrcode/keyfork-qrcode/src/lib.rs @@ -213,7 +213,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR let arced = Arc::new((Mutex::new(scan_queue), Condvar::new())); let (tx, rx) = channel(); - for _ in 0..thread_count { + for i in 0..thread_count { let tx = tx.clone(); let arced = arced.clone(); @@ -244,13 +244,16 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR } let mut queue = condvar .wait_while(queue, |queue| { + eprintln!("thread {i} received signal"); queue.images.is_empty() || !queue.shutdown }) .expect(bug::bug!(POISONED_MUTEX)); if let Some(image) = queue.images.pop() { + eprintln!("thread {i} received image"); // drop the queue here since this is what's going to take time drop(queue); if let Some(content) = scanner.scan_image(image) { + println!("scanned: {content}"); tx.send(content).expect( bug::bug!("A scanned image could not be sent") ); @@ -263,6 +266,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR while Instant::now().duration_since(start) < timeout { if let Ok(content) = rx.try_recv() { arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; + eprintln!("shutting down threads (good)"); arced.1.notify_all(); return Ok(Some(content)); } @@ -280,12 +284,14 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR .expect(bug::bug!(POISONED_MUTEX)) .images .push(image); + eprintln!("notifying any thread"); arced.1.notify_one(); } - // dbg_elapsed(count, start); + dbg_elapsed(count, start); arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true; + eprintln!("shutting down threads (bad)"); arced.1.notify_all(); Ok(None) -- 2.40.1 From f63b686e705e2a5e02dca59b35a7f0b8571630bc Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 10 May 2025 14:25:55 -0400 Subject: [PATCH 3/5] keyfork-qrcode: wait while empty AND running --- crates/qrcode/keyfork-qrcode/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/qrcode/keyfork-qrcode/src/lib.rs b/crates/qrcode/keyfork-qrcode/src/lib.rs index b08810e..0955be0 100644 --- a/crates/qrcode/keyfork-qrcode/src/lib.rs +++ b/crates/qrcode/keyfork-qrcode/src/lib.rs @@ -245,7 +245,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR let mut queue = condvar .wait_while(queue, |queue| { eprintln!("thread {i} received signal"); - queue.images.is_empty() || !queue.shutdown + queue.images.is_empty() || queue.shutdown }) .expect(bug::bug!(POISONED_MUTEX)); if let Some(image) = queue.images.pop() { -- 2.40.1 From 2083eb216fa96ed7b9385c8559a01cb16c84c42e Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 10 May 2025 14:42:50 -0400 Subject: [PATCH 4/5] keyfork-qrcode: handle two good scans --- crates/qrcode/keyfork-qrcode/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/qrcode/keyfork-qrcode/src/lib.rs b/crates/qrcode/keyfork-qrcode/src/lib.rs index 0955be0..e1a81c7 100644 --- a/crates/qrcode/keyfork-qrcode/src/lib.rs +++ b/crates/qrcode/keyfork-qrcode/src/lib.rs @@ -244,22 +244,21 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR } let mut queue = condvar .wait_while(queue, |queue| { - eprintln!("thread {i} received signal"); - queue.images.is_empty() || queue.shutdown + queue.images.is_empty() && !queue.shutdown }) .expect(bug::bug!(POISONED_MUTEX)); if let Some(image) = queue.images.pop() { - eprintln!("thread {i} received image"); // drop the queue here since this is what's going to take time drop(queue); if let Some(content) = scanner.scan_image(image) { - println!("scanned: {content}"); - tx.send(content).expect( - bug::bug!("A scanned image could not be sent") - ); + eprintln!("scanned: {content}"); + if tx.send(content).is_err() { + break; + } } } } + eprintln!("shutting down thread {i}"); }); } @@ -284,7 +283,6 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result, QR .expect(bug::bug!(POISONED_MUTEX)) .images .push(image); - eprintln!("notifying any thread"); arced.1.notify_one(); } -- 2.40.1 From 18773d351c6a63becfbe1a17fce9f10dec18fb6d Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 10 May 2025 15:01:22 -0400 Subject: [PATCH 5/5] keyfork-qrcode: add framerate tracker --- crates/qrcode/keyfork-qrcode/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/qrcode/keyfork-qrcode/src/lib.rs b/crates/qrcode/keyfork-qrcode/src/lib.rs index e1a81c7..96bbe01 100644 --- a/crates/qrcode/keyfork-qrcode/src/lib.rs +++ b/crates/qrcode/keyfork-qrcode/src/lib.rs @@ -170,6 +170,8 @@ fn dbg_elapsed(count: u64, instant: Instant) { let elapsed = instant.elapsed().as_secs(); let framerate = count as f64 / elapsed as f64; eprintln!("framerate: {count}/{elapsed} = {framerate}"); + std::thread::sleep(std::time::Duration::from_secs(5)); + } #[derive(Debug)] -- 2.40.1