use crate::context::Context;
use crate::messages::Message;
use crate::prelude::now_micros;
use crate::topics::Topic;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum InterruptMode {
Disabled,
#[default]
InterruptOnly,
InterruptWithAutoResume,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorInterruptConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default)]
pub mode: InterruptMode,
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
#[serde(default)]
pub await_completion: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_topic: Option<String>,
#[serde(default = "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 {
pub fn interrupt(source: &str) -> Self {
Self {
action: "interrupt".to_string(),
source: Some(source.to_string()),
duration_secs: None,
timestamp: now_micros(),
}
}
pub fn interrupt_with_duration(source: &str, duration_secs: u64) -> Self {
Self {
action: "interrupt".to_string(),
source: Some(source.to_string()),
duration_secs: Some(duration_secs),
timestamp: now_micros(),
}
}
pub fn resume(source: &str) -> Self {
Self {
action: "resume".to_string(),
source: Some(source.to_string()),
duration_secs: None,
timestamp: now_micros(),
}
}
pub fn enable() -> Self {
Self {
action: "enable".to_string(),
source: None,
duration_secs: None,
timestamp: now_micros(),
}
}
pub fn disable() -> Self {
Self {
action: "disable".to_string(),
source: None,
duration_secs: None,
timestamp: now_micros(),
}
}
}
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");
}
}