718 lines
22 KiB
Rust
718 lines
22 KiB
Rust
use anyhow::{Result, bail};
|
|
use colored::*;
|
|
use serde::Deserialize;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
pub mod presets;
|
|
pub mod pinmap;
|
|
|
|
/// Information about a detected serial port.
|
|
#[derive(Debug, Clone)]
|
|
pub struct PortInfo {
|
|
pub port_name: String,
|
|
pub protocol: String,
|
|
pub board_name: String,
|
|
pub fqbn: String,
|
|
pub vid: String,
|
|
pub pid: String,
|
|
pub serial_number: String,
|
|
}
|
|
|
|
impl PortInfo {
|
|
/// Returns true if this looks like a USB serial port rather than a
|
|
/// legacy motherboard COM port.
|
|
pub fn is_usb(&self) -> bool {
|
|
// Has USB identifiers
|
|
if !self.vid.is_empty() && !self.pid.is_empty() {
|
|
return true;
|
|
}
|
|
// arduino-cli labels USB ports "Serial Port (USB)"
|
|
if self.protocol.contains("USB") {
|
|
return true;
|
|
}
|
|
// Unix: ttyUSB* and ttyACM* are always USB
|
|
if self.port_name.contains("ttyUSB") || self.port_name.contains("ttyACM") {
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Returns VID:PID string like "2341:0043", or empty if unknown.
|
|
pub fn vid_pid(&self) -> String {
|
|
if self.vid.is_empty() || self.pid.is_empty() {
|
|
return String::new();
|
|
}
|
|
// Normalize: strip 0x prefix, lowercase, 4-digit padded
|
|
let vid = self.vid.trim_start_matches("0x").trim_start_matches("0X");
|
|
let pid = self.pid.trim_start_matches("0x").trim_start_matches("0X");
|
|
format!("{}:{}", vid.to_lowercase(), pid.to_lowercase())
|
|
}
|
|
}
|
|
|
|
/// JSON schema for `arduino-cli board list --format json`
|
|
#[derive(Debug, Deserialize)]
|
|
struct BoardListOutput {
|
|
#[serde(default)]
|
|
detected_ports: Vec<DetectedPort>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct DetectedPort {
|
|
#[serde(default)]
|
|
port: Option<PortEntry>,
|
|
#[serde(default)]
|
|
matching_boards: Option<Vec<MatchingBoard>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct PortEntry {
|
|
#[serde(default)]
|
|
address: String,
|
|
#[serde(default)]
|
|
protocol: String,
|
|
#[serde(default)]
|
|
protocol_label: String,
|
|
#[serde(default)]
|
|
properties: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct MatchingBoard {
|
|
#[serde(default)]
|
|
name: String,
|
|
#[serde(default)]
|
|
fqbn: String,
|
|
}
|
|
|
|
/// Enumerate serial ports via `arduino-cli board list --format json`.
|
|
/// Falls back to OS-level detection if arduino-cli is unavailable.
|
|
pub fn list_ports() -> Vec<PortInfo> {
|
|
if let Some(cli) = find_arduino_cli() {
|
|
if let Ok(ports) = list_ports_via_cli(&cli) {
|
|
return ports;
|
|
}
|
|
}
|
|
list_ports_fallback()
|
|
}
|
|
|
|
fn list_ports_via_cli(cli: &Path) -> Result<Vec<PortInfo>> {
|
|
let output = Command::new(cli)
|
|
.args(["board", "list", "--format", "json"])
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
bail!("arduino-cli board list failed");
|
|
}
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let parsed: BoardListOutput = serde_json::from_str(&stdout)?;
|
|
let mut result = Vec::new();
|
|
|
|
for dp in parsed.detected_ports {
|
|
let port = match dp.port {
|
|
Some(p) => p,
|
|
None => continue,
|
|
};
|
|
|
|
if port.protocol != "serial" {
|
|
continue;
|
|
}
|
|
|
|
let (board_name, fqbn) = match dp.matching_boards {
|
|
Some(ref boards) if !boards.is_empty() => {
|
|
(boards[0].name.clone(), boards[0].fqbn.clone())
|
|
}
|
|
_ => ("Unknown".to_string(), String::new()),
|
|
};
|
|
|
|
// Extract VID, PID, serial number from properties
|
|
let (vid, pid, serial_number) = match &port.properties {
|
|
Some(props) => {
|
|
let v = props.get("vid")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let p = props.get("pid")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let sn = props.get("serialNumber")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
(v, p, sn)
|
|
}
|
|
None => (String::new(), String::new(), String::new()),
|
|
};
|
|
|
|
result.push(PortInfo {
|
|
port_name: port.address,
|
|
protocol: port.protocol_label,
|
|
board_name,
|
|
fqbn,
|
|
vid,
|
|
pid,
|
|
serial_number,
|
|
});
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Fallback port detection when arduino-cli is not available.
|
|
fn list_ports_fallback() -> Vec<PortInfo> {
|
|
let mut result = Vec::new();
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::fs;
|
|
if let Ok(entries) = fs::read_dir("/dev") {
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name().to_string_lossy().to_string();
|
|
if name.starts_with("ttyUSB") || name.starts_with("ttyACM") {
|
|
let path = format!("/dev/{}", name);
|
|
let board = if name.starts_with("ttyUSB") {
|
|
"Likely CH340/FTDI (run 'anvil setup' for full detection)"
|
|
} else {
|
|
"Likely Arduino (run 'anvil setup' for full detection)"
|
|
};
|
|
result.push(PortInfo {
|
|
port_name: path,
|
|
protocol: "serial".to_string(),
|
|
board_name: board.to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(),
|
|
pid: String::new(),
|
|
serial_number: String::new(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
if let Ok(output) = Command::new("powershell")
|
|
.args([
|
|
"-NoProfile", "-Command",
|
|
"Get-CimInstance Win32_SerialPort | Select-Object DeviceID,Caption | ConvertTo-Json",
|
|
])
|
|
.output()
|
|
{
|
|
if output.status.success() {
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for line in stdout.lines() {
|
|
if let Some(idx) = line.find("COM") {
|
|
let port = line[idx..].split('"').next().unwrap_or("").trim();
|
|
if !port.is_empty() {
|
|
result.push(PortInfo {
|
|
port_name: port.to_string(),
|
|
protocol: "serial".to_string(),
|
|
board_name: "Detected via WMI".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(),
|
|
pid: String::new(),
|
|
serial_number: String::new(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Find a port by VID:PID. Returns the port name if found.
|
|
pub fn resolve_vid_pid(vid_pid: &str) -> Option<String> {
|
|
let ports = list_ports();
|
|
let needle = vid_pid.to_lowercase();
|
|
|
|
for p in &ports {
|
|
if p.vid_pid() == needle {
|
|
return Some(p.port_name.clone());
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Pick the best default port from a list. Returns the index into the
|
|
/// slice, or None if the list is empty.
|
|
///
|
|
/// Priority:
|
|
/// 1. A port with a recognized board (non-empty FQBN)
|
|
/// 2. A USB serial port (skip legacy motherboard COM ports)
|
|
/// 3. First port in the list
|
|
pub fn pick_default_port(ports: &[PortInfo]) -> Option<usize> {
|
|
if ports.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Prefer a port with a recognized FQBN
|
|
for (i, p) in ports.iter().enumerate() {
|
|
if !p.fqbn.is_empty() {
|
|
return Some(i);
|
|
}
|
|
}
|
|
|
|
// Prefer a USB port over a legacy serial port
|
|
for (i, p) in ports.iter().enumerate() {
|
|
if p.is_usb() {
|
|
return Some(i);
|
|
}
|
|
}
|
|
|
|
// Fall back to the first port
|
|
Some(0)
|
|
}
|
|
|
|
/// Auto-detect a single serial port.
|
|
pub fn auto_detect_port() -> Result<String> {
|
|
let ports = list_ports();
|
|
|
|
if ports.is_empty() {
|
|
bail!(
|
|
"No serial ports found. Is the board plugged in?\n\
|
|
Run: anvil devices"
|
|
);
|
|
}
|
|
|
|
let idx = pick_default_port(&ports).unwrap_or(0);
|
|
|
|
if ports.len() > 1 {
|
|
eprintln!("{}", "Multiple serial ports detected:".yellow());
|
|
for p in &ports {
|
|
eprintln!(" {} ({})", p.port_name, p.board_name);
|
|
}
|
|
eprintln!(
|
|
"{}",
|
|
format!(
|
|
"Auto-selected {}. Use -p to override.",
|
|
ports[idx].port_name
|
|
).yellow()
|
|
);
|
|
}
|
|
|
|
Ok(ports[idx].port_name.clone())
|
|
}
|
|
|
|
/// Print detailed port information.
|
|
pub fn print_port_details(ports: &[PortInfo]) {
|
|
if ports.is_empty() {
|
|
println!(" {}", "No serial devices found.".yellow());
|
|
println!();
|
|
println!(" Checklist:");
|
|
println!(" 1. Is the board plugged in via USB?");
|
|
println!(" 2. Is the USB cable a data cable (not charge-only)?");
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
println!(" 3. Check kernel log: dmesg | tail -20");
|
|
println!(" 4. Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'");
|
|
}
|
|
println!(" 5. Try a different USB port or cable");
|
|
return;
|
|
}
|
|
|
|
let default_idx = pick_default_port(ports);
|
|
|
|
for (i, port) in ports.iter().enumerate() {
|
|
let is_default = default_idx == Some(i);
|
|
|
|
if is_default {
|
|
print!(" {}", port.port_name.green().bold());
|
|
println!(" {}", "<-- default".bright_green());
|
|
} else {
|
|
println!(" {}", port.port_name.green().bold());
|
|
}
|
|
|
|
println!(" Board: {}", port.board_name);
|
|
if !port.fqbn.is_empty() {
|
|
println!(" FQBN: {}", port.fqbn);
|
|
}
|
|
if !port.protocol.is_empty() {
|
|
println!(" Protocol: {}", port.protocol);
|
|
}
|
|
|
|
let vp = port.vid_pid();
|
|
if !vp.is_empty() {
|
|
println!(" VID:PID: {}", vp.bright_cyan());
|
|
}
|
|
if !port.serial_number.is_empty() {
|
|
println!(" Serial: {}", port.serial_number.bright_black());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::fs::OpenOptions;
|
|
let path = std::path::Path::new(&port.port_name);
|
|
if path.exists() {
|
|
match OpenOptions::new().write(true).open(path) {
|
|
Ok(_) => {
|
|
println!(
|
|
" Permissions: {}",
|
|
"OK (writable)".green()
|
|
);
|
|
}
|
|
Err(_) => {
|
|
println!(
|
|
" Permissions: {}",
|
|
"NOT writable -- run: sudo usermod -aG dialout $USER"
|
|
.red()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
println!();
|
|
}
|
|
|
|
// Show help section
|
|
if let Some(idx) = default_idx {
|
|
if ports.len() > 1 {
|
|
println!(
|
|
" Anvil will use {} when no port is specified.",
|
|
ports[idx].port_name.bright_white()
|
|
);
|
|
println!();
|
|
}
|
|
|
|
println!(
|
|
" {}",
|
|
"Save a default port for your project:".bright_white()
|
|
);
|
|
println!();
|
|
println!(
|
|
" {}",
|
|
format!("anvil devices --set {}", ports[idx].port_name).bright_cyan(),
|
|
);
|
|
println!(
|
|
" {}",
|
|
"anvil devices --set".bright_cyan(),
|
|
);
|
|
println!();
|
|
println!(
|
|
" {}",
|
|
"Both forms save the port AND VID:PID to .anvil.local automatically.".bright_black()
|
|
);
|
|
println!(
|
|
" {}",
|
|
"Use --get to see what's saved, --set to change it.".bright_black()
|
|
);
|
|
println!();
|
|
|
|
// Show VID:PID explanation if any USB devices are present
|
|
let has_usb = ports.iter().any(|p| !p.vid_pid().is_empty());
|
|
if has_usb {
|
|
println!(
|
|
" {}",
|
|
"VID:PID identifies the USB device, not the port number.".bright_white()
|
|
);
|
|
println!(
|
|
" {}",
|
|
"If the device moves to a different port after replug,".bright_black()
|
|
);
|
|
println!(
|
|
" {}",
|
|
"Anvil will find it automatically by VID:PID.".bright_black()
|
|
);
|
|
println!();
|
|
}
|
|
|
|
println!(
|
|
" {}",
|
|
"Port resolution priority:".bright_white()
|
|
);
|
|
println!(
|
|
" {} {}",
|
|
"1.".bright_white(),
|
|
"-p flag (upload.bat -p COM3)".bright_black()
|
|
);
|
|
println!(
|
|
" {} {}",
|
|
"2.".bright_white(),
|
|
"VID:PID from .anvil.local (tracks device across port changes)".bright_black()
|
|
);
|
|
println!(
|
|
" {} {}",
|
|
"3.".bright_white(),
|
|
"Saved port from .anvil.local".bright_black()
|
|
);
|
|
println!(
|
|
" {} {}",
|
|
"4.".bright_white(),
|
|
"Auto-detect (prefers USB over legacy COM)".bright_black()
|
|
);
|
|
println!();
|
|
}
|
|
}
|
|
|
|
/// Find arduino-cli in PATH or in ~/.anvil/bin.
|
|
pub fn find_arduino_cli() -> Option<PathBuf> {
|
|
if let Ok(path) = which::which("arduino-cli") {
|
|
return Some(path);
|
|
}
|
|
|
|
if let Ok(home) = crate::project::config::anvil_home() {
|
|
let name = if cfg!(target_os = "windows") {
|
|
"arduino-cli.exe"
|
|
} else {
|
|
"arduino-cli"
|
|
};
|
|
let bin = home.join("bin").join(name);
|
|
if bin.exists() {
|
|
return Some(bin);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Check if the arduino:avr core is installed.
|
|
pub fn is_avr_core_installed(cli_path: &Path) -> bool {
|
|
let output = Command::new(cli_path)
|
|
.args(["core", "list"])
|
|
.output();
|
|
|
|
match output {
|
|
Ok(out) => {
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
stdout.contains("arduino:avr")
|
|
}
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_list_ports_does_not_panic() {
|
|
let _ports = list_ports();
|
|
}
|
|
|
|
#[test]
|
|
fn test_port_info_clone() {
|
|
let info = PortInfo {
|
|
port_name: "/dev/ttyUSB0".to_string(),
|
|
protocol: "serial".to_string(),
|
|
board_name: "Test".to_string(),
|
|
fqbn: "arduino:avr:uno".to_string(),
|
|
vid: "0x2341".to_string(),
|
|
pid: "0x0043".to_string(),
|
|
serial_number: String::new(),
|
|
};
|
|
let cloned = info.clone();
|
|
assert_eq!(cloned.port_name, info.port_name);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vid_pid_formatting() {
|
|
let info = PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Arduino Uno".to_string(),
|
|
fqbn: "arduino:avr:uno".to_string(),
|
|
vid: "0x2341".to_string(),
|
|
pid: "0x0043".to_string(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert_eq!(info.vid_pid(), "2341:0043");
|
|
}
|
|
|
|
#[test]
|
|
fn test_vid_pid_empty() {
|
|
let info = PortInfo {
|
|
port_name: "COM1".to_string(),
|
|
protocol: "Serial Port".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(),
|
|
pid: String::new(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert_eq!(info.vid_pid(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_vid_pid_no_prefix() {
|
|
let info = PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: "2341".to_string(),
|
|
pid: "0043".to_string(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert_eq!(info.vid_pid(), "2341:0043");
|
|
}
|
|
|
|
#[test]
|
|
fn test_vid_pid_case_insensitive() {
|
|
let info = PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: "0x2341".to_string(),
|
|
pid: "0x0043".to_string(),
|
|
serial_number: String::new(),
|
|
};
|
|
// vid_pid() should normalize to lowercase
|
|
assert_eq!(info.vid_pid(), "2341:0043");
|
|
|
|
let upper = PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: "0x1A86".to_string(),
|
|
pid: "0x7523".to_string(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert_eq!(upper.vid_pid(), "1a86:7523");
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_usb_from_vid_pid() {
|
|
let info = PortInfo {
|
|
port_name: "COM5".to_string(),
|
|
protocol: "Serial Port".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: "1a86".to_string(),
|
|
pid: "7523".to_string(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert!(info.is_usb());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_empty_board_list() {
|
|
let json = r#"{"detected_ports": []}"#;
|
|
let parsed: BoardListOutput = serde_json::from_str(json).unwrap();
|
|
assert!(parsed.detected_ports.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_board_list_with_port() {
|
|
let json = r#"{
|
|
"detected_ports": [{
|
|
"port": {
|
|
"address": "/dev/ttyUSB0",
|
|
"protocol": "serial",
|
|
"protocol_label": "Serial Port (USB)",
|
|
"properties": {
|
|
"vid": "0x2341",
|
|
"pid": "0x0043",
|
|
"serialNumber": "ABC123"
|
|
}
|
|
},
|
|
"matching_boards": [{
|
|
"name": "Arduino Uno",
|
|
"fqbn": "arduino:avr:uno"
|
|
}]
|
|
}]
|
|
}"#;
|
|
let parsed: BoardListOutput = serde_json::from_str(json).unwrap();
|
|
assert_eq!(parsed.detected_ports.len(), 1);
|
|
let dp = &parsed.detected_ports[0];
|
|
let port = dp.port.as_ref().unwrap();
|
|
assert_eq!(port.address, "/dev/ttyUSB0");
|
|
let props = port.properties.as_ref().unwrap();
|
|
assert_eq!(props["vid"].as_str().unwrap(), "0x2341");
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_usb_from_protocol_label() {
|
|
let usb = PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(),
|
|
pid: String::new(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert!(usb.is_usb());
|
|
|
|
let legacy = PortInfo {
|
|
port_name: "COM1".to_string(),
|
|
protocol: "Serial Port".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(),
|
|
pid: String::new(),
|
|
serial_number: String::new(),
|
|
};
|
|
assert!(!legacy.is_usb());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pick_default_prefers_fqbn() {
|
|
let ports = vec![
|
|
PortInfo {
|
|
port_name: "COM1".to_string(),
|
|
protocol: "Serial Port".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(), pid: String::new(), serial_number: String::new(),
|
|
},
|
|
PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Arduino Uno".to_string(),
|
|
fqbn: "arduino:avr:uno".to_string(),
|
|
vid: "0x2341".to_string(), pid: "0x0043".to_string(), serial_number: String::new(),
|
|
},
|
|
];
|
|
assert_eq!(pick_default_port(&ports), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pick_default_prefers_usb_over_legacy() {
|
|
let ports = vec![
|
|
PortInfo {
|
|
port_name: "COM1".to_string(),
|
|
protocol: "Serial Port".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(), pid: String::new(), serial_number: String::new(),
|
|
},
|
|
PortInfo {
|
|
port_name: "COM3".to_string(),
|
|
protocol: "Serial Port (USB)".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: "1a86".to_string(), pid: "7523".to_string(), serial_number: String::new(),
|
|
},
|
|
];
|
|
assert_eq!(pick_default_port(&ports), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pick_default_empty() {
|
|
let ports: Vec<PortInfo> = vec![];
|
|
assert_eq!(pick_default_port(&ports), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pick_default_single() {
|
|
let ports = vec![
|
|
PortInfo {
|
|
port_name: "COM1".to_string(),
|
|
protocol: "Serial Port".to_string(),
|
|
board_name: "Unknown".to_string(),
|
|
fqbn: String::new(),
|
|
vid: String::new(), pid: String::new(), serial_number: String::new(),
|
|
},
|
|
];
|
|
assert_eq!(pick_default_port(&ports), Some(0));
|
|
}
|
|
} |