use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::vec;
use agent_stream_kit::{
ASKit, Agent, AgentContext, AgentData, AgentError, AgentOutput, AgentSpec, AgentStatus,
AgentValue, AsAgent, askit_agent, async_trait,
};
use chrono::{DateTime, Local, Utc};
use cron::Schedule;
use log;
use regex::Regex;
use tokio::task::JoinHandle;
static CATEGORY: &str = "Std/Time";
static PIN_TIME: &str = "time";
static PIN_UNIT: &str = "unit";
static CONFIG_DELAY: &str = "delay";
static CONFIG_MAX_NUM_DATA: &str = "max_num_data";
static CONFIG_INTERVAL: &str = "interval";
static CONFIG_SCHEDULE: &str = "schedule";
static CONFIG_TIME: &str = "time";
const DELAY_MS_DEFAULT: i64 = 1000; const MAX_NUM_DATA_DEFAULT: i64 = 10;
static INTERVAL_DEFAULT: &str = "10s";
static TIME_DEFAULT: &str = "1s";
#[askit_agent(
title = "Delay",
description = "Delays output by a specified time",
category = CATEGORY,
inputs = ["*"],
outputs = ["*"],
integer_config(name = CONFIG_DELAY, default = DELAY_MS_DEFAULT, title = "delay (ms)"),
integer_config(name = CONFIG_MAX_NUM_DATA, default = MAX_NUM_DATA_DEFAULT, title = "max num data")
)]
struct DelayAgent {
data: AgentData,
num_waiting_data: Arc<Mutex<i64>>,
}
#[async_trait]
impl AsAgent for DelayAgent {
fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
Ok(Self {
data: AgentData::new(askit, id, spec),
num_waiting_data: Arc::new(Mutex::new(0)),
})
}
async fn process(
&mut self,
ctx: AgentContext,
pin: String,
value: AgentValue,
) -> Result<(), AgentError> {
let config = self.configs()?;
let delay_ms = config.get_integer_or(CONFIG_DELAY, DELAY_MS_DEFAULT);
let max_num_data = config.get_integer_or(CONFIG_MAX_NUM_DATA, MAX_NUM_DATA_DEFAULT);
{
let num_waiting_data = self.num_waiting_data.clone();
let mut num_waiting_data = num_waiting_data.lock().unwrap();
if *num_waiting_data >= max_num_data {
return Ok(());
}
*num_waiting_data += 1;
}
tokio::time::sleep(Duration::from_millis(delay_ms as u64)).await;
self.try_output(ctx.clone(), pin, value.clone())?;
let mut num_waiting_data = self.num_waiting_data.lock().unwrap();
*num_waiting_data -= 1;
Ok(())
}
}
#[askit_agent(
title = "Interval Timer",
description = "Outputs a unit signal at specified intervals",
category = CATEGORY,
outputs = [PIN_UNIT],
string_config(name = CONFIG_INTERVAL, default = INTERVAL_DEFAULT, description = "(ex. 10s, 5m, 100ms, 1h, 1d)")
)]
struct IntervalTimerAgent {
data: AgentData,
timer_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
interval_ms: u64,
}
impl IntervalTimerAgent {
fn start_timer(&mut self) -> Result<(), AgentError> {
let timer_handle = self.timer_handle.clone();
let interval_ms = self.interval_ms;
let askit = self.askit().clone();
let agent_id = self.id().to_string();
let handle = self.runtime().spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_millis(interval_ms)).await;
if let Ok(handle) = timer_handle.lock() {
if handle.is_none() {
break;
}
}
if let Err(e) = askit.try_send_agent_out(
agent_id.clone(),
AgentContext::new(),
PIN_UNIT.to_string(),
AgentValue::unit(),
) {
log::error!("Failed to send interval timer output: {}", e);
}
}
});
if let Ok(mut timer_handle) = self.timer_handle.lock() {
*timer_handle = Some(handle);
}
Ok(())
}
fn stop_timer(&mut self) -> Result<(), AgentError> {
if let Ok(mut timer_handle) = self.timer_handle.lock() {
if let Some(handle) = timer_handle.take() {
handle.abort();
}
}
Ok(())
}
}
#[async_trait]
impl AsAgent for IntervalTimerAgent {
fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
let interval = spec
.configs
.as_ref()
.ok_or(AgentError::NoConfig)?
.get_string_or(CONFIG_INTERVAL, INTERVAL_DEFAULT);
let interval_ms = parse_duration_to_ms(&interval)?;
Ok(Self {
data: AgentData::new(askit, id, spec),
timer_handle: Default::default(),
interval_ms,
})
}
async fn start(&mut self) -> Result<(), AgentError> {
self.start_timer()
}
async fn stop(&mut self) -> Result<(), AgentError> {
self.stop_timer()
}
fn configs_changed(&mut self) -> Result<(), AgentError> {
let interval = self.configs()?.get_string(CONFIG_INTERVAL)?;
let new_interval = parse_duration_to_ms(&interval)?;
if new_interval != self.interval_ms {
self.interval_ms = new_interval;
if *self.status() == AgentStatus::Start {
self.stop_timer()?;
self.start_timer()?;
}
}
Ok(())
}
}
#[askit_agent(
title = "On Start",
category = CATEGORY,
outputs = [PIN_UNIT],
integer_config(name = CONFIG_DELAY, default = DELAY_MS_DEFAULT, title = "delay (ms)")
)]
struct OnStartAgent {
data: AgentData,
}
#[async_trait]
impl AsAgent for OnStartAgent {
fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
Ok(Self {
data: AgentData::new(askit, id, spec),
})
}
async fn start(&mut self) -> Result<(), AgentError> {
let config = self.configs()?;
let delay_ms = config.get_integer_or(CONFIG_DELAY, DELAY_MS_DEFAULT);
let askit = self.askit().clone();
let agent_id = self.id().to_string();
self.runtime().spawn(async move {
tokio::time::sleep(Duration::from_millis(delay_ms as u64)).await;
if let Err(e) = askit.try_send_agent_out(
agent_id,
AgentContext::new(),
PIN_UNIT.to_string(),
AgentValue::unit(),
) {
log::error!("Failed to send delayed output: {}", e);
}
});
Ok(())
}
}
#[askit_agent(
title = "Schedule Timer",
category = CATEGORY,
outputs = [PIN_TIME],
string_config(name = CONFIG_SCHEDULE, default = "0 0 * * * *", description = "sec min hour day month week year")
)]
struct ScheduleTimerAgent {
data: AgentData,
cron_schedule: Option<Schedule>,
timer_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
}
impl ScheduleTimerAgent {
fn start_timer(&mut self) -> Result<(), AgentError> {
let Some(schedule) = &self.cron_schedule else {
return Err(AgentError::InvalidConfig("No schedule defined".into()));
};
let askit = self.askit().clone();
let agent_id = self.id().to_string();
let timer_handle = self.timer_handle.clone();
let schedule = schedule.clone();
let handle = self.runtime().spawn(async move {
loop {
let now: DateTime<Utc> = Utc::now();
let next = match schedule.upcoming(Utc).next() {
Some(next_time) => next_time,
None => {
log::error!("No upcoming schedule times found");
break;
}
};
let duration = match (next - now).to_std() {
Ok(duration) => duration,
Err(e) => {
log::error!("Failed to calculate duration until next schedule: {}", e);
tokio::time::sleep(Duration::from_secs(60)).await;
continue;
}
};
let next_local = next.with_timezone(&Local);
log::debug!(
"Scheduling timer for '{}' to fire at {} (in {:?})",
agent_id,
next_local.format("%Y-%m-%d %H:%M:%S %z"),
duration
);
tokio::time::sleep(duration).await;
if let Ok(handle) = timer_handle.lock() {
if handle.is_none() {
break;
}
}
let current_local_time = Local::now().timestamp();
if let Err(e) = askit.try_send_agent_out(
agent_id.clone(),
AgentContext::new(),
PIN_TIME.to_string(),
AgentValue::integer(current_local_time),
) {
log::error!("Failed to send schedule timer output: {}", e);
}
}
});
if let Ok(mut timer_handle) = self.timer_handle.lock() {
*timer_handle = Some(handle);
}
Ok(())
}
fn stop_timer(&mut self) -> Result<(), AgentError> {
if let Ok(mut timer_handle) = self.timer_handle.lock() {
if let Some(handle) = timer_handle.take() {
handle.abort();
}
}
Ok(())
}
fn parse_schedule(&mut self, schedule_str: &str) -> Result<(), AgentError> {
if schedule_str.trim().is_empty() {
self.cron_schedule = None;
return Ok(());
}
let schedule = Schedule::from_str(schedule_str).map_err(|e| {
AgentError::InvalidConfig(format!("Invalid cron schedule '{}': {}", schedule_str, e))
})?;
self.cron_schedule = Some(schedule);
Ok(())
}
}
#[async_trait]
impl AsAgent for ScheduleTimerAgent {
fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
let schedule_str = spec
.configs
.as_ref()
.map(|cfg| cfg.get_string(CONFIG_SCHEDULE))
.transpose()?;
let mut agent = Self {
data: AgentData::new(askit, id, spec),
cron_schedule: None,
timer_handle: Default::default(),
};
if let Some(schedule_str) = schedule_str {
if !schedule_str.is_empty() {
agent.parse_schedule(&schedule_str)?;
}
}
Ok(agent)
}
async fn start(&mut self) -> Result<(), AgentError> {
if self.cron_schedule.is_some() {
self.start_timer()?;
}
Ok(())
}
async fn stop(&mut self) -> Result<(), AgentError> {
self.stop_timer()
}
fn configs_changed(&mut self) -> Result<(), AgentError> {
let schedule_str = self.configs()?.get_string(CONFIG_SCHEDULE)?;
self.parse_schedule(&schedule_str)?;
if *self.status() == AgentStatus::Start {
self.stop_timer()?;
if self.cron_schedule.is_some() {
self.start_timer()?;
}
}
Ok(())
}
}
#[askit_agent(
title = "Throttle Time",
category = CATEGORY,
inputs = ["*"],
outputs = ["*"],
string_config(name = CONFIG_TIME, default = TIME_DEFAULT, description = "(ex. 10s, 5m, 100ms, 1h, 1d)"),
integer_config(name = CONFIG_MAX_NUM_DATA, title = "max num data", description = "0: no data, -1: all data")
)]
struct ThrottleTimeAgent {
data: AgentData,
timer_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
time_ms: u64,
max_num_data: i64,
waiting_data: Arc<Mutex<Vec<(AgentContext, String, AgentValue)>>>,
}
impl ThrottleTimeAgent {
fn start_timer(&mut self) -> Result<(), AgentError> {
let timer_handle = self.timer_handle.clone();
let time_ms = self.time_ms;
let waiting_data = self.waiting_data.clone();
let askit = self.askit().clone();
let agent_id = self.id().to_string();
let handle = self.runtime().spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_millis(time_ms)).await;
let mut handle = timer_handle.lock().unwrap();
if handle.is_none() {
break;
}
let mut wd = waiting_data.lock().unwrap();
if wd.len() > 0 {
let (ctx, pin, data) = wd.remove(0);
askit
.try_send_agent_out(agent_id.clone(), ctx, pin, data)
.unwrap_or_else(|e| {
log::error!("Failed to send delayed output: {}", e);
});
}
if wd.len() == 0 {
handle.take();
break;
}
}
});
if let Ok(mut timer_handle) = self.timer_handle.lock() {
*timer_handle = Some(handle);
}
Ok(())
}
fn stop_timer(&mut self) -> Result<(), AgentError> {
if let Ok(mut timer_handle) = self.timer_handle.lock() {
if let Some(handle) = timer_handle.take() {
handle.abort();
}
}
Ok(())
}
}
#[async_trait]
impl AsAgent for ThrottleTimeAgent {
fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
let time = spec
.configs
.as_ref()
.ok_or(AgentError::NoConfig)?
.get_string_or(CONFIG_TIME, TIME_DEFAULT);
let time_ms = parse_duration_to_ms(&time)?;
let max_num_data = spec
.configs
.as_ref()
.ok_or(AgentError::NoConfig)?
.get_integer_or(CONFIG_MAX_NUM_DATA, 0);
Ok(Self {
data: AgentData::new(askit, id, spec),
timer_handle: Default::default(),
time_ms,
max_num_data,
waiting_data: Arc::new(Mutex::new(vec![])),
})
}
async fn stop(&mut self) -> Result<(), AgentError> {
self.stop_timer()
}
fn configs_changed(&mut self) -> Result<(), AgentError> {
let time = self.configs()?.get_string(CONFIG_TIME)?;
let new_time = parse_duration_to_ms(&time)?;
if new_time != self.time_ms {
self.time_ms = new_time;
}
let max_num_data = self.configs()?.get_integer(CONFIG_MAX_NUM_DATA)?;
if self.max_num_data != max_num_data {
let mut wd = self.waiting_data.lock().unwrap();
let wd_len = wd.len();
if max_num_data >= 0 && wd_len > (max_num_data as usize) {
wd.drain(0..(wd_len - (max_num_data as usize)));
}
self.max_num_data = max_num_data;
}
Ok(())
}
async fn process(
&mut self,
ctx: AgentContext,
pin: String,
value: AgentValue,
) -> Result<(), AgentError> {
if self.timer_handle.lock().unwrap().is_some() {
let mut wd = self.waiting_data.lock().unwrap();
if self.max_num_data == 0 {
return Ok(());
}
wd.push((ctx, pin, value));
if self.max_num_data > 0 && wd.len() > self.max_num_data as usize {
wd.remove(0);
}
return Ok(());
}
self.start_timer()?;
self.try_output(ctx, pin, value)?;
Ok(())
}
}
fn parse_duration_to_ms(duration_str: &str) -> Result<u64, AgentError> {
const MIN_DURATION: u64 = 10;
let re = Regex::new(r"^(\d+)(?:([a-zA-Z]+))?$").expect("Failed to compile regex");
if let Some(captures) = re.captures(duration_str.trim()) {
let value: u64 = captures.get(1).unwrap().as_str().parse().map_err(|e| {
AgentError::InvalidConfig(format!(
"Invalid number in duration '{}': {}",
duration_str, e
))
})?;
let unit = captures
.get(2)
.map_or("s".to_string(), |m| m.as_str().to_lowercase());
let milliseconds = match unit.as_str() {
"ms" => value, "s" => value * 1000, "m" => value * 60 * 1000, "h" => value * 3600 * 1000, "d" => value * 86400 * 1000, _ => {
return Err(AgentError::InvalidConfig(format!(
"Unknown time unit: {}",
unit
)));
}
};
Ok(std::cmp::max(milliseconds, MIN_DURATION))
} else {
let value: u64 = duration_str.parse().map_err(|e| {
AgentError::InvalidConfig(format!("Invalid duration format '{}': {}", duration_str, e))
})?;
Ok(std::cmp::max(value * 1000, MIN_DURATION)) }
}