tloop 0.0.3

Tauri plugin for Arduino integration — flash firmware, stream serial data, detect boards from your desktop app
use crate::{arduino, state::LoopState, LoopFeature};
use std::collections::HashMap;
use tauri::State;

fn format_json_value(v: &serde_json::Value) -> String {
    match v {
        serde_json::Value::String(s) => format!("\"{}\"", s),
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::Bool(b) => b.to_string(),
        _ => v.to_string(),
    }
}

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

    let sketch_path = state
        .firmware_sketch
        .clone()
        .ok_or_else(|| "firmware sketch path not configured".to_string())?;

    let fqbn = state
        .board_fqbn(&board)
        .ok_or_else(|| format!("unknown board: {board}"))?;

    // Read sketch source from disk
    let source = std::fs::read_to_string(&sketch_path)
        .map_err(|e| format!("failed to read sketch {sketch_path}: {e}"))?;

    // Build merged env: defaults first, runtime overrides win
    let mut merged: HashMap<String, String> = state
        .env_defaults
        .iter()
        .map(|(k, v)| (k.clone(), format_json_value(v)))
        .collect();
    for (k, v) in state.env_vars.lock().unwrap().iter() {
        merged.insert(k.clone(), format_json_value(v));
    }

    // Preprocess the sketch
    let processed = tloop_preprocess::preprocess(&source, &merged)
        .map_err(|e| e.to_string())?;

    // Write preprocessed sketch to temp dir
    // arduino-cli requires the sketch file to have the same name as its containing directory
    let tmp_sketch_dir = std::env::temp_dir().join("loop_flash").join("sketch");
    std::fs::create_dir_all(&tmp_sketch_dir)
        .map_err(|e| format!("failed to create temp sketch dir: {e}"))?;
    std::fs::write(tmp_sketch_dir.join("sketch.ino"), &processed)
        .map_err(|e| format!("failed to write preprocessed sketch: {e}"))?;

    let tmp_build_dir = std::env::temp_dir().join("loop_flash").join("build");
    std::fs::create_dir_all(&tmp_build_dir)
        .map_err(|e| format!("failed to create temp build dir: {e}"))?;

    let cli = state.arduino_cli_path.clone();
    let libraries = state.libraries.clone();

    tokio::task::spawn_blocking(move || {
        // Step 1: Install required libraries (skipped if list is empty)
        arduino::install_libraries(&cli, &libraries)?;

        // Step 2: Compile
        arduino::run(
            &cli,
            &[
                "compile",
                "--fqbn",
                &fqbn,
                "--output-dir",
                tmp_build_dir.to_str().unwrap(),
                tmp_sketch_dir.to_str().unwrap(),
            ],
        )?;
        // Step 2: Upload
        arduino::run(
            &cli,
            &[
                "upload",
                "--port",
                &port,
                "--fqbn",
                &fqbn,
                "--input-dir",
                tmp_build_dir.to_str().unwrap(),
            ],
        )
    })
    .await
    .map_err(|e| e.to_string())?
    .map_err(|e| e.to_string())?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::state::LoopState;
    use crate::LoopFeature;
    use std::collections::HashSet;

    #[test]
    fn flash_rejects_unknown_board() {
        let state = LoopState::new(
            std::path::PathBuf::from("dummy"),
            [LoopFeature::Flash].into(),
            vec![],
        );
        assert!(state.board_fqbn("nonexistent").is_none());
    }

    #[test]
    fn flash_feature_flag_rejects_when_missing() {
        let state = LoopState::new(
            std::path::PathBuf::from("dummy"),
            HashSet::new(),
            vec![],
        );
        assert!(!state.has_feature(&LoopFeature::Flash));
    }

    #[test]
    fn env_defaults_merged_with_runtime_overrides() {
        let state = LoopState::new(
            std::path::PathBuf::from("dummy"),
            [LoopFeature::Flash].into(),
            vec![],
        );
        state.set_env("speed".into(), serde_json::json!(200));
        assert_eq!(state.get_env("speed"), Some(serde_json::json!(200)));
    }
}