mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Node command handler
//!
//! Runs bundled nodes directly. This is an internal command used by standalone mode
//! to avoid requiring users to build their project.
//!
//! Requires the `bundled-nodes` feature to be enabled.

use crate::commands::NodeArgs;
use anyhow::Result;

#[cfg(feature = "bundled-nodes")]
use crate::services::ModelService;
#[cfg(feature = "bundled-nodes")]
use anyhow::Context;
#[cfg(feature = "bundled-nodes")]
use std::path::PathBuf;
#[cfg(feature = "bundled-nodes")]
use tracing::{info, warn};

/// Handle the node command - runs a bundled node directly
#[cfg(not(feature = "bundled-nodes"))]
pub async fn handle_node(_args: &NodeArgs) -> Result<()> {
    anyhow::bail!(
        "Bundled nodes are not available in this build.\n\
         Install with bundled nodes: cargo install mecha10-cli --features bundled-nodes\n\
         Or use: mecha10 dev (which builds your project's nodes)"
    )
}

/// Available bundled nodes (base set)
#[cfg(all(feature = "bundled-nodes", not(feature = "vision")))]
const BUNDLED_NODES: &[&str] = &[
    "behavior-executor",
    "diagnostics-node",
    "imu",
    "listener",
    "llm-command",
    "motor",
    "simulation-bridge",
    "speaker",
    "teleop",
    "websocket-bridge",
];

/// Available bundled nodes (with vision support)
#[cfg(all(feature = "bundled-nodes", feature = "vision"))]
const BUNDLED_NODES: &[&str] = &[
    "behavior-executor",
    "diagnostics-node",
    "image-classifier",
    "imu",
    "listener",
    "llm-command",
    "motor",
    "object-detector",
    "simulation-bridge",
    "speaker",
    "teleop",
    "websocket-bridge",
];

/// Handle the node command - runs a bundled node directly
#[cfg(feature = "bundled-nodes")]
pub async fn handle_node(args: &NodeArgs) -> Result<()> {
    let name = &args.name;

    if !BUNDLED_NODES.contains(&name.as_str()) {
        anyhow::bail!(
            "Unknown node: '{}'. Available nodes: {}",
            name,
            BUNDLED_NODES.join(", ")
        );
    }

    // Ensure required models are available before running vision nodes
    #[cfg(feature = "vision")]
    {
        if let Some(model_name) = get_required_model(name) {
            ensure_model_available(model_name).await?;
        }
    }

    info!("Starting bundled node: {}", name);
    run_bundled_node(name).await
}

/// Get the model name required by a node (if any)
#[cfg(all(feature = "bundled-nodes", feature = "vision"))]
fn get_required_model(node_name: &str) -> Option<&'static str> {
    match node_name {
        "object-detector" => Some("yolov8n"),
        "image-classifier" => Some("mobilenet-v2"),
        _ => None,
    }
}

/// Ensure a model is available, downloading if necessary
#[cfg(all(feature = "bundled-nodes", feature = "vision"))]
async fn ensure_model_available(model_name: &str) -> Result<()> {
    let model_path = PathBuf::from(format!("models/{}/model.onnx", model_name));

    if model_path.exists() {
        info!("Model '{}' found at {}", model_name, model_path.display());
        return Ok(());
    }

    info!("Model '{}' not found, downloading...", model_name);

    // Try to create model service and pull the model
    let model_service = ModelService::with_models_dir("models".into()).context("Failed to initialize model service")?;

    // Create a simple spinner for progress
    let pb = indicatif::ProgressBar::new_spinner();
    pb.set_message(format!("Downloading model '{}'...", model_name));
    pb.enable_steady_tick(std::time::Duration::from_millis(100));

    match model_service.pull(model_name, Some(&pb)).await {
        Ok(_) => {
            pb.finish_with_message(format!("Model '{}' downloaded successfully", model_name));
            info!("Model '{}' ready", model_name);
            Ok(())
        }
        Err(e) => {
            pb.finish_with_message(format!("Failed to download model '{}'", model_name));
            warn!(
                "Failed to download model '{}': {}. Node may fail to start.",
                model_name, e
            );
            // Don't fail here - let the node report its own error
            Ok(())
        }
    }
}

/// Run a bundled node by name
#[cfg(feature = "bundled-nodes")]
async fn run_bundled_node(name: &str) -> Result<()> {
    match name {
        "behavior-executor" => mecha10_nodes_behavior_executor::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        "diagnostics-node" => mecha10_nodes_diagnostics::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        #[cfg(feature = "vision")]
        "image-classifier" => mecha10_nodes_image_classifier::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        "imu" => mecha10_nodes_imu::run().await.map_err(|e| anyhow::anyhow!("{}", e)),
        "listener" => mecha10_nodes_listener::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        "llm-command" => mecha10_nodes_llm_command::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        "motor" => mecha10_nodes_motor::run().await.map_err(|e| anyhow::anyhow!("{}", e)),
        #[cfg(feature = "vision")]
        "object-detector" => mecha10_nodes_object_detector::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        "simulation-bridge" => mecha10_nodes_simulation_bridge::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        "speaker" => mecha10_nodes_speaker::run().await.map_err(|e| anyhow::anyhow!("{}", e)),
        "teleop" => mecha10_nodes_teleop::run().await.map_err(|e| anyhow::anyhow!("{}", e)),
        "websocket-bridge" => mecha10_nodes_websocket_bridge::run()
            .await
            .map_err(|e| anyhow::anyhow!("{}", e)),
        _ => Err(anyhow::anyhow!("Unknown node: {}", name)),
    }
}