Expand description
§phoxal
A production-oriented framework for autonomous robots.
Phoxal gives a robot a small, strongly-typed core: a contract bus over Zenoh, a single dated API version per robot graph, and a runtime authoring model where one struct of typed handles plus a couple of attribute macros is a complete participant. The framework owns the awkward parts - argument parsing, bus connection, scheduling, query serving, shutdown, and health - so the code you write is the robot’s behavior, not its plumbing.
Three ideas hold it together:
- A typed contract bus. Every message is a plain serde body bound to one
contract family and one API version. Handles are body-typed
(
Publisher<T>,Subscriber<T>,Latest<T>,Querier<Req, Resp>), so the compiler - not a runtime check - rejects sending the wrong type on a topic. - One dated API version per graph. API versions are dated modules
(
api::y2026_1, …), not semver crates. A runtime authors against exactly one of them; mixing bodies from two versions is a compile error. - Runtimes are authored, not wired. You write a struct and an
impl; the#[derive(Runtime)]and#[phoxal::runtime]macros derive the static metadata, andrunturns the type into a binary.
§Author a runtime
A runtime is one struct of typed handles and one annotated inherent impl.
The struct declares the contracts it uses (as handle fields) and its one API
version; the impl declares the lifecycle. This is the whole getting-started
surface:
use phoxal::api::y2026_1 as api; // select ONE dated API version
use phoxal::prelude::*;
#[derive(phoxal::Runtime)]
#[phoxal(id = "avoid-obstacles", api = y2026_1)]
struct AvoidObstacles {
state: Latest<api::drive::State>, // keep-last view of the drive state
target: Publisher<api::drive::Target>, // commanded drive target
}
#[phoxal::runtime]
impl AvoidObstacles {
#[setup]
async fn setup(ctx: &mut SetupContext<Self>) -> Result<Self> {
Ok(Self {
// api-local topic builders bind each handle to this API version
state: ctx.subscribe(api::topic::new().drive().state()).latest().await?,
target: ctx.publisher(api::topic::new().drive().target()).await?,
})
}
#[step(hz = 50)]
async fn step(&mut self, step: StepContext) -> Result<()> {
let now = step.time();
self.target.publish_at(now, api::drive::Target {
linear_x_mps: 0.2,
angular_z_radps: 0.0,
curvature_limit_radpm: None,
}).await?;
Ok(())
}
}
fn main() -> phoxal::Result<()> { phoxal::run::<AvoidObstacles>() }What each piece does:
use phoxal::api::y2026_1 as api;and#[phoxal(api = y2026_1)]pick the runtime’s single API version. Every handle body type comes throughapi::…; switching versions is a one-line edit at the top plus the attribute.- Handle fields name version-local bodies (
Publisher<api::drive::Target>,Latest<api::drive::State>). A body from another API version does not compile. - All handles are built in
#[setup]from api-local topic builders (api::topic::new().drive().state()); long-lived ones become struct fields. #[step(hz = ...)]is the scheduled control loop; the runner owns timing and delivers logical time viaStepContext. Query servers use#[server]/#[server_snapshot], and#[shutdown]runs graceful cleanup before the bus closes.fn main() -> phoxal::Result<()> { phoxal::run::<R>() }is the default blocking entrypoint. For a custom Tokio main, callphoxal::tokio::run::<R>().await.
The runner also exposes an emit-apis subcommand
(cargo run --example runtime_control_loop emit-apis) that prints a runtime’s
static metadata as one JSON document and exits, without opening the bus. Worked
examples live in phoxal/examples/.
§Where to look next
api- the dated API-version modules (y2026_1, …): version-local wire bodies, theApiVersion/ContractBodytraits, and the api-local topic builders, all generated byphoxal_api_tree!.prelude- everything a runtime author imports withuse phoxal::prelude::*;: the handle types,SetupContext/StepContext, andResult.runtime- the authoring surface behind the macros: the static metadata traits, the contexts, the clock and scheduler, and the runner (run/tokio::run).bus- the Zenoh-nativebus_abiboundary: the key scheme, the MessagePack codec, theBusMetadataattachment, and the body-typed handles.model- the authored manifest schemas (robot.yaml,structure.urdf,component.yaml, …) that runtimes and the CLI parse.- The official runtime set ships alongside this crate in the workspace
runtime/tree (drive,localize,map,safety, …): full platform runtimes authored on exactly this surface, useful as reference reading.
Re-exports§
Modules§
- api
- The single API layer (D60/D61).
- bus
- The Zenoh-native
bus_abiboundary (D26/D62). - model
- Authored manifest model.
- prelude
- Everything a runtime author imports with
use phoxal::prelude::*;. - runtime
- The runtime engine: static metadata traits, contexts, clock, launch contract,
emit-apis, and the runner. - tokio
- Async host runner entrypoints for custom Tokio mains
(
phoxal::tokio::run::<Runtime>().await). - util
Macros§
- phoxal_
api_ tree - Declare a dated API-version tree of version-local wire bodies + topics.
Type Aliases§
- Result
- The framework result type (
anyhow-backed). Authoring code uses bareResult<T>via theprelude.Result<T, Error>
Attribute Macros§
- runtime
- The bare
#[phoxal::runtime]attribute for a runtime’s inherent impl. The bare#[phoxal::runtime]attribute on a runtime’s inherent impl.
Derive Macros§
- Runtime
- Derive the static metadata for a runtime struct. See the crate docs. Derive the static metadata for a runtime struct.