use std::fmt;
use std::fs;
use serde::{Deserialize, Serialize};
use crate::wrapper::platformio;
pub const UNSPECIFIED_PARAM: &str = "UNSPECIFIED";
const UNSPECIFIED_RUSTC_VERSION: &str = "nightly-2025-04-27";
const PLATFORMS_DIR: &str = "platforms";
const BOARDS_DIR: &str = "boards";
const SUPPORTED_BOARD_IDS: &[&str] = &[
"diecimilaatmega168",
"leonardo",
"ATmega2560",
"ATmega1280",
"nanoatmega328",
"nanoatmega328new",
"uno",
"micro",
"sparkfun_promicro8",
"nanoatmega168",
];
#[derive(Debug, Serialize)]
pub struct Board {
pub id: String,
pub mcu: String,
pub platform: Platform,
pub cargo_feature: String,
pub bus_speed: u32,
pub upload_protocol: String,
pub rustc_version: String,
pub fcpu: u32,
pub ram: u32,
pub rom: u32,
pub name: String,
}
impl Board {
pub fn new(
id: &str,
cargo_feature: &str,
rustc_version: &str,
) -> Result<Board, String> {
let board = get_pio_board(id)?;
let upload_config = get_pio_upload_config(id, &board.platform)?;
Ok(Board {
id: board.id,
mcu: board.mcu,
platform: Platform::from(board.platform.as_str()),
cargo_feature: String::from(cargo_feature),
bus_speed: upload_config.speed,
upload_protocol: upload_config.protocol,
rustc_version: String::from(rustc_version),
fcpu: board.fcpu,
ram: board.ram,
rom: board.rom,
name: board.name,
})
}
pub fn new_custom(
id: &str,
mcu: &str,
platform: &str,
cargo_feature: &str,
bus_speed: u32,
upload_protocol: &str,
rustc_version: &str,
fcpu: u32,
ram: u32,
rom: u32,
name: &str,
) -> Board {
Board {
id: String::from(id),
mcu: String::from(mcu),
platform: Platform::from(platform),
cargo_feature: String::from(cargo_feature),
bus_speed: bus_speed,
upload_protocol: String::from(upload_protocol),
rustc_version: String::from(rustc_version),
fcpu: fcpu,
ram: ram,
rom: rom,
name: name.to_string(),
}
}
}
#[derive(Debug, Serialize, Clone, PartialEq)]
pub enum Platform {
ATMELAVR,
UNKNOWN,
}
impl Platform {
pub fn from(value: &str) -> Platform {
match value {
"atmelavr" => Platform::ATMELAVR,
_ => Platform::UNKNOWN
}
}
pub fn to_string(&self) ->String {
match self {
Self::ATMELAVR => "atmelavr".to_string(),
Self::UNKNOWN => UNSPECIFIED_PARAM.to_string()
}
}
pub fn to_cargo_arch(&self) -> String {
match self {
Self::ATMELAVR => String::from("avr-none"),
Self::UNKNOWN => UNSPECIFIED_PARAM.to_string()
}
}
pub fn to_framework(&self) -> String {
match self {
_ => String::from("arduino"),
}
}
}
impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Platform::ATMELAVR => write!(f, "atmelavr"),
Platform::UNKNOWN => write!(f, "Unknown"),
}
}
}
#[derive(Debug, Deserialize, Clone)]
struct PioBoard {
id: String,
mcu: String,
fcpu: u32,
ram: u32,
rom: u32,
name: String,
platform: String,
}
fn get_pio_board(id: &str) -> Result<PioBoard, String> {
match get_pio_boards(id) {
Ok(boards) => {
if boards.is_empty() {
return Err("Unsupported board ID".to_string());
}
for board in &boards {
if board.id == id {
return Ok(board.clone());
}
}
let best_match = boards[0].clone();
return Ok(best_match);
},
Err(e) => { return Err(e) },
}
}
fn get_pio_boards(filter: &str) -> Result<Vec<PioBoard>, String> {
let result = platformio::get_boards(filter)?;
match serde_json::from_str::<Vec<PioBoard>>(result.as_str()) {
Ok(boards) => Ok(boards),
Err(_) => Err("Failed to parse JSON from platformIO.".to_string())
}
}
#[derive(Deserialize, Debug)]
struct PioBoardManifest {
upload: PioUploadConfig,
}
#[derive(Deserialize, Debug)]
struct PioUploadConfig {
speed: u32,
protocol: String,
}
fn get_pio_upload_config(board_id: &str, platform: &str) -> Result<PioUploadConfig, String> {
let (_, core_dir) = platformio::get_pio_dirs()?;
let confs_path = core_dir.join(PLATFORMS_DIR).join(platform).join(BOARDS_DIR);
if !confs_path.exists() {
platformio::download_pio_platform(platform)?;
}
let board_path = confs_path.join(format!("{board_id}.json"));
if !board_path.exists() {
return Err("Unsupported board identifier.".to_string());
}
let file_contents = match fs::read_to_string(&board_path) {
Ok(s) => s,
Err(_) => {
return Err("Failed to read board's configuration file.".to_string());
}
};
let manifest: PioBoardManifest = match serde_json::from_str(&file_contents) {
Ok(json) => json,
Err(_) => {
return Err("Failed to parse board's configuration file.".to_string());
}
};
Ok(manifest.upload)
}
pub fn get_board(id: &str) -> Result<Board, String> {
let board = match id {
"diecimilaatmega168" => Board::new(id, "arduino-diecimila", "nightly-2025-04-27")?,
"leonardo" => Board::new(id, "arduino-leonardo", "nightly-2025-04-27")?,
"ATmega2560" => Board::new(id, "arduino-mega2560", "nightly-2025-04-27")?,
"ATmega1280" => Board::new(id, "arduino-mega1280", "nightly-2025-04-27")?,
"nanoatmega328" => Board::new(id, "arduino-nano", "nightly-2025-04-27")?,
"nanoatmega328new" => Board::new(id, "arduino-nano", "nightly-2025-04-27")?,
"micro" => Board::new(id, "arduino-micro", "nightly-2025-04-27")?,
"uno" => Board::new(id, "arduino-uno", "nightly-2025-04-27")?,
"sparkfun_promicro8" => Board::new(id, "sparkfun-promicro", "nightly-2025-04-27")?,
"nanoatmega168" => Board::new(id, "nano168", "nightly-2025-04-27")?,
_ => {
return Err("Unsupported board ID.".to_string());
}
};
Ok(board)
}
pub fn get_boards(filter: Option<&String>) -> Result<Vec<Board>, String> {
let board_ids = match filter {
Some(f) => get_filtered_board_ids(f),
None => Vec::from(SUPPORTED_BOARD_IDS)
};
let mut boards = Vec::new();
for id in board_ids {
let board = get_board(id)?;
boards.push(board);
}
Ok(boards)
}
fn get_filtered_board_ids(filter: &String) -> Vec<&str> {
let parsed_filter = filter.to_lowercase();
let board_ids: Vec<&str> = SUPPORTED_BOARD_IDS.iter()
.copied()
.filter(|board_id| {
board_id.to_lowercase().contains(&parsed_filter)
})
.collect();
board_ids
}
pub fn get_unspecified_board() -> Board {
Board::new_custom(
UNSPECIFIED_PARAM,
UNSPECIFIED_PARAM,
UNSPECIFIED_PARAM,
UNSPECIFIED_PARAM,
0,
UNSPECIFIED_PARAM,
UNSPECIFIED_RUSTC_VERSION,
0,
0,
0,
UNSPECIFIED_PARAM,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_board_new_custom() {
let board = Board::new_custom(
"custom_uno",
"atmega328p",
"atmelavr",
"arduino-uno",
115200,
"arduino",
"nightly-2025-04-27",
16000000,
2048,
32768,
"Custom Uno Board",
);
assert_eq!(board.id, "custom_uno");
assert_eq!(board.mcu, "atmega328p");
assert_eq!(board.platform, Platform::ATMELAVR);
assert_eq!(board.cargo_feature, "arduino-uno");
assert_eq!(board.bus_speed, 115200);
assert_eq!(board.upload_protocol, "arduino");
assert_eq!(board.rustc_version, "nightly-2025-04-27");
assert_eq!(board.fcpu, 16000000);
assert_eq!(board.ram, 2048);
assert_eq!(board.rom, 32768);
assert_eq!(board.name, "Custom Uno Board");
}
#[test]
fn test_get_unspecified_board() {
let board = get_unspecified_board();
assert_eq!(board.id, UNSPECIFIED_PARAM);
assert_eq!(board.mcu, UNSPECIFIED_PARAM);
assert_eq!(board.platform, Platform::UNKNOWN);
assert_eq!(board.cargo_feature, UNSPECIFIED_PARAM);
assert_eq!(board.bus_speed, 0);
assert_eq!(board.upload_protocol, UNSPECIFIED_PARAM);
assert_eq!(board.rustc_version, UNSPECIFIED_RUSTC_VERSION);
assert_eq!(board.fcpu, 0);
assert_eq!(board.ram, 0);
assert_eq!(board.rom, 0);
assert_eq!(board.name, UNSPECIFIED_PARAM);
}
#[test]
fn test_get_filtered_board_ids() {
let filter_nano = String::from("nano");
let filtered_nano = get_filtered_board_ids(&filter_nano);
assert!(filtered_nano.contains(&"nanoatmega328"));
assert!(filtered_nano.contains(&"nanoatmega328new"));
assert!(filtered_nano.contains(&"nanoatmega168"));
assert!(!filtered_nano.contains(&"uno"));
let filter_upper = String::from("UNO");
let filtered_upper = get_filtered_board_ids(&filter_upper);
assert!(filtered_upper.contains(&"uno"));
assert_eq!(filtered_upper.len(), 1);
let filter_empty = String::from("");
let filtered_empty = get_filtered_board_ids(&filter_empty);
assert_eq!(filtered_empty.len(), SUPPORTED_BOARD_IDS.len());
let filter_none = String::from("raspberrypi");
let filtered_none = get_filtered_board_ids(&filter_none);
assert!(filtered_none.is_empty());
}
}