use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use std::sync::Arc;
use arora_behavior::{golden, BehaviorContext, BehaviorInterpreter, BehaviorStatus};
use arora_behavior_tree::{
behavior::BehaviorTreeInterpreter, schema_groot, tree_node::TreeNode, BehaviorTree,
ModuleFunction,
};
use arora_bridge::{Bridge, BridgeCommand, BridgeOp, Inbound};
use arora_engine::engine::PinnedEngine;
use arora_hal::Hal;
use arora_simple_data_store::SimpleDataStore;
use arora_types::call::CallResult;
use arora_types::data::{DataStore, Key, Slot, StateChange, Subscription};
use arora_types::value::Value;
use uuid::Uuid;
use crate::Arora;
#[derive(Debug, PartialEq, Eq)]
pub enum StepOutcome {
Live,
Unregistered,
}
#[derive(Debug)]
pub enum RuntimeError {
Store(String),
BehaviorTree(String),
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeError::Store(m) => write!(f, "data store error: {m}"),
RuntimeError::BehaviorTree(m) => write!(f, "behavior tree error: {m}"),
}
}
}
impl std::error::Error for RuntimeError {}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TelemetrySnapshot {
pub loop_hz: Option<f32>,
pub claimed: bool,
pub behavior: Option<String>,
}
#[derive(Clone, Default)]
pub struct Telemetry {
inner: Arc<std::sync::RwLock<TelemetrySnapshot>>,
}
impl Telemetry {
pub fn snapshot(&self) -> TelemetrySnapshot {
self.inner.read().expect("telemetry lock poisoned").clone()
}
fn update(&self, apply: impl FnOnce(&mut TelemetrySnapshot)) {
apply(&mut self.inner.write().expect("telemetry lock poisoned"));
}
}
struct QueuedBehavior {
name: Option<String>,
behavior: Box<dyn BehaviorInterpreter>,
}
pub struct Runtime {
store: Arc<dyn DataStore>,
engine: PinnedEngine,
function_index: Rc<HashMap<Uuid, ModuleFunction>>,
behaviors: VecDeque<QueuedBehavior>,
telemetry: Telemetry,
hal: Arc<dyn Hal>,
bridge: Arc<dyn Bridge>,
hal_updates: Subscription,
store_changes: Subscription,
time_ns: u64,
time_slot: Box<dyn Slot>,
dt_slot: Box<dyn Slot>,
}
impl Runtime {
pub fn with_io(arora: Arora, hal: Arc<dyn Hal>, bridge: Arc<dyn Bridge>) -> Self {
Self::with_io_in(arora, hal, bridge, Arc::new(SimpleDataStore::new()))
}
pub fn with_io_in(
arora: Arora,
hal: Arc<dyn Hal>,
bridge: Arc<dyn Bridge>,
store: Arc<dyn DataStore>,
) -> Self {
let Arora {
engine,
function_index,
} = arora;
let store_changes = store.subscribe();
let time_slot = store.slot(&Key::from(golden::TIME));
let dt_slot = store.slot(&Key::from(golden::DT));
let hal_updates = hal.updates();
Self {
store,
engine,
function_index,
behaviors: VecDeque::new(),
telemetry: Telemetry::default(),
hal,
bridge,
hal_updates,
store_changes,
time_ns: 0,
time_slot,
dt_slot,
}
}
pub fn telemetry(&self) -> Telemetry {
self.telemetry.clone()
}
pub fn queue_groot_xml(&mut self, xml: &str) -> Result<(), RuntimeError> {
self.queue_groot_xml_as(None, xml)
}
pub fn queue_named_groot_xml(&mut self, name: &str, xml: &str) -> Result<(), RuntimeError> {
self.queue_groot_xml_as(Some(name.to_string()), xml)
}
fn queue_groot_xml_as(&mut self, name: Option<String>, xml: &str) -> Result<(), RuntimeError> {
let groot = schema_groot::BehaviorTree::try_from_groot_xml(xml)
.map_err(|e| RuntimeError::BehaviorTree(format!("parse: {e:?}")))?;
let mut variables = HashMap::new();
let tree_node: TreeNode = groot
.root
.try_into_tree_node(self.function_index.as_ref(), &mut variables)
.map_err(|e| RuntimeError::BehaviorTree(format!("build: {e:?}")))?;
let id_to_name: HashMap<Uuid, String> =
variables.into_iter().map(|(name, id)| (id, name)).collect();
let store = self.store.clone();
let resolver = move |name: &str| Some(store.slot(&Key::from(name)));
let behavior: BehaviorTree = tree_node
.into_behavior_tree(&resolver, &id_to_name)
.map_err(|e| RuntimeError::BehaviorTree(format!("instantiate: {e:?}")))?;
self.behaviors.push_back(QueuedBehavior {
name,
behavior: Box::new(BehaviorTreeInterpreter::new(
behavior,
self.function_index.clone(),
)),
});
Ok(())
}
pub fn queue_behavior(&mut self, behavior: Box<dyn BehaviorInterpreter>) {
self.behaviors.push_back(QueuedBehavior {
name: None,
behavior,
});
}
pub fn queue_named_behavior(&mut self, name: &str, behavior: Box<dyn BehaviorInterpreter>) {
self.behaviors.push_back(QueuedBehavior {
name: Some(name.to_string()),
behavior,
});
}
pub fn step(&mut self, dt_ns: u64) -> Result<StepOutcome, RuntimeError> {
while let Some(change) = self.hal_updates.try_recv() {
self.apply(change)?;
}
while let Some(event) = self.bridge.try_recv() {
match event {
Inbound::Command(cmd) => self.handle_command(cmd)?,
Inbound::DeviceInfo(Ok(None)) => return Ok(StepOutcome::Unregistered),
Inbound::DeviceInfo(Ok(Some(_info))) => { }
Inbound::DeviceInfo(Err(_e)) => { }
Inbound::DataRequested(requested) => {
self.telemetry.update(|t| t.claimed = requested);
}
}
}
self.time_ns = self.time_ns.saturating_add(dt_ns);
self.dt_slot
.set(Some(Value::U64(dt_ns)))
.map_err(|e| RuntimeError::Store(e.to_string()))?;
self.time_slot
.set(Some(Value::U64(self.time_ns)))
.map_err(|e| RuntimeError::Store(e.to_string()))?;
if let Some(mut queued) = self.behaviors.pop_front() {
self.telemetry.update(|t| {
if t.behavior != queued.name {
t.behavior = queued.name.clone();
}
});
let mut ctx = BehaviorContext {
store: &*self.store,
caller: &mut self.engine,
};
let status = queued
.behavior
.tick(&mut ctx)
.map_err(|e| RuntimeError::BehaviorTree(e.to_string()))?;
if status == BehaviorStatus::Running {
self.behaviors.push_back(queued);
} else {
self.telemetry.update(|t| t.behavior = None);
}
}
let mut merged = StateChange::new();
while let Some(change) = self.store_changes.try_recv() {
for (key, value) in change.set {
if golden::is_golden(key.get_path()) {
continue;
}
merged.unset.remove(&key);
merged.set.insert(key, value);
}
for key in change.unset {
if golden::is_golden(key.get_path()) {
continue;
}
merged.set.remove(&key);
merged.unset.insert(key);
}
}
if !merged.is_empty() {
self.bridge.try_send(&merged);
self.hal.try_send(&merged);
}
Ok(StepOutcome::Live)
}
fn apply(&self, change: StateChange) -> Result<(), RuntimeError> {
self.store
.write(change)
.map_err(|e| RuntimeError::Store(e.to_string()))
}
fn handle_command(&mut self, cmd: BridgeCommand) -> Result<(), RuntimeError> {
let result = match &cmd.op {
BridgeOp::Get(keys) => {
let values = self.store.read(keys);
let array = values
.into_iter()
.map(|v| Value::Option(v.map(Box::new)))
.collect();
Ok(CallResult {
ret: Value::ArrayValue(array),
mutated: Vec::new(),
})
}
BridgeOp::Update(change) => match self.store.write(change.clone()) {
Ok(()) => Ok(CallResult {
ret: Value::Unit,
mutated: Vec::new(),
}),
Err(e) => Err(e.to_string()),
},
BridgeOp::Call(_call) => {
Err("call handling is not yet wired".to_string())
}
BridgeOp::ListKeys { prefix } => {
let snapshot = self.store.snapshot();
let mut paths: Vec<String> = snapshot
.storage
.iter()
.filter(|(_, value)| value.is_some())
.map(|(key, _)| key.path.clone())
.filter(|path| prefix.as_ref().is_none_or(|p| path.starts_with(p.as_str())))
.collect();
paths.sort();
Ok(CallResult {
ret: Value::ArrayValue(paths.into_iter().map(Value::String).collect()),
mutated: Vec::new(),
})
}
BridgeOp::ListMethods { prefix } => {
let mut names: Vec<String> = self
.function_index
.values()
.map(|f| f.function_name.clone())
.filter(|name| prefix.as_ref().is_none_or(|p| name.starts_with(p.as_str())))
.collect();
names.sort();
names.dedup();
Ok(CallResult {
ret: Value::ArrayValue(names.into_iter().map(Value::String).collect()),
mutated: Vec::new(),
})
}
};
cmd.reply(result);
Ok(())
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Runtime {
pub fn run(&mut self) -> Result<(), RuntimeError> {
let mut window_start = std::time::Instant::now();
let mut steps_in_window: u32 = 0;
let mut last_step = std::time::Instant::now();
loop {
let now = std::time::Instant::now();
let dt_ns = now.duration_since(last_step).as_nanos() as u64;
last_step = now;
if self.step(dt_ns)? == StepOutcome::Unregistered {
return Ok(());
}
steps_in_window += 1;
let elapsed = window_start.elapsed();
if elapsed >= std::time::Duration::from_secs(1) {
let hz = steps_in_window as f32 / elapsed.as_secs_f32();
self.telemetry.update(|t| t.loop_hz = Some(hz));
window_start = std::time::Instant::now();
steps_in_window = 0;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use arora_bridge::{BridgeResult, DeviceInfo};
use arora_simple_data_store::NamespacedStore;
use arora_types::data::{Key, StateChange};
use async_trait::async_trait;
use futures::channel::oneshot;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Default)]
struct UnregisterBridge {
done: AtomicBool,
}
#[async_trait]
impl Bridge for UnregisterBridge {
fn try_recv(&self) -> Option<Inbound> {
if !self.done.swap(true, Ordering::Relaxed) {
Some(Inbound::DeviceInfo(Ok(None)))
} else {
None
}
}
fn try_send(&self, _change: &StateChange) {}
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
}
async fn build(bridge: Arc<dyn Bridge>) -> Runtime {
let arora = Arora::start().await.expect("arora starts");
Runtime::with_io(arora, Arc::new(arora_hal::FakeHal::new()), bridge)
}
async fn build_in(bridge: Arc<dyn Bridge>, store: Arc<dyn DataStore>) -> Runtime {
let arora = Arora::start().await.expect("arora starts");
Runtime::with_io_in(arora, Arc::new(arora_hal::FakeHal::new()), bridge, store)
}
fn drive_until_unregistered(mut runtime: Runtime) {
for _ in 0..1000 {
if runtime.step(16_000_000).expect("step ok") == StepOutcome::Unregistered {
return;
}
}
panic!("runtime never reported unregistered");
}
#[tokio::test]
async fn stops_when_unregistered() {
let runtime = build(Arc::new(UnregisterBridge::default())).await;
drive_until_unregistered(runtime);
}
#[tokio::test]
async fn runs_a_queued_tree() {
let xml = r#"<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="11111111-1111-4111-8111-111111111111">
<Succeed name="22222222-2222-4222-8222-222222222222" />
</Sequence>
</BehaviorTree>
</root>"#;
let mut runtime = build(Arc::new(UnregisterBridge::default())).await;
runtime.queue_groot_xml(xml).expect("tree queues");
drive_until_unregistered(runtime);
}
#[tokio::test]
async fn get_and_update_commands_round_trip() {
let mut runtime = build(Arc::new(UnregisterBridge::default())).await;
let key = Key::from("greeting");
let (tx, rx) = oneshot::channel();
let mut set = std::collections::HashMap::new();
set.insert(key.clone(), Some(Value::String("hi".into())));
let change = StateChange {
set,
unset: std::collections::HashSet::new(),
};
runtime
.handle_command(BridgeCommand::new(BridgeOp::Update(change), tx))
.unwrap();
assert!(rx.await.unwrap().is_ok(), "update should succeed");
let (tx, rx) = oneshot::channel();
runtime
.handle_command(BridgeCommand::new(BridgeOp::Get(vec![key]), tx))
.unwrap();
let result = rx.await.unwrap().expect("get should succeed");
assert_eq!(
result.ret,
Value::ArrayValue(vec![Value::Option(Some(Box::new(Value::String(
"hi".into()
))))])
);
}
struct SilentBridge;
#[async_trait]
impl Bridge for SilentBridge {
fn try_recv(&self) -> Option<Inbound> {
None
}
fn try_send(&self, _change: &StateChange) {}
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
}
struct WriteOnce;
impl BehaviorInterpreter for WriteOnce {
fn tick(
&mut self,
ctx: &mut BehaviorContext,
) -> Result<BehaviorStatus, arora_behavior::BehaviorError> {
ctx.store
.write(StateChange::set(
"from_behavior",
Value::String("hi".into()),
))
.map_err(|e| arora_behavior::BehaviorError {
message: e.to_string(),
})?;
Ok(BehaviorStatus::Done)
}
}
#[tokio::test]
async fn runs_a_queued_non_tree_behavior() {
let mut runtime = build(Arc::new(SilentBridge)).await;
runtime.queue_behavior(Box::new(WriteOnce));
runtime.step(16_000_000).expect("step");
let (tx, rx) = oneshot::channel();
runtime
.handle_command(BridgeCommand::new(
BridgeOp::Get(vec![Key::from("from_behavior")]),
tx,
))
.unwrap();
let result = rx.await.unwrap().expect("get ok");
assert_eq!(
result.ret,
Value::ArrayValue(vec![Value::Option(Some(Box::new(Value::String(
"hi".into()
))))])
);
}
#[tokio::test]
async fn list_keys_enumerates_the_store_by_prefix() {
let mut runtime = build(Arc::new(UnregisterBridge::default())).await;
let mut set = std::collections::HashMap::new();
set.insert(Key::from("face/mouth"), Some(Value::F32(0.5)));
set.insert(Key::from("face/eyes"), Some(Value::F32(0.1)));
set.insert(Key::from("body/hand"), Some(Value::F32(0.9)));
let (tx, _rx) = oneshot::channel();
runtime
.handle_command(BridgeCommand::new(
BridgeOp::Update(StateChange {
set,
unset: std::collections::HashSet::new(),
}),
tx,
))
.unwrap();
let (tx, rx) = oneshot::channel();
runtime
.handle_command(BridgeCommand::new(
BridgeOp::ListKeys {
prefix: Some("face".into()),
},
tx,
))
.unwrap();
let result = rx.await.unwrap().expect("list_keys ok");
assert_eq!(
result.ret,
Value::ArrayValue(vec![
Value::String("face/eyes".into()),
Value::String("face/mouth".into()),
])
);
let (tx, rx) = oneshot::channel();
runtime
.handle_command(BridgeCommand::new(
BridgeOp::ListMethods { prefix: None },
tx,
))
.unwrap();
let methods = rx.await.unwrap().expect("list_methods ok");
assert!(
matches!(methods.ret, Value::ArrayValue(_)),
"list_methods returns an array"
);
}
#[tokio::test]
async fn runtime_over_namespaced_store_writes_under_namespace() {
let shared = SimpleDataStore::new();
let store: Arc<dyn DataStore> =
Arc::new(NamespacedStore::new(Arc::new(shared.clone()), "robotA"));
let mut runtime = build_in(Arc::new(SilentBridge), store.clone()).await;
let (tx, rx) = oneshot::channel();
let mut set = std::collections::HashMap::new();
set.insert(Key::from("greeting"), Some(Value::String("hi".into())));
runtime
.handle_command(BridgeCommand::new(
BridgeOp::Update(StateChange {
set,
unset: std::collections::HashSet::new(),
}),
tx,
))
.unwrap();
assert!(rx.await.unwrap().is_ok(), "update should succeed");
assert_eq!(
runtime.step(16_000_000).expect("step ok"),
StepOutcome::Live
);
assert_eq!(
shared.read(&[Key::from("robotA/greeting")]),
vec![Some(Value::String("hi".into()))],
"the runtime's write landed under the device namespace"
);
assert_eq!(
shared.read(&[Key::from("greeting")]),
vec![None],
"the bare key must not be set in the shared store"
);
}
struct WriteKey {
key: &'static str,
value: Value,
}
impl BehaviorInterpreter for WriteKey {
fn tick(
&mut self,
ctx: &mut BehaviorContext,
) -> Result<BehaviorStatus, arora_behavior::BehaviorError> {
ctx.store
.write(StateChange::set(self.key, self.value.clone()))
.map_err(|e| arora_behavior::BehaviorError {
message: e.to_string(),
})?;
Ok(BehaviorStatus::Done)
}
}
#[tokio::test]
async fn behavior_writes_then_switching_changes_the_namespaced_store() {
let shared = SimpleDataStore::new();
let store: Arc<dyn DataStore> =
Arc::new(NamespacedStore::new(Arc::new(shared.clone()), "robotA"));
let mut runtime = build_in(Arc::new(SilentBridge), store).await;
runtime.queue_behavior(Box::new(WriteKey {
key: "greeting",
value: Value::String("hi".into()),
}));
assert_eq!(runtime.step(16_000_000).expect("step"), StepOutcome::Live);
assert_eq!(
shared.read(&[Key::from("robotA/greeting")]),
vec![Some(Value::String("hi".into()))],
"the behavior's write landed under the device namespace"
);
runtime.queue_behavior(Box::new(WriteKey {
key: "greeting",
value: Value::String("bye".into()),
}));
assert_eq!(runtime.step(16_000_000).expect("step"), StepOutcome::Live);
assert_eq!(
shared.read(&[Key::from("robotA/greeting")]),
vec![Some(Value::String("bye".into()))],
"switching the queued behavior changed what was written"
);
}
#[tokio::test]
async fn golden_clock_is_published_to_the_store_each_step() {
let store = Arc::new(SimpleDataStore::new());
let mut runtime = build_in(Arc::new(SilentBridge), store.clone()).await;
assert_eq!(store.read(&[Key::from(golden::DT)]), vec![None]);
assert_eq!(store.read(&[Key::from(golden::TIME)]), vec![None]);
assert_eq!(runtime.step(16_000_000).expect("step"), StepOutcome::Live);
assert_eq!(
store.read(&[Key::from(golden::DT)]),
vec![Some(Value::U64(16_000_000))]
);
assert_eq!(
store.read(&[Key::from(golden::TIME)]),
vec![Some(Value::U64(16_000_000))]
);
assert_eq!(runtime.step(4_000_000).expect("step"), StepOutcome::Live);
assert_eq!(
store.read(&[Key::from(golden::DT)]),
vec![Some(Value::U64(4_000_000))]
);
assert_eq!(
store.read(&[Key::from(golden::TIME)]),
vec![Some(Value::U64(20_000_000))]
);
}
struct RecordingBridge {
sent: Arc<std::sync::Mutex<Vec<StateChange>>>,
}
#[async_trait]
impl Bridge for RecordingBridge {
fn try_recv(&self) -> Option<Inbound> {
None
}
fn try_send(&self, change: &StateChange) {
self.sent.lock().unwrap().push(change.clone());
}
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
}
#[tokio::test]
async fn golden_keys_are_not_forwarded_outbound() {
let sent = Arc::new(std::sync::Mutex::new(Vec::new()));
let mut runtime = build(Arc::new(RecordingBridge { sent: sent.clone() })).await;
runtime.queue_behavior(Box::new(WriteKey {
key: "greeting",
value: Value::String("hi".into()),
}));
for _ in 0..5 {
runtime.step(16_000_000).expect("step");
}
let sent = sent.lock().unwrap();
let forwarded_keys: Vec<String> = sent
.iter()
.flat_map(|c| c.set.keys().map(|k| k.path.clone()))
.collect();
assert!(
forwarded_keys.iter().any(|k| k.as_str() == "greeting"),
"the ordinary behavior write should be forwarded outbound, got {forwarded_keys:?}"
);
assert!(
!forwarded_keys.iter().any(|k| golden::is_golden(k.as_str())),
"golden keys must never be forwarded outbound, got {forwarded_keys:?}"
);
}
}