800 lines
27 KiB
Rust
800 lines
27 KiB
Rust
//! Device types and registry — stable aliases for discovered hardware.
|
|
//!
|
|
//! The LLM always refers to devices by alias (`"pico0"`, `"arduino0"`), never
|
|
//! by raw `/dev/` paths. The `DeviceRegistry` assigns these aliases at startup
|
|
//! and provides lookup + context building for tool execution.
|
|
|
|
use super::transport::Transport;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
// ── DeviceRuntime ─────────────────────────────────────────────────────────────
|
|
|
|
/// The software runtime / execution environment of a device.
|
|
///
|
|
/// Determines which host-side tooling is used for code deployment and execution.
|
|
/// Currently only [`MicroPython`](DeviceRuntime::MicroPython) is implemented;
|
|
/// other variants return a clear "not yet supported" error.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DeviceRuntime {
|
|
/// MicroPython — uses `mpremote` for code read/write/exec.
|
|
MicroPython,
|
|
/// CircuitPython — `mpremote`-compatible (future).
|
|
CircuitPython,
|
|
/// Arduino — `arduino-cli` for sketch upload (future).
|
|
Arduino,
|
|
/// STM32 / probe-rs based flashing and debugging (future).
|
|
Nucleus,
|
|
/// Linux / Raspberry Pi — ssh/shell execution (future).
|
|
Linux,
|
|
}
|
|
|
|
impl DeviceRuntime {
|
|
/// Derive the default runtime from a [`DeviceKind`].
|
|
pub fn from_kind(kind: &DeviceKind) -> Self {
|
|
match kind {
|
|
DeviceKind::Pico | DeviceKind::Esp32 | DeviceKind::Generic => Self::MicroPython,
|
|
DeviceKind::Arduino => Self::Arduino,
|
|
DeviceKind::Nucleo => Self::Nucleus,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for DeviceRuntime {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::MicroPython => write!(f, "MicroPython"),
|
|
Self::CircuitPython => write!(f, "CircuitPython"),
|
|
Self::Arduino => write!(f, "Arduino"),
|
|
Self::Nucleus => write!(f, "Nucleus"),
|
|
Self::Linux => write!(f, "Linux"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── DeviceKind ────────────────────────────────────────────────────────────────
|
|
|
|
/// The category of a discovered hardware device.
|
|
///
|
|
/// Derived from USB Vendor ID or, for unknown VIDs, from a successful
|
|
/// ping handshake (which yields `Generic`).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum DeviceKind {
|
|
/// Raspberry Pi Pico / Pico W (VID `0x2E8A`).
|
|
Pico,
|
|
/// Arduino Uno, Mega, etc. (VID `0x2341`).
|
|
Arduino,
|
|
/// ESP32 via CP2102 bridge (VID `0x10C4`).
|
|
Esp32,
|
|
/// STM32 Nucleo (VID `0x0483`).
|
|
Nucleo,
|
|
/// Unknown VID that passed the ZeroClaw firmware ping handshake.
|
|
Generic,
|
|
}
|
|
|
|
impl DeviceKind {
|
|
/// Derive the device kind from a USB Vendor ID.
|
|
/// Returns `None` if the VID is unknown (0 or unrecognised).
|
|
pub fn from_vid(vid: u16) -> Option<Self> {
|
|
match vid {
|
|
0x2e8a => Some(Self::Pico),
|
|
0x2341 => Some(Self::Arduino),
|
|
0x10c4 => Some(Self::Esp32),
|
|
0x0483 => Some(Self::Nucleo),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for DeviceKind {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Pico => write!(f, "pico"),
|
|
Self::Arduino => write!(f, "arduino"),
|
|
Self::Esp32 => write!(f, "esp32"),
|
|
Self::Nucleo => write!(f, "nucleo"),
|
|
Self::Generic => write!(f, "generic"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Capability flags for a connected device.
|
|
///
|
|
/// Populated from device handshake or static board metadata.
|
|
/// Tools can check capabilities before attempting unsupported operations.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct DeviceCapabilities {
|
|
pub gpio: bool,
|
|
pub i2c: bool,
|
|
pub spi: bool,
|
|
pub swd: bool,
|
|
pub uart: bool,
|
|
pub adc: bool,
|
|
pub pwm: bool,
|
|
}
|
|
|
|
/// A discovered and registered hardware device.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Device {
|
|
/// Stable session alias (e.g. `"pico0"`, `"arduino0"`, `"nucleo0"`).
|
|
pub alias: String,
|
|
/// Board name from registry (e.g. `"raspberry-pi-pico"`, `"arduino-uno"`).
|
|
pub board_name: String,
|
|
/// Device category derived from VID or ping handshake.
|
|
pub kind: DeviceKind,
|
|
/// Software runtime that determines how code is deployed/executed.
|
|
pub runtime: DeviceRuntime,
|
|
/// USB Vendor ID (if USB-connected).
|
|
pub vid: Option<u16>,
|
|
/// USB Product ID (if USB-connected).
|
|
pub pid: Option<u16>,
|
|
/// Raw device path (e.g. `"/dev/ttyACM0"`) — internal use only.
|
|
/// Tools MUST NOT use this directly; always go through Transport.
|
|
pub device_path: Option<String>,
|
|
/// Architecture description (e.g. `"ARM Cortex-M0+"`).
|
|
pub architecture: Option<String>,
|
|
/// Firmware identifier reported by device during ping handshake.
|
|
pub firmware: Option<String>,
|
|
}
|
|
|
|
impl Device {
|
|
/// Convenience accessor — same as `device_path` (matches the Phase 2 spec naming).
|
|
pub fn port(&self) -> Option<&str> {
|
|
self.device_path.as_deref()
|
|
}
|
|
}
|
|
|
|
/// Context passed to hardware tools during execution.
|
|
///
|
|
/// Provides the tool with access to the device identity, transport layer,
|
|
/// and capability flags without the tool managing connections itself.
|
|
pub struct DeviceContext {
|
|
/// The device this tool is operating on.
|
|
pub device: Arc<Device>,
|
|
/// Transport for sending commands to the device.
|
|
pub transport: Arc<dyn Transport>,
|
|
/// Device capabilities (gpio, i2c, spi, etc.).
|
|
pub capabilities: DeviceCapabilities,
|
|
}
|
|
|
|
/// A registered device entry with its transport and capabilities.
|
|
struct RegisteredDevice {
|
|
device: Arc<Device>,
|
|
transport: Option<Arc<dyn Transport>>,
|
|
capabilities: DeviceCapabilities,
|
|
}
|
|
|
|
/// Summary string returned by [`DeviceRegistry::prompt_summary`] when no
|
|
/// devices are registered. Exported so callers can compare against it without
|
|
/// duplicating the literal.
|
|
pub const NO_HW_DEVICES_SUMMARY: &str = "No hardware devices connected.";
|
|
|
|
/// Registry of discovered devices with stable session aliases.
|
|
///
|
|
/// - Scans at startup (via `hardware::discover`)
|
|
/// - Assigns aliases: `pico0`, `pico1`, `arduino0`, `nucleo0`, `device0`, etc.
|
|
/// - Provides alias-based lookup for tool dispatch
|
|
/// - Generates prompt summaries for LLM context
|
|
pub struct DeviceRegistry {
|
|
devices: HashMap<String, RegisteredDevice>,
|
|
alias_counters: HashMap<String, u32>,
|
|
}
|
|
|
|
impl DeviceRegistry {
|
|
/// Create an empty registry.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
devices: HashMap::new(),
|
|
alias_counters: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Register a discovered device and assign a stable alias.
|
|
///
|
|
/// Returns the assigned alias (e.g. `"pico0"`).
|
|
pub fn register(
|
|
&mut self,
|
|
board_name: &str,
|
|
vid: Option<u16>,
|
|
pid: Option<u16>,
|
|
device_path: Option<String>,
|
|
architecture: Option<String>,
|
|
) -> String {
|
|
let prefix = alias_prefix(board_name);
|
|
let counter = self.alias_counters.entry(prefix.clone()).or_insert(0);
|
|
let alias = format!("{}{}", prefix, counter);
|
|
*counter += 1;
|
|
|
|
let kind = vid
|
|
.and_then(DeviceKind::from_vid)
|
|
.unwrap_or(DeviceKind::Generic);
|
|
let runtime = DeviceRuntime::from_kind(&kind);
|
|
|
|
let device = Arc::new(Device {
|
|
alias: alias.clone(),
|
|
board_name: board_name.to_string(),
|
|
kind,
|
|
runtime,
|
|
vid,
|
|
pid,
|
|
device_path,
|
|
architecture,
|
|
firmware: None,
|
|
});
|
|
|
|
self.devices.insert(
|
|
alias.clone(),
|
|
RegisteredDevice {
|
|
device,
|
|
transport: None,
|
|
capabilities: DeviceCapabilities::default(),
|
|
},
|
|
);
|
|
|
|
alias
|
|
}
|
|
|
|
/// Attach a transport and capabilities to a previously registered device.
|
|
///
|
|
/// Returns `Err` when `alias` is not found in the registry (should not
|
|
/// happen in normal usage because callers pass aliases from `register`).
|
|
pub fn attach_transport(
|
|
&mut self,
|
|
alias: &str,
|
|
transport: Arc<dyn Transport>,
|
|
capabilities: DeviceCapabilities,
|
|
) -> anyhow::Result<()> {
|
|
if let Some(entry) = self.devices.get_mut(alias) {
|
|
entry.transport = Some(transport);
|
|
entry.capabilities = capabilities;
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("unknown device alias: {}", alias))
|
|
}
|
|
}
|
|
|
|
/// Look up a device by alias.
|
|
pub fn get_device(&self, alias: &str) -> Option<Arc<Device>> {
|
|
self.devices.get(alias).map(|e| e.device.clone())
|
|
}
|
|
|
|
/// Build a `DeviceContext` for a device by alias.
|
|
///
|
|
/// Returns `None` if the alias is unknown or no transport is attached.
|
|
pub fn context(&self, alias: &str) -> Option<DeviceContext> {
|
|
self.devices.get(alias).and_then(|e| {
|
|
e.transport.as_ref().map(|t| DeviceContext {
|
|
device: e.device.clone(),
|
|
transport: t.clone(),
|
|
capabilities: e.capabilities.clone(),
|
|
})
|
|
})
|
|
}
|
|
|
|
/// List all registered device aliases.
|
|
pub fn aliases(&self) -> Vec<&str> {
|
|
self.devices.keys().map(|s| s.as_str()).collect()
|
|
}
|
|
|
|
/// Return a summary of connected devices for the LLM system prompt.
|
|
pub fn prompt_summary(&self) -> String {
|
|
if self.devices.is_empty() {
|
|
return NO_HW_DEVICES_SUMMARY.to_string();
|
|
}
|
|
|
|
let mut lines = vec!["Connected devices:".to_string()];
|
|
let mut sorted_aliases: Vec<&String> = self.devices.keys().collect();
|
|
sorted_aliases.sort();
|
|
for alias in sorted_aliases {
|
|
let entry = &self.devices[alias];
|
|
let status = entry
|
|
.transport
|
|
.as_ref()
|
|
.map(|t| {
|
|
if t.is_connected() {
|
|
"connected"
|
|
} else {
|
|
"disconnected"
|
|
}
|
|
})
|
|
.unwrap_or("no transport");
|
|
let arch = entry
|
|
.device
|
|
.architecture
|
|
.as_deref()
|
|
.unwrap_or("unknown arch");
|
|
lines.push(format!(
|
|
" {} — {} ({}) [{}]",
|
|
alias, entry.device.board_name, arch, status
|
|
));
|
|
}
|
|
lines.join("\n")
|
|
}
|
|
|
|
/// Resolve a GPIO-capable device alias from tool arguments.
|
|
///
|
|
/// If `args["device"]` is provided, uses that alias directly.
|
|
/// Otherwise, auto-selects the single GPIO-capable device, returning an
|
|
/// error description if zero or multiple GPIO devices are available.
|
|
///
|
|
/// On success returns `(alias, DeviceContext)` — both are owned / Arc-based
|
|
/// so the caller can drop the registry lock before doing async I/O.
|
|
pub fn resolve_gpio_device(
|
|
&self,
|
|
args: &serde_json::Value,
|
|
) -> Result<(String, DeviceContext), String> {
|
|
let device_alias: String = match args.get("device").and_then(|v| v.as_str()) {
|
|
Some(a) => a.to_string(),
|
|
None => {
|
|
let gpio_aliases: Vec<String> = self
|
|
.aliases()
|
|
.into_iter()
|
|
.filter(|a| {
|
|
self.context(a)
|
|
.map(|c| c.capabilities.gpio)
|
|
.unwrap_or(false)
|
|
})
|
|
.map(|a| a.to_string())
|
|
.collect();
|
|
match gpio_aliases.as_slice() {
|
|
[single] => single.clone(),
|
|
[] => {
|
|
return Err("no GPIO-capable device found; specify \"device\" parameter"
|
|
.to_string());
|
|
}
|
|
_ => {
|
|
return Err(format!(
|
|
"multiple devices available ({}); specify \"device\" parameter",
|
|
gpio_aliases.join(", ")
|
|
));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let ctx = self.context(&device_alias).ok_or_else(|| {
|
|
format!(
|
|
"device '{}' not found or has no transport attached",
|
|
device_alias
|
|
)
|
|
})?;
|
|
|
|
// Verify the device advertises GPIO capability.
|
|
if !ctx.capabilities.gpio {
|
|
return Err(format!(
|
|
"device '{}' does not support GPIO; specify a GPIO-capable device",
|
|
device_alias
|
|
));
|
|
}
|
|
|
|
Ok((device_alias, ctx))
|
|
}
|
|
|
|
/// Number of registered devices.
|
|
pub fn len(&self) -> usize {
|
|
self.devices.len()
|
|
}
|
|
|
|
/// Whether the registry is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.devices.is_empty()
|
|
}
|
|
|
|
/// Look up a device by alias (alias for `get_device` matching the Phase 2 spec).
|
|
pub fn get(&self, alias: &str) -> Option<Arc<Device>> {
|
|
self.get_device(alias)
|
|
}
|
|
|
|
/// Return all registered devices.
|
|
pub fn all(&self) -> Vec<Arc<Device>> {
|
|
self.devices.values().map(|e| e.device.clone()).collect()
|
|
}
|
|
|
|
/// One-line summary per device: `"pico0: raspberry-pi-pico /dev/ttyACM0"`.
|
|
///
|
|
/// Suitable for CLI output and debug logging.
|
|
pub fn summary(&self) -> String {
|
|
if self.devices.is_empty() {
|
|
return String::new();
|
|
}
|
|
let mut lines: Vec<String> = self
|
|
.devices
|
|
.values()
|
|
.map(|e| {
|
|
let path = e.device.port().unwrap_or("(native)");
|
|
format!("{}: {} {}", e.device.alias, e.device.board_name, path)
|
|
})
|
|
.collect();
|
|
lines.sort(); // deterministic for tests
|
|
lines.join("\n")
|
|
}
|
|
|
|
/// Discover all connected serial devices and populate the registry.
|
|
///
|
|
/// Steps:
|
|
/// 1. Call `discover::scan_serial_devices()` to enumerate port paths + VID/PID.
|
|
/// 2. For each device with a recognised VID: register and attach a transport.
|
|
/// 3. For unknown VID (`0`): attempt a 300 ms ping handshake; register only
|
|
/// if the device responds with ZeroClaw firmware.
|
|
/// 4. Return the populated registry.
|
|
///
|
|
/// Returns an empty registry when no devices are found or the `hardware`
|
|
/// feature is disabled.
|
|
#[cfg(feature = "hardware")]
|
|
pub async fn discover() -> Self {
|
|
use super::{
|
|
discover::scan_serial_devices,
|
|
serial::{HardwareSerialTransport, DEFAULT_BAUD},
|
|
};
|
|
|
|
let mut registry = Self::new();
|
|
|
|
for info in scan_serial_devices() {
|
|
let is_known_vid = info.vid != 0;
|
|
|
|
// For unknown VIDs, run the ping handshake before registering.
|
|
// This avoids registering random USB-serial adapters.
|
|
// If the probe succeeds we reuse the same transport instance below.
|
|
let probe_transport = if !is_known_vid {
|
|
let probe = HardwareSerialTransport::new(&info.port_path, DEFAULT_BAUD);
|
|
if !probe.ping_handshake().await {
|
|
tracing::debug!(
|
|
port = %info.port_path,
|
|
"skipping unknown device: no ZeroClaw firmware response"
|
|
);
|
|
continue;
|
|
}
|
|
Some(probe)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let board_name = info.board_name.as_deref().unwrap_or("unknown").to_string();
|
|
|
|
let alias = registry.register(
|
|
&board_name,
|
|
if info.vid != 0 { Some(info.vid) } else { None },
|
|
if info.pid != 0 { Some(info.pid) } else { None },
|
|
Some(info.port_path.clone()),
|
|
info.architecture,
|
|
);
|
|
|
|
// For unknown-VID devices that passed ping: mark as Generic.
|
|
// (register() will have already set kind = Generic for vid=None)
|
|
|
|
let transport: Arc<dyn super::transport::Transport> =
|
|
if let Some(probe) = probe_transport {
|
|
Arc::new(probe)
|
|
} else {
|
|
Arc::new(HardwareSerialTransport::new(&info.port_path, DEFAULT_BAUD))
|
|
};
|
|
let caps = DeviceCapabilities {
|
|
gpio: true, // assume GPIO; Phase 3 will populate via capabilities handshake
|
|
..DeviceCapabilities::default()
|
|
};
|
|
registry.attach_transport(&alias, transport, caps)
|
|
.unwrap_or_else(|e| tracing::warn!(alias = %alias, err = %e, "attach_transport: unexpected unknown alias"));
|
|
|
|
tracing::info!(
|
|
alias = %alias,
|
|
port = %info.port_path,
|
|
vid = %info.vid,
|
|
"device registered"
|
|
);
|
|
}
|
|
|
|
registry
|
|
}
|
|
}
|
|
|
|
impl DeviceRegistry {
|
|
/// Reconnect a device after reboot/reflash.
|
|
///
|
|
/// Drops the old transport, creates a fresh [`HardwareSerialTransport`] for
|
|
/// the given (or existing) port path, runs the ping handshake to confirm
|
|
/// ZeroClaw firmware is alive, and re-attaches the transport.
|
|
///
|
|
/// Pass `new_port` when the OS assigned a different path after reboot;
|
|
/// pass `None` to reuse the device's current path.
|
|
#[cfg(feature = "hardware")]
|
|
pub async fn reconnect(&mut self, alias: &str, new_port: Option<&str>) -> anyhow::Result<()> {
|
|
use super::serial::{HardwareSerialTransport, DEFAULT_BAUD};
|
|
|
|
let entry = self
|
|
.devices
|
|
.get_mut(alias)
|
|
.ok_or_else(|| anyhow::anyhow!("unknown device alias: {alias}"))?;
|
|
|
|
// Determine the port path — prefer the caller's override.
|
|
let port_path = match new_port {
|
|
Some(p) => {
|
|
// Update the device record with the new path.
|
|
let mut updated = (*entry.device).clone();
|
|
updated.device_path = Some(p.to_string());
|
|
entry.device = Arc::new(updated);
|
|
p.to_string()
|
|
}
|
|
None => entry
|
|
.device
|
|
.device_path
|
|
.clone()
|
|
.ok_or_else(|| anyhow::anyhow!("device {alias} has no port path"))?,
|
|
};
|
|
|
|
// Drop the stale transport.
|
|
entry.transport = None;
|
|
|
|
// Create a fresh transport and verify firmware is alive.
|
|
let transport = HardwareSerialTransport::new(&port_path, DEFAULT_BAUD);
|
|
if !transport.ping_handshake().await {
|
|
anyhow::bail!(
|
|
"ping handshake failed after reconnect on {port_path} — \
|
|
firmware may not be running"
|
|
);
|
|
}
|
|
|
|
entry.transport = Some(Arc::new(transport) as Arc<dyn super::transport::Transport>);
|
|
entry.capabilities.gpio = true;
|
|
|
|
tracing::info!(alias = %alias, port = %port_path, "device reconnected");
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Default for DeviceRegistry {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Derive alias prefix from board name.
|
|
fn alias_prefix(board_name: &str) -> String {
|
|
match board_name {
|
|
s if s.starts_with("raspberry-pi-pico") || s.starts_with("pico") => "pico".to_string(),
|
|
s if s.starts_with("arduino") => "arduino".to_string(),
|
|
s if s.starts_with("esp32") || s.starts_with("esp") => "esp".to_string(),
|
|
s if s.starts_with("nucleo") || s.starts_with("stm32") => "nucleo".to_string(),
|
|
s if s.starts_with("rpi") || s == "raspberry-pi" => "rpi".to_string(),
|
|
_ => "device".to_string(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn alias_prefix_pico_variants() {
|
|
assert_eq!(alias_prefix("raspberry-pi-pico"), "pico");
|
|
assert_eq!(alias_prefix("pico-w"), "pico");
|
|
assert_eq!(alias_prefix("pico"), "pico");
|
|
}
|
|
|
|
#[test]
|
|
fn alias_prefix_arduino() {
|
|
assert_eq!(alias_prefix("arduino-uno"), "arduino");
|
|
assert_eq!(alias_prefix("arduino-mega"), "arduino");
|
|
}
|
|
|
|
#[test]
|
|
fn alias_prefix_esp() {
|
|
assert_eq!(alias_prefix("esp32"), "esp");
|
|
assert_eq!(alias_prefix("esp32-s3"), "esp");
|
|
}
|
|
|
|
#[test]
|
|
fn alias_prefix_nucleo() {
|
|
assert_eq!(alias_prefix("nucleo-f401re"), "nucleo");
|
|
assert_eq!(alias_prefix("stm32-discovery"), "nucleo");
|
|
}
|
|
|
|
#[test]
|
|
fn alias_prefix_rpi() {
|
|
assert_eq!(alias_prefix("rpi-gpio"), "rpi");
|
|
assert_eq!(alias_prefix("raspberry-pi"), "rpi");
|
|
}
|
|
|
|
#[test]
|
|
fn alias_prefix_unknown() {
|
|
assert_eq!(alias_prefix("custom-board"), "device");
|
|
}
|
|
|
|
#[test]
|
|
fn registry_assigns_sequential_aliases() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let a1 = reg.register("raspberry-pi-pico", Some(0x2E8A), Some(0x000A), None, None);
|
|
let a2 = reg.register("raspberry-pi-pico", Some(0x2E8A), Some(0x000A), None, None);
|
|
let a3 = reg.register("arduino-uno", Some(0x2341), Some(0x0043), None, None);
|
|
|
|
assert_eq!(a1, "pico0");
|
|
assert_eq!(a2, "pico1");
|
|
assert_eq!(a3, "arduino0");
|
|
assert_eq!(reg.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_get_device_by_alias() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let alias = reg.register(
|
|
"nucleo-f401re",
|
|
Some(0x0483),
|
|
Some(0x374B),
|
|
Some("/dev/ttyACM0".to_string()),
|
|
Some("ARM Cortex-M4".to_string()),
|
|
);
|
|
|
|
let device = reg.get_device(&alias).unwrap();
|
|
assert_eq!(device.alias, "nucleo0");
|
|
assert_eq!(device.board_name, "nucleo-f401re");
|
|
assert_eq!(device.vid, Some(0x0483));
|
|
assert_eq!(device.architecture.as_deref(), Some("ARM Cortex-M4"));
|
|
}
|
|
|
|
#[test]
|
|
fn registry_unknown_alias_returns_none() {
|
|
let reg = DeviceRegistry::new();
|
|
assert!(reg.get_device("nonexistent").is_none());
|
|
assert!(reg.context("nonexistent").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn registry_context_none_without_transport() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let alias = reg.register("pico", None, None, None, None);
|
|
// No transport attached → context returns None.
|
|
assert!(reg.context(&alias).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn registry_prompt_summary_empty() {
|
|
let reg = DeviceRegistry::new();
|
|
assert_eq!(reg.prompt_summary(), NO_HW_DEVICES_SUMMARY);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_prompt_summary_with_devices() {
|
|
let mut reg = DeviceRegistry::new();
|
|
reg.register(
|
|
"raspberry-pi-pico",
|
|
Some(0x2E8A),
|
|
None,
|
|
None,
|
|
Some("ARM Cortex-M0+".to_string()),
|
|
);
|
|
let summary = reg.prompt_summary();
|
|
assert!(summary.contains("pico0"));
|
|
assert!(summary.contains("raspberry-pi-pico"));
|
|
assert!(summary.contains("ARM Cortex-M0+"));
|
|
assert!(summary.contains("no transport"));
|
|
}
|
|
|
|
#[test]
|
|
fn device_capabilities_default_all_false() {
|
|
let caps = DeviceCapabilities::default();
|
|
assert!(!caps.gpio);
|
|
assert!(!caps.i2c);
|
|
assert!(!caps.spi);
|
|
assert!(!caps.swd);
|
|
assert!(!caps.uart);
|
|
assert!(!caps.adc);
|
|
assert!(!caps.pwm);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_default_is_empty() {
|
|
let reg = DeviceRegistry::default();
|
|
assert!(reg.is_empty());
|
|
assert_eq!(reg.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_aliases_returns_all() {
|
|
let mut reg = DeviceRegistry::new();
|
|
reg.register("pico", None, None, None, None);
|
|
reg.register("arduino-uno", None, None, None, None);
|
|
let mut aliases = reg.aliases();
|
|
aliases.sort();
|
|
assert_eq!(aliases, vec!["arduino0", "pico0"]);
|
|
}
|
|
|
|
// ── Phase 2 new tests ────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn device_kind_from_vid_known() {
|
|
assert_eq!(DeviceKind::from_vid(0x2e8a), Some(DeviceKind::Pico));
|
|
assert_eq!(DeviceKind::from_vid(0x2341), Some(DeviceKind::Arduino));
|
|
assert_eq!(DeviceKind::from_vid(0x10c4), Some(DeviceKind::Esp32));
|
|
assert_eq!(DeviceKind::from_vid(0x0483), Some(DeviceKind::Nucleo));
|
|
}
|
|
|
|
#[test]
|
|
fn device_kind_from_vid_unknown() {
|
|
assert_eq!(DeviceKind::from_vid(0x0000), None);
|
|
assert_eq!(DeviceKind::from_vid(0xffff), None);
|
|
}
|
|
|
|
#[test]
|
|
fn device_kind_display() {
|
|
assert_eq!(DeviceKind::Pico.to_string(), "pico");
|
|
assert_eq!(DeviceKind::Arduino.to_string(), "arduino");
|
|
assert_eq!(DeviceKind::Esp32.to_string(), "esp32");
|
|
assert_eq!(DeviceKind::Nucleo.to_string(), "nucleo");
|
|
assert_eq!(DeviceKind::Generic.to_string(), "generic");
|
|
}
|
|
|
|
#[test]
|
|
fn register_sets_kind_from_vid() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let a = reg.register("raspberry-pi-pico", Some(0x2e8a), Some(0x000a), None, None);
|
|
assert_eq!(reg.get(&a).unwrap().kind, DeviceKind::Pico);
|
|
|
|
let b = reg.register("arduino-uno", Some(0x2341), Some(0x0043), None, None);
|
|
assert_eq!(reg.get(&b).unwrap().kind, DeviceKind::Arduino);
|
|
|
|
let c = reg.register("unknown-device", None, None, None, None);
|
|
assert_eq!(reg.get(&c).unwrap().kind, DeviceKind::Generic);
|
|
}
|
|
|
|
#[test]
|
|
fn device_port_returns_device_path() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let alias = reg.register(
|
|
"raspberry-pi-pico",
|
|
Some(0x2e8a),
|
|
None,
|
|
Some("/dev/ttyACM0".to_string()),
|
|
None,
|
|
);
|
|
let device = reg.get(&alias).unwrap();
|
|
assert_eq!(device.port(), Some("/dev/ttyACM0"));
|
|
}
|
|
|
|
#[test]
|
|
fn device_port_none_without_path() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let alias = reg.register("pico", None, None, None, None);
|
|
assert!(reg.get(&alias).unwrap().port().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn registry_get_is_alias_for_get_device() {
|
|
let mut reg = DeviceRegistry::new();
|
|
let alias = reg.register("raspberry-pi-pico", Some(0x2e8a), None, None, None);
|
|
let via_get = reg.get(&alias);
|
|
let via_get_device = reg.get_device(&alias);
|
|
assert!(via_get.is_some());
|
|
assert!(via_get_device.is_some());
|
|
assert_eq!(via_get.unwrap().alias, via_get_device.unwrap().alias);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_all_returns_every_device() {
|
|
let mut reg = DeviceRegistry::new();
|
|
reg.register("raspberry-pi-pico", Some(0x2e8a), None, None, None);
|
|
reg.register("arduino-uno", Some(0x2341), None, None, None);
|
|
assert_eq!(reg.all().len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn registry_summary_one_liner_per_device() {
|
|
let mut reg = DeviceRegistry::new();
|
|
reg.register(
|
|
"raspberry-pi-pico",
|
|
Some(0x2e8a),
|
|
None,
|
|
Some("/dev/ttyACM0".to_string()),
|
|
None,
|
|
);
|
|
let s = reg.summary();
|
|
assert!(s.contains("pico0"));
|
|
assert!(s.contains("raspberry-pi-pico"));
|
|
assert!(s.contains("/dev/ttyACM0"));
|
|
}
|
|
|
|
#[test]
|
|
fn registry_summary_empty_when_no_devices() {
|
|
let reg = DeviceRegistry::new();
|
|
assert_eq!(reg.summary(), "");
|
|
}
|
|
}
|