use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use std::sync::Arc;
use arora_behavior::{Behavior, BehaviorContext, BehaviorStatus};
use arora_behavior_tree::{
behavior::TreeBehavior, schema_groot, tree_node::TreeNode, BehaviorTree, ModuleFunction,
};
use arora_bridge::{Bridge, BridgeCommand, BridgeOp, BridgeResult, DeviceInfo};
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, StateChange, Subscription};
use arora_types::value::Value;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures::{Future, StreamExt};
use uuid::Uuid;
use crate::Arora;
enum Inbound {
Command(BridgeCommand),
DeviceInfo(BridgeResult<Option<DeviceInfo>>),
DataRequested(bool),
}
enum Outbound {
StateChanged(StateChange),
}
#[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 {}
pub struct Runtime {
store: Arc<dyn DataStore>,
engine: PinnedEngine,
function_index: Rc<HashMap<Uuid, ModuleFunction>>,
behaviors: VecDeque<Box<dyn Behavior>>,
hal_updates: Subscription,
inbound: UnboundedReceiver<Inbound>,
outbound: UnboundedSender<Outbound>,
store_changes: Subscription,
}
impl Runtime {
pub fn with_io(
arora: Arora,
hal: Arc<dyn Hal>,
bridge: Arc<dyn Bridge>,
) -> (Self, impl Future<Output = ()>) {
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, impl Future<Output = ()>) {
let Arora {
engine,
function_index,
} = arora;
let store_changes = store.subscribe();
let hal_updates = hal.updates();
let (inbound_tx, inbound) = futures::channel::mpsc::unbounded::<Inbound>();
let (outbound, outbound_rx) = futures::channel::mpsc::unbounded::<Outbound>();
let pump = io(bridge, hal, inbound_tx, outbound_rx);
let runtime = Self {
store,
engine,
function_index,
behaviors: VecDeque::new(),
hal_updates,
inbound,
outbound,
store_changes,
};
(runtime, pump)
}
pub fn queue_groot_xml(&mut self, 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.queue_behavior(Box::new(TreeBehavior::new(
behavior,
self.function_index.clone(),
)));
Ok(())
}
pub fn queue_behavior(&mut self, behavior: Box<dyn Behavior>) {
self.behaviors.push_back(behavior);
}
pub fn step(&mut self) -> Result<StepOutcome, RuntimeError> {
while let Some(change) = self.hal_updates.try_recv() {
self.apply(change)?;
}
while let Ok(event) = self.inbound.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) => { }
}
}
if let Some(mut behavior) = self.behaviors.pop_front() {
let mut ctx = BehaviorContext {
store: &*self.store,
caller: &mut self.engine,
dt: 0.0,
};
let status = behavior
.tick(&mut ctx)
.map_err(|e| RuntimeError::BehaviorTree(e.to_string()))?;
if status == BehaviorStatus::Running {
self.behaviors.push_back(behavior);
}
}
let mut merged = StateChange::new();
while let Some(change) = self.store_changes.try_recv() {
for (key, value) in change.set {
merged.unset.remove(&key);
merged.set.insert(key, value);
}
for key in change.unset {
merged.set.remove(&key);
merged.unset.insert(key);
}
}
if !merged.is_empty() {
let _ = self.outbound.unbounded_send(Outbound::StateChanged(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> {
loop {
if self.step()? == StepOutcome::Unregistered {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
}
async fn io(
bridge: Arc<dyn Bridge>,
hal: Arc<dyn Hal>,
inbound_tx: UnboundedSender<Inbound>,
mut outbound_rx: UnboundedReceiver<Outbound>,
) {
let forward = async {
let commands = bridge.commands().await.map(Inbound::Command);
let device_info = bridge
.device_info_updated()
.await
.unwrap_or_else(|_| Box::pin(futures::stream::empty()))
.map(Inbound::DeviceInfo);
let data_requested = bridge.data_requested().await.map(Inbound::DataRequested);
let mut merged = futures::stream::select(
commands,
futures::stream::select(device_info, data_requested),
);
while let Some(event) = merged.next().await {
if inbound_tx.unbounded_send(event).is_err() {
break; }
}
};
let drain = async {
while let Some(out) = outbound_rx.next().await {
match out {
Outbound::StateChanged(change) => {
let _ = bridge.send_data(change.clone()).await;
let _ = hal.write(change).await;
}
}
}
};
futures::future::join(forward, drain).await;
}
#[cfg(test)]
mod tests {
use super::*;
use arora_bridge::{CommandStream, DataRequestedStream, DeviceInfoStream};
use arora_simple_data_store::NamespacedStore;
use arora_types::data::{Key, StateChange};
use async_trait::async_trait;
use futures::channel::oneshot;
struct UnregisterBridge;
#[async_trait]
impl Bridge for UnregisterBridge {
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn device_info_updated(&self) -> BridgeResult<DeviceInfoStream> {
Ok(Box::pin(futures::stream::once(async { Ok(None) })))
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
async fn data_requested(&self) -> DataRequestedStream {
Box::pin(futures::stream::empty())
}
async fn send_data(&self, _data: StateChange) -> BridgeResult<()> {
Ok(())
}
async fn commands(&self) -> CommandStream {
Box::pin(futures::stream::empty())
}
}
async fn build(bridge: Arc<dyn Bridge>) -> (Runtime, impl Future<Output = ()>) {
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, impl Future<Output = ()>) {
let arora = Arora::start().await.expect("arora starts");
Runtime::with_io_in(arora, Arc::new(arora_hal::FakeHal::new()), bridge, store)
}
async fn drive_until_unregistered(mut runtime: Runtime) {
for _ in 0..1000 {
match runtime.step().expect("step ok") {
StepOutcome::Unregistered => return,
StepOutcome::Live => tokio::time::sleep(std::time::Duration::from_millis(1)).await,
}
}
panic!("runtime never reported unregistered");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn stops_when_unregistered() {
let (runtime, pump) = build(Arc::new(UnregisterBridge)).await;
tokio::spawn(pump);
drive_until_unregistered(runtime).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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, pump) = build(Arc::new(UnregisterBridge)).await;
runtime.queue_groot_xml(xml).expect("tree queues");
tokio::spawn(pump);
drive_until_unregistered(runtime).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_and_update_commands_round_trip() {
let (mut runtime, _pump) = build(Arc::new(UnregisterBridge)).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 {
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn device_info_updated(&self) -> BridgeResult<DeviceInfoStream> {
Ok(Box::pin(futures::stream::empty()))
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
async fn data_requested(&self) -> DataRequestedStream {
Box::pin(futures::stream::empty())
}
async fn send_data(&self, _data: StateChange) -> BridgeResult<()> {
Ok(())
}
async fn commands(&self) -> CommandStream {
Box::pin(futures::stream::empty())
}
}
struct WriteOnce;
impl Behavior 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(flavor = "multi_thread", worker_threads = 2)]
async fn runs_a_queued_non_tree_behavior() {
let (mut runtime, pump) = build(Arc::new(SilentBridge)).await;
tokio::spawn(pump);
runtime.queue_behavior(Box::new(WriteOnce));
runtime.step().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(flavor = "multi_thread", worker_threads = 2)]
async fn list_keys_enumerates_the_store_by_prefix() {
let (mut runtime, _pump) = build(Arc::new(UnregisterBridge)).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(flavor = "multi_thread", worker_threads = 2)]
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, _pump) = build_in(Arc::new(UnregisterBridge), 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().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 Behavior 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(flavor = "multi_thread", worker_threads = 2)]
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, _pump) = build_in(Arc::new(UnregisterBridge), store).await;
runtime.queue_behavior(Box::new(WriteKey {
key: "greeting",
value: Value::String("hi".into()),
}));
assert_eq!(runtime.step().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().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"
);
}
}