221 lines
6.3 KiB
Rust
221 lines
6.3 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
use std::thread::{self, JoinHandle};
|
|
use std::time::Duration;
|
|
|
|
use crossbeam_channel::{self, Receiver, Sender, TryRecvError};
|
|
|
|
use crate::recording::Recorder;
|
|
use crate::serial_core::serial::{
|
|
SerialIoStats, SerialPortReadWrite, SerialProtocol, run_serial_loop,
|
|
};
|
|
|
|
/// Connection state visible to the UI.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ConnectionState {
|
|
Disconnected,
|
|
Connecting,
|
|
Connected,
|
|
Streaming,
|
|
Error,
|
|
}
|
|
|
|
/// A pressure sample forwarded to the render layer.
|
|
pub struct PressureSample {
|
|
pub matrix: Vec<u32>,
|
|
pub rows: u32,
|
|
pub cols: u32,
|
|
}
|
|
|
|
struct Session {
|
|
cancel_tx: Sender<()>,
|
|
handle: JoinHandle<()>,
|
|
sample_rx: Receiver<Vec<i32>>,
|
|
stats_rx: Receiver<SerialIoStats>,
|
|
rows: u32,
|
|
cols: u32,
|
|
}
|
|
|
|
/// Thread-safe connection manager that the UI and renderer can share.
|
|
pub struct ConnectionManager {
|
|
state: Arc<Mutex<ConnectionState>>,
|
|
session: Arc<Mutex<Option<Session>>>,
|
|
latest_sample: Arc<Mutex<Option<PressureSample>>>,
|
|
stats: Arc<Mutex<SerialIoStats>>,
|
|
}
|
|
|
|
impl ConnectionManager {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
state: Arc::new(Mutex::new(ConnectionState::Disconnected)),
|
|
session: Arc::new(Mutex::new(None)),
|
|
latest_sample: Arc::new(Mutex::new(None)),
|
|
stats: Arc::new(Mutex::new(SerialIoStats::default())),
|
|
}
|
|
}
|
|
|
|
pub fn state(&self) -> ConnectionState {
|
|
*self.state.lock().unwrap()
|
|
}
|
|
|
|
pub fn set_state(&self, new_state: ConnectionState) {
|
|
*self.state.lock().unwrap() = new_state;
|
|
}
|
|
|
|
pub fn stats(&self) -> SerialIoStats {
|
|
let session = self.session.lock().unwrap();
|
|
if let Some(ref session) = *session {
|
|
loop {
|
|
match session.stats_rx.try_recv() {
|
|
Ok(stats) => *self.stats.lock().unwrap() = stats,
|
|
Err(TryRecvError::Empty) => break,
|
|
Err(TryRecvError::Disconnected) => break,
|
|
}
|
|
}
|
|
}
|
|
|
|
*self.stats.lock().unwrap()
|
|
}
|
|
|
|
/// Connect to the given serial port and start streaming in a background thread.
|
|
pub fn connect(
|
|
&self,
|
|
port_name: &str,
|
|
rows: u32,
|
|
cols: u32,
|
|
baud_rate: u32,
|
|
protocol: SerialProtocol,
|
|
recorder: Recorder,
|
|
) {
|
|
self.disconnect();
|
|
self.set_state(ConnectionState::Connecting);
|
|
|
|
let port = port_name.to_owned();
|
|
let state = Arc::clone(&self.state);
|
|
let session_guard = Arc::clone(&self.session);
|
|
let latest_sample = Arc::clone(&self.latest_sample);
|
|
let (cancel_tx, cancel_rx) = crossbeam_channel::bounded::<()>(1);
|
|
let (sample_tx, sample_rx) = crossbeam_channel::bounded::<Vec<i32>>(16);
|
|
let (stats_tx, stats_rx) = crossbeam_channel::bounded::<SerialIoStats>(16);
|
|
*self.stats.lock().unwrap() = SerialIoStats::default();
|
|
|
|
let handle = thread::spawn(move || {
|
|
let result = run_device_loop(
|
|
&port,
|
|
rows,
|
|
cols,
|
|
baud_rate,
|
|
protocol,
|
|
&state,
|
|
&cancel_rx,
|
|
&sample_tx,
|
|
&stats_tx,
|
|
&latest_sample,
|
|
recorder,
|
|
);
|
|
if let Err(e) = result {
|
|
eprintln!("[connection] device loop error: {e}");
|
|
*state.lock().unwrap() = ConnectionState::Error;
|
|
}
|
|
});
|
|
|
|
*session_guard.lock().unwrap() = Some(Session {
|
|
cancel_tx,
|
|
handle,
|
|
sample_rx,
|
|
stats_rx,
|
|
rows,
|
|
cols,
|
|
});
|
|
}
|
|
|
|
/// Disconnect from the device, stopping the background thread.
|
|
pub fn disconnect(&self) {
|
|
let session = {
|
|
let mut guard = self.session.lock().unwrap();
|
|
guard.take()
|
|
};
|
|
|
|
if let Some(session) = session {
|
|
let _ = session.cancel_tx.send(());
|
|
let _ = session.handle.join();
|
|
}
|
|
|
|
self.set_state(ConnectionState::Disconnected);
|
|
*self.latest_sample.lock().unwrap() = None;
|
|
*self.stats.lock().unwrap() = SerialIoStats::default();
|
|
}
|
|
|
|
/// Drain pending samples (non-blocking) and return the last one.
|
|
pub fn take_latest_sample(&self) -> Option<PressureSample> {
|
|
let session = self.session.lock().unwrap();
|
|
if let Some(ref session) = *session {
|
|
let mut last = None;
|
|
loop {
|
|
match session.sample_rx.try_recv() {
|
|
Ok(vals) => {
|
|
let matrix = vals.iter().map(|v| (*v).max(0) as u32).collect();
|
|
last = Some(PressureSample {
|
|
matrix,
|
|
rows: session.rows,
|
|
cols: session.cols,
|
|
});
|
|
}
|
|
Err(TryRecvError::Empty) => break,
|
|
Err(TryRecvError::Disconnected) => break,
|
|
}
|
|
}
|
|
if last.is_some() {
|
|
return last;
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Default for ConnectionManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// The blocking device loop that runs on a background thread.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn run_device_loop(
|
|
port_name: &str,
|
|
rows: u32,
|
|
cols: u32,
|
|
baud_rate: u32,
|
|
protocol: SerialProtocol,
|
|
state: &Arc<Mutex<ConnectionState>>,
|
|
cancel_rx: &Receiver<()>,
|
|
sample_tx: &Sender<Vec<i32>>,
|
|
stats_tx: &Sender<SerialIoStats>,
|
|
latest_sample: &Arc<Mutex<Option<PressureSample>>>,
|
|
recorder: Recorder,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let port = serialport::new(port_name, baud_rate)
|
|
.timeout(Duration::from_millis(1))
|
|
.open()?;
|
|
|
|
*state.lock().unwrap() = ConnectionState::Connected;
|
|
|
|
let mut rw = SerialPortReadWrite::new(port);
|
|
*state.lock().unwrap() = ConnectionState::Streaming;
|
|
|
|
run_serial_loop(
|
|
&mut rw,
|
|
rows as usize,
|
|
cols as usize,
|
|
protocol,
|
|
cancel_rx,
|
|
sample_tx,
|
|
Some(stats_tx),
|
|
Some(&recorder),
|
|
);
|
|
|
|
if let Ok(mut latest) = latest_sample.lock() {
|
|
*latest = None;
|
|
}
|
|
Ok(())
|
|
}
|