arora 0.1.0

Opinionated Arora runtime: an engine pre-wired with the native behavior-tree control nodes and the Semio backend.
//! The arora launcher — the reusable entry point a device-specific build calls
//! to run an arora instance with **its own** HAL, bridge, and data store.
//!
//! Customization happens from the *outside*: rather than `arora` carrying a
//! feature flag per robot, a device-specific binary depends on `arora` plus its
//! custom HAL (and bridge) crates and calls [`launch`]:
//!
//! ```no_run
//! # use std::sync::Arc;
//! # use arora_simple_data_store::SimpleDataStore;
//! # #[cfg(feature = "native")]
//! # fn main() -> anyhow::Result<()> {
//! // a hypothetical `arora-ur5` binary:
//! arora::launch(
//!     Arc::new(my_hal::Ur5Hal::new()),
//!     Arc::new(my_bridge::Studio::new()),
//!     SimpleDataStore::new(),
//! )
//! # }
//! # #[cfg(not(feature = "native"))] fn main() {}
//! # mod my_hal { pub struct Ur5Hal; impl Ur5Hal { pub fn new() -> arora_hal::FakeHal { arora_hal::FakeHal::new() } } }
//! # mod my_bridge { pub struct Studio; impl Studio { pub fn new() -> arora_bridge::FakeBridge { arora_bridge::FakeBridge::new() } } }
//! ```
//!
//! The default `arora` binary calls this with the in-process fakes and a fresh
//! store. The launcher owns the parts every device shares (engine startup, the
//! run loop, and — as they are migrated from studio-bridge's `headless` —
//! CLI/env, config, token storage and device-info sync); the device-specific
//! binary only injects the HAL, bridge, and store.

#[cfg(feature = "native")]
use std::sync::Arc;

#[cfg(feature = "native")]
use anyhow::{anyhow, Context, Result};
#[cfg(feature = "native")]
use arora_bridge::Bridge;
#[cfg(feature = "native")]
use arora_hal::Hal;
#[cfg(feature = "native")]
use arora_simple_data_store::SimpleDataStore;

#[cfg(feature = "native")]
use crate::runtime::Runtime;
#[cfg(feature = "native")]
use crate::Arora;

/// Run an arora instance with the given HAL, bridge, and data store until the
/// device is unregistered (or the process is interrupted).
///
/// Starts the engine (with the basic behavior-tree control nodes wired
/// natively), wires the portable [`Runtime`] around the injected HAL + bridge
/// over `store`, queues an optional Groot tree given as the first CLI argument,
/// spawns the asynchronous io pump on a Tokio runtime, then drives the
/// synchronous step loop on this thread.
///
/// Pass a freshly created [`SimpleDataStore`] for a self-contained device, or a
/// clone of a shared one to mutualize the blackboard across runtimes (e.g.
/// Studio handing one store to every spawned device).
///
/// This is the native launcher; on the web, drive the runtime via
/// `arora-web`'s `AroraRuntime` instead.
#[cfg(feature = "native")]
pub fn launch(hal: Arc<dyn Hal>, bridge: Arc<dyn Bridge>, store: SimpleDataStore) -> Result<()> {
    launch_with(hal, store, move || async move { Ok(bridge) })
}

/// Like [`launch`], but constructs the bridge inside arora's Tokio runtime via
/// an asynchronous builder.
///
/// A bridge whose construction is `async` and whose background tasks must live
/// on the runtime that drives it — such as the studio-bridge connector's
/// `ZenohDeviceClient` — can't be built before `launch` creates its runtime, and
/// must not be built on a throwaway one it would outlive. This variant runs the
/// builder on arora's runtime, so the bridge and its tasks share that runtime's
/// lifetime.
#[cfg(feature = "native")]
pub fn launch_with<F, Fut>(hal: Arc<dyn Hal>, store: SimpleDataStore, make_bridge: F) -> Result<()>
where
    F: FnOnce() -> Fut,
    Fut: std::future::Future<Output = Result<Arc<dyn Bridge>>>,
{
    // The async setup runs inside a Tokio runtime; the step loop that drives the
    // engine is synchronous and runs on this (main) thread afterwards — the wasm
    // executor manages its own blocking runtime and must not be ticked inside
    // Tokio.
    let tokio = tokio::runtime::Runtime::new().context("failed to start Tokio runtime")?;
    let (mut runtime, io) = tokio.block_on(async {
        let bridge = make_bridge().await.context("failed to build the bridge")?;
        let arora = Arora::start().await.context("failed to start Arora")?;
        // The public launcher API still takes a concrete `SimpleDataStore`; the
        // runtime holds `Arc<dyn DataStore>`, so wrap it here.
        let store: Arc<dyn arora_types::data::DataStore> = Arc::new(store);
        Ok::<_, anyhow::Error>(Runtime::with_io_in(arora, hal, bridge, store))
    })?;
    println!("arora: engine started; native behavior-tree control nodes ready.");

    if let Some(path) = std::env::args().nth(1) {
        let xml = std::fs::read_to_string(&path)
            .with_context(|| format!("could not read Groot file {path}"))?;
        runtime
            .queue_groot_xml(&xml)
            .map_err(|e| anyhow!("failed to queue behavior tree from {path}: {e}"))?;
        println!("arora: queued behavior tree from {path}");
    }

    tokio.spawn(io);
    println!("arora: running — Ctrl-C to stop.");
    runtime.run().map_err(|e| anyhow!("runtime error: {e}"))
}