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}"))?;
let source = std::fs::read_to_string(&sketch_path)
.map_err(|e| format!("failed to read sketch {sketch_path}: {e}"))?;
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));
}
let processed = tloop_preprocess::preprocess(&source, &merged)
.map_err(|e| e.to_string())?;
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 || {
arduino::install_libraries(&cli, &libraries)?;
arduino::run(
&cli,
&[
"compile",
"--fqbn",
&fqbn,
"--output-dir",
tmp_build_dir.to_str().unwrap(),
tmp_sketch_dir.to_str().unwrap(),
],
)?;
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)));
}
}