use crate::{arduino, state::LoopState, LoopFeature};
use tauri::State;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct BoardInfo {
pub name: String,
pub port: String,
pub fqbn: String,
}
fn parse_board_list_json(json: &serde_json::Value) -> Vec<BoardInfo> {
let mut result = vec![];
if let Some(ports) = json.get("detected_ports").and_then(|v| v.as_array()) {
for entry in ports {
let port = entry
.get("port")
.and_then(|p| p.get("address"))
.and_then(|a| a.as_str())
.unwrap_or("")
.to_string();
if port.is_empty() {
continue;
}
let board_list = entry
.get("matching_boards")
.or_else(|| entry.get("boards"))
.and_then(|v| v.as_array());
if let Some(bl) = board_list {
for board in bl {
let fqbn = board["fqbn"].as_str().unwrap_or("").to_string();
if !fqbn.is_empty() {
result.push(BoardInfo {
name: board["name"].as_str().unwrap_or("Unknown").to_string(),
fqbn,
port: port.clone(),
});
}
}
}
}
return result;
}
if let Some(entries) = json.as_array() {
for entry in entries {
let port = entry["address"].as_str().unwrap_or("").to_string();
if let Some(boards) = entry["boards"].as_array() {
for board in boards {
let fqbn = board["fqbn"].as_str().unwrap_or("").to_string();
if !fqbn.is_empty() {
result.push(BoardInfo {
name: board["name"].as_str().unwrap_or("Unknown").to_string(),
fqbn,
port: port.clone(),
});
}
}
}
}
}
result
}
pub fn detect_boards(cli_path: &std::path::Path) -> Result<Vec<BoardInfo>, String> {
let output = arduino::run(cli_path, &["board", "list", "--format", "json"])
.map_err(|e| e.to_string())?;
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).map_err(|e| e.to_string())?;
Ok(parse_board_list_json(&json))
}
#[tauri::command]
pub fn loop_connected_boards(state: State<LoopState>) -> Result<Vec<BoardInfo>, String> {
if !state.has_feature(&LoopFeature::ConnectedBoards) {
return Err("feature connected_boards not enabled in loop.config.toml".into());
}
detect_boards(&state.arduino_cli_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn board_info_serializes() {
let b = BoardInfo {
name: "Arduino Uno".into(),
port: "COM3".into(),
fqbn: "arduino:avr:uno".into(),
};
let json = serde_json::to_string(&b).unwrap();
let roundtrip: BoardInfo = serde_json::from_str(&json).unwrap();
assert_eq!(roundtrip.name, "Arduino Uno");
assert_eq!(roundtrip.port, "COM3");
assert_eq!(roundtrip.fqbn, "arduino:avr:uno");
}
#[test]
fn parse_old_format_flat_array() {
let json: serde_json::Value = serde_json::json!([
{
"address": "COM3",
"boards": [{ "name": "Arduino Uno", "fqbn": "arduino:avr:uno" }]
}
]);
let boards = parse_board_list_json(&json);
assert_eq!(boards.len(), 1);
assert_eq!(boards[0].port, "COM3");
assert_eq!(boards[0].fqbn, "arduino:avr:uno");
}
#[test]
fn parse_new_format_detected_ports() {
let json: serde_json::Value = serde_json::json!({
"detected_ports": [
{
"port": { "address": "COM4", "protocol": "serial" },
"matching_boards": [{ "name": "Arduino Mega", "fqbn": "arduino:avr:mega" }]
}
]
});
let boards = parse_board_list_json(&json);
assert_eq!(boards.len(), 1);
assert_eq!(boards[0].port, "COM4");
assert_eq!(boards[0].fqbn, "arduino:avr:mega");
}
#[test]
fn parse_new_format_falls_back_to_boards_field() {
let json: serde_json::Value = serde_json::json!({
"detected_ports": [
{
"port": { "address": "/dev/ttyUSB0" },
"boards": [{ "name": "Arduino Nano", "fqbn": "arduino:avr:nano" }]
}
]
});
let boards = parse_board_list_json(&json);
assert_eq!(boards.len(), 1);
assert_eq!(boards[0].port, "/dev/ttyUSB0");
}
#[test]
fn parse_skips_entries_with_empty_fqbn() {
let json: serde_json::Value = serde_json::json!({
"detected_ports": [
{
"port": { "address": "COM5" },
"matching_boards": [{ "name": "", "fqbn": "" }]
}
]
});
let boards = parse_board_list_json(&json);
assert_eq!(boards.len(), 0);
}
#[test]
fn parse_empty_input_returns_empty_vec() {
let json: serde_json::Value = serde_json::json!([]);
let boards = parse_board_list_json(&json);
assert_eq!(boards.len(), 0);
}
#[test]
#[ignore = "requires bundled arduino-cli binary; run with -- --ignored"]
fn detect_boards_parses_empty_json() {
let tmp = std::env::temp_dir().join("loop_boards_test");
let cli = crate::arduino::extract_arduino_cli(tmp).unwrap();
let result = detect_boards(&cli);
assert!(result.is_ok(), "detect_boards failed: {:?}", result.err());
}
}