tloop 0.0.5

Tauri plugin for Arduino integration — flash firmware, stream serial data, detect boards from your desktop app
// crates/tauri-loop/src/commands/serial.rs
use crate::{state::LoopState, LoopFeature};
use tauri::{AppHandle, Emitter, State};

#[derive(serde::Serialize, Clone)]
pub struct SerialLine {
    pub line: String,
}

// Pure logic — testable without Tauri runtime
pub fn list_available_ports() -> Result<Vec<String>, String> {
    serialport::available_ports()
        .map(|ports| ports.into_iter().map(|p| p.port_name).collect())
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub fn loop_serial_ports(state: State<LoopState>) -> Result<Vec<String>, String> {
    if !state.has_feature(&LoopFeature::SerialPorts) {
        return Err("feature serial_ports not enabled in loop.config.toml".into());
    }
    list_available_ports()
}

#[tauri::command]
pub async fn loop_serial_start(
    port: String,
    state: State<'_, LoopState>,
    app: AppHandle,
) -> Result<(), String> {
    if !state.has_feature(&LoopFeature::SerialRead) {
        return Err("feature serial_read not enabled in loop.config.toml".into());
    }

    // Stop any running session first
    if let Some(tx) = state.serial_stop_tx.lock().unwrap().take() {
        let _ = tx.send(());
    }

    let baudrate = state.serial_baudrate;
    let (stop_tx, stop_rx) = tokio::sync::oneshot::channel::<()>();
    *state.serial_stop_tx.lock().unwrap() = Some(stop_tx);

    tokio::spawn(run_serial_loop(port, baudrate, stop_rx, app));
    Ok(())
}

async fn run_serial_loop(
    port: String,
    baudrate: u32,
    mut stop_rx: tokio::sync::oneshot::Receiver<()>,
    app: AppHandle,
) {
    let mut serial = match serialport::new(&port, baudrate)
        .timeout(std::time::Duration::from_millis(100))
        .open()
    {
        Ok(s) => s,
        Err(e) => {
            let _ = app.emit("loop://serial-error", e.to_string());
            return;
        }
    };

    let mut buf = vec![0u8; 256];
    let mut line_buf = String::new();

    loop {
        if stop_rx.try_recv().is_ok() {
            break;
        }
        match serial.read(&mut buf) {
            Ok(n) => {
                let chunk = String::from_utf8_lossy(&buf[..n]);
                line_buf.push_str(&chunk);
                while let Some(i) = line_buf.find('\n') {
                    let line = line_buf[..i].trim_end_matches('\r').to_string();
                    let _ = app.emit("loop://serial", SerialLine { line });
                    line_buf = line_buf[i + 1..].to_string();
                }
            }
            Err(ref e) if e.kind() == std::io::ErrorKind::TimedOut => {}
            Err(e) => {
                let _ = app.emit("loop://serial-error", e.to_string());
                break;
            }
        }
    }
}

#[tauri::command]
pub fn loop_serial_stop(state: State<LoopState>) -> Result<(), String> {
    if !state.has_feature(&LoopFeature::SerialRead) {
        return Err("feature serial_read not enabled in loop.config.toml".into());
    }
    if let Some(tx) = state.serial_stop_tx.lock().unwrap().take() {
        let _ = tx.send(());
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn list_ports_returns_ok() {
        // Returns at least an empty vec — count varies by machine
        let ports = list_available_ports();
        assert!(ports.is_ok(), "list_available_ports errored: {:?}", ports);
    }
}