mecha10-core 0.8.0

Mecha10 core runtime and foundational types
Documentation
//! Behavior Interrupt Trigger
//!
//! Reusable system for nodes to interrupt and resume behavior tree execution.
//!
//! # Overview
//!
//! This module provides a configurable interrupt trigger that any node can use
//! to pause autonomous behaviors when issuing direct commands (motor, navigation, etc.).
//!
//! # Features
//!
//! - **Interrupt Modes**: interrupt-only, interrupt-with-auto-resume, disabled
//! - **Auto-resume**: Automatically resume after timeout or command completion
//! - **Completion Detection**: Wait for specific topic before resuming
//! - **Configurable Timeouts**: Control how long to wait before auto-resume
//! - **Source Tracking**: Know which node triggered the interrupt
//!
//! # Example Usage
//!
//! ```rust
//! use mecha10_core::behavior_interrupt::{BehaviorInterruptTrigger, BehaviorInterruptConfig};
//! use mecha10_core::prelude::*;
//!
//! # async fn example(ctx: &Context) -> Result<()> {
//! // Create trigger from config
//! let config = BehaviorInterruptConfig {
//!     enabled: true,
//!     mode: InterruptMode::InterruptWithAutoResume,
//!     timeout_secs: 30,
//!     await_completion: true,
//!     completion_topic: Some("/motor/status".to_string()),
//!     ..Default::default()
//! };
//!
//! let mut trigger = BehaviorInterruptTrigger::new("llm-command", config);
//!
//! // Interrupt behavior tree before sending motor command
//! trigger.interrupt(&ctx).await?;
//!
//! // ... send motor command ...
//!
//! // Manually resume (or let auto-resume handle it)
//! trigger.resume(&ctx).await?;
//! # Ok(())
//! # }
//! ```

use crate::context::Context;
use crate::messages::Message;
use crate::prelude::now_micros;
use crate::topics::Topic;
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use tracing::{debug, info, warn};

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum InterruptMode {
    Disabled,

    #[default]
    InterruptOnly,

    InterruptWithAutoResume,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BehaviorInterruptConfig {
    #[serde(default = "default_enabled")]
    #[schemars(example = default_enabled())]
    pub enabled: bool,

    #[serde(default)]
    #[schemars(example = InterruptMode::InterruptOnly)]
    pub mode: InterruptMode,

    #[serde(default = "default_timeout_secs")]
    #[schemars(example = default_timeout_secs())]
    pub timeout_secs: u64,

    #[serde(default)]
    #[schemars(example = false)]
    pub await_completion: bool,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[schemars(example = Some("/behavior/completion".to_string()))]
    pub completion_topic: Option<String>,

    #[serde(default = "default_control_topic")]
    #[schemars(example = default_control_topic())]
    pub control_topic: String,
}

impl Default for BehaviorInterruptConfig {
    fn default() -> Self {
        Self {
            enabled: default_enabled(),
            mode: InterruptMode::default(),
            timeout_secs: default_timeout_secs(),
            await_completion: false,
            completion_topic: None,
            control_topic: default_control_topic(),
        }
    }
}

fn default_enabled() -> bool {
    true
}

fn default_timeout_secs() -> u64 {
    30
}

fn default_control_topic() -> String {
    "/behavior/control".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorControl {
    pub action: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_secs: Option<u64>,

    #[serde(default)]
    pub timestamp: u64,
}

impl Message for BehaviorControl {}

impl BehaviorControl {
    /// Shared constructor behind `interrupt`/`interrupt_with_duration`/`resume`/`enable`/
    /// `disable`, which only differ in the action label and which optional fields are set.
    fn build(action: &str, source: Option<&str>, duration_secs: Option<u64>) -> Self {
        Self {
            action: action.to_string(),
            source: source.map(|s| s.to_string()),
            duration_secs,
            timestamp: now_micros(),
        }
    }

    /// Create a new interrupt command
    pub fn interrupt(source: &str) -> Self {
        Self::build("interrupt", Some(source), None)
    }

    /// Create a new interrupt command with auto-resume duration
    pub fn interrupt_with_duration(source: &str, duration_secs: u64) -> Self {
        Self::build("interrupt", Some(source), Some(duration_secs))
    }

    /// Create a new resume command
    pub fn resume(source: &str) -> Self {
        Self::build("resume", Some(source), None)
    }

    /// Create an enable command (backward compatible)
    pub fn enable() -> Self {
        Self::build("enable", None, None)
    }

    /// Create a disable command (backward compatible)
    pub fn disable() -> Self {
        Self::build("disable", None, None)
    }
}

pub struct BehaviorInterruptTrigger {
    source: String,

    config: BehaviorInterruptConfig,

    interrupt_time: Option<SystemTime>,

    auto_resume_task: Option<tokio::task::JoinHandle<()>>,
}

impl BehaviorInterruptTrigger {
    /// Create a new behavior interrupt trigger
    pub fn new(source: &str, config: BehaviorInterruptConfig) -> Self {
        Self {
            source: source.to_string(),
            config,
            interrupt_time: None,
            auto_resume_task: None,
        }
    }

    /// Interrupt behavior tree execution
    ///
    /// Sends an interrupt command to the behavior executor.
    /// If auto-resume is enabled, schedules automatic resume after timeout.
    pub async fn interrupt(&mut self, ctx: &Context) -> Result<()> {
        if !self.config.enabled || self.config.mode == InterruptMode::Disabled {
            debug!("Behavior interrupt disabled, skipping");
            return Ok(());
        }

        if let Some(task) = self.auto_resume_task.take() {
            task.abort();
        }

        let control = match self.config.mode {
            InterruptMode::InterruptOnly => BehaviorControl::interrupt(&self.source),
            InterruptMode::InterruptWithAutoResume => {
                BehaviorControl::interrupt_with_duration(&self.source, self.config.timeout_secs)
            }
            InterruptMode::Disabled => return Ok(()),
        };

        let control_topic =
            Topic::<BehaviorControl>::new(Box::leak(self.config.control_topic.clone().into_boxed_str()));
        ctx.publish_to(control_topic, &control).await?;

        info!(
            "⏸️  Interrupted behavior tree (source: {}, mode: {:?})",
            self.source, self.config.mode
        );

        self.interrupt_time = Some(SystemTime::now());

        if self.config.mode == InterruptMode::InterruptWithAutoResume {
            self.schedule_auto_resume(ctx).await?;
        }

        Ok(())
    }

    /// Resume behavior tree execution
    ///
    /// Sends a resume command to the behavior executor.
    /// Cancels any pending auto-resume task.
    pub async fn resume(&mut self, ctx: &Context) -> Result<()> {
        if !self.config.enabled {
            return Ok(());
        }

        if let Some(task) = self.auto_resume_task.take() {
            task.abort();
        }

        let control = BehaviorControl::resume(&self.source);

        let control_topic =
            Topic::<BehaviorControl>::new(Box::leak(self.config.control_topic.clone().into_boxed_str()));
        ctx.publish_to(control_topic, &control).await?;

        info!("▶️  Resumed behavior tree (source: {})", self.source);

        self.interrupt_time = None;

        Ok(())
    }

    /// Schedule auto-resume task
    async fn schedule_auto_resume(&mut self, ctx: &Context) -> Result<()> {
        let timeout = Duration::from_secs(self.config.timeout_secs);
        let control_topic = self.config.control_topic.clone();
        let source = self.source.clone();
        let ctx_clone = ctx.clone();

        let task = tokio::spawn(async move {
            tokio::time::sleep(timeout).await;

            let control = BehaviorControl::resume(&source);
            let topic = Topic::<BehaviorControl>::new(Box::leak(control_topic.into_boxed_str()));

            match ctx_clone.publish_to(topic, &control).await {
                Ok(_) => {
                    info!(
                        "⏰ Auto-resumed behavior tree after {}s timeout (source: {})",
                        timeout.as_secs(),
                        source
                    );
                }
                Err(e) => {
                    warn!("Failed to auto-resume behavior tree: {}", e);
                }
            }
        });

        self.auto_resume_task = Some(task);

        Ok(())
    }

    /// Get interrupt duration (time since interrupt was triggered)
    pub fn interrupt_duration(&self) -> Option<Duration> {
        self.interrupt_time
            .and_then(|t| SystemTime::now().duration_since(t).ok())
    }

    /// Check if currently interrupted
    pub fn is_interrupted(&self) -> bool {
        self.interrupt_time.is_some()
    }
}

impl Drop for BehaviorInterruptTrigger {
    fn drop(&mut self) {
        if let Some(task) = self.auto_resume_task.take() {
            task.abort();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_interrupt_mode_default() {
        assert_eq!(InterruptMode::default(), InterruptMode::InterruptOnly);
    }

    #[test]
    fn test_config_default() {
        let config = BehaviorInterruptConfig::default();
        assert!(config.enabled);
        assert_eq!(config.mode, InterruptMode::InterruptOnly);
        assert_eq!(config.timeout_secs, 30);
        assert!(!config.await_completion);
        assert!(config.completion_topic.is_none());
        assert_eq!(config.control_topic, "/behavior/control");
    }

    #[test]
    fn test_control_message_creation() {
        let interrupt = BehaviorControl::interrupt("test-node");
        assert_eq!(interrupt.action, "interrupt");
        assert_eq!(interrupt.source, Some("test-node".to_string()));
        assert!(interrupt.duration_secs.is_none());

        let interrupt_with_duration = BehaviorControl::interrupt_with_duration("test-node", 60);
        assert_eq!(interrupt_with_duration.action, "interrupt");
        assert_eq!(interrupt_with_duration.duration_secs, Some(60));

        let resume = BehaviorControl::resume("test-node");
        assert_eq!(resume.action, "resume");

        let enable = BehaviorControl::enable();
        assert_eq!(enable.action, "enable");
        assert!(enable.source.is_none());

        let disable = BehaviorControl::disable();
        assert_eq!(disable.action, "disable");
    }
}