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 {
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(),
}
}
pub fn interrupt(source: &str) -> Self {
Self::build("interrupt", Some(source), None)
}
pub fn interrupt_with_duration(source: &str, duration_secs: u64) -> Self {
Self::build("interrupt", Some(source), Some(duration_secs))
}
pub fn resume(source: &str) -> Self {
Self::build("resume", Some(source), None)
}
pub fn enable() -> Self {
Self::build("enable", None, None)
}
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 {
pub fn new(source: &str, config: BehaviorInterruptConfig) -> Self {
Self {
source: source.to_string(),
config,
interrupt_time: None,
auto_resume_task: None,
}
}
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(())
}
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(())
}
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(())
}
pub fn interrupt_duration(&self) -> Option<Duration> {
self.interrupt_time
.and_then(|t| SystemTime::now().duration_since(t).ok())
}
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");
}
}