jumperless-mcp 0.1.0

MCP server for the Jumperless V5 — persistent USB-serial bridge exposing the firmware API to LLMs
//! Health status types for the [`crate::Subsystem`] lifecycle.
//!
//! [`HealthStatus`] is returned by [`crate::Subsystem::health_check`] and
//! consumed by federation gateways to make routing decisions.

use crate::base::cli::McpTransport;
use crate::base::subsystem::RingId;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

/// Rich health snapshot for a [`crate::Subsystem`].
///
/// Federation gateways act on [`HealthStatus::level`] for routing decisions;
/// the other fields feed observability dashboards.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
    /// Routing-decision-relevant classification. Federation gateways key on this.
    pub level: HealthLevel,

    /// Unix epoch timestamp in milliseconds when the last successful roundtrip
    /// to the underlying device completed. Use
    /// `SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64`
    /// to populate. Zero means "never observed."
    pub last_seen_unix_ms: u64,

    /// Last roundtrip latency, if the subsystem can measure it.
    /// `None` for tool-only subsystems without a hardware ping concept.
    pub latency_ms: Option<u64>,

    /// Crate version of the subsystem MCP (from `env!("CARGO_PKG_VERSION")`).
    pub version: String,

    /// Carabiner ring this subsystem belongs to, mirroring
    /// [`crate::Subsystem::carabiner_ring`].
    pub ring: Option<RingId>,

    /// Subsystem-specific diagnostic blob.
    ///
    /// Each MCP can populate this with its own fields without touching the
    /// base struct. Examples:
    /// - Jumperless: `{"port": "COM3", "baud": 115200, "raw_repl": "ok"}`
    /// - ESP32-P4:   `{"port": "COM5", "firmware_hash": "abc123", "free_heap_kb": 152}`
    /// - HuskyLens:  `{"port": "COM7", "model": "v2", "fps": 30}`
    pub subsystem_specific: serde_json::Value,
}

impl HealthStatus {
    /// Convenience helper: current Unix epoch milliseconds.
    ///
    /// Use to populate [`last_seen_unix_ms`](Self::last_seen_unix_ms):
    /// ```rust,ignore
    /// last_seen_unix_ms: HealthStatus::now_unix_ms(),
    /// ```
    pub fn now_unix_ms() -> u64 {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64
    }
}

/// Three-tier health classification for federation routing decisions.
///
/// Derives `PartialOrd`/`Ord` so federation aggregation is a one-liner:
/// ```rust,ignore
/// self.subsystems.iter().map(|s| s.health_level()).max().unwrap_or(HealthLevel::Healthy)
/// ```
/// Declaration order defines ordinal: `Healthy < Degraded < Unhealthy`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum HealthLevel {
    /// Fully operational. Federation routes normally.
    Healthy,

    /// Partially functional. Federation may still route but should warn or
    /// fall back to alternatives if available.
    Degraded { reason: String },

    /// Not functional. Federation drops from active routing (still visible
    /// in topology dumps for diagnostic purposes).
    Unhealthy { reason: String },
}

/// Structured self-description returned by [`crate::Subsystem::describe_self`].
/// Used by gateways to build aggregated topology views.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubsystemDescriptor {
    /// Stable identifier (matches [`crate::Subsystem::name`]).
    pub name: String,
    /// Number of tools this subsystem currently exposes.
    pub tool_count: usize,
    /// Carabiner ring membership.
    pub ring: Option<RingId>,
    /// Transport this subsystem is serving on.
    pub transport: McpTransport,
    /// Crate version (`env!("CARGO_PKG_VERSION")`).
    pub version: String,
}