Skip to main content

Crate phoxal

Crate phoxal 

Source
Expand description

§phoxal

A production-oriented framework for autonomous robots.

Phoxal gives a robot a small, strongly-typed core: a contract bus over Zenoh, stable v1 plus evolving preview v2 contracts, and a participant authoring model where one struct plus a couple of attribute macros is a complete service, driver, tool, or simulator. 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 version-qualified contract name. Handles are body-typed (Publisher<T>, Subscriber<T>, Latest<T>, Querier<Req, Resp>), so the compiler - not a late check - rejects sending the wrong type on a topic.
  • No per-participant API version ceiling. API versions are conventional vN modules (phoxal_api::v1, …), not semver crates. A participant’s Api handle struct may mix bodies from different versions freely across its fields - compatibility is per-contract name identity, realized on the wire by the version-qualified key (D1); there is no schema_id.
  • Participants are authored, not wired. You write a Config struct, an Api handle struct, a state struct, and an impl; #[derive(Config)] / #[derive(Api)] plus #[phoxal::service|driver|simulator|tool] and #[phoxal::behavior] derive the static metadata, and run turns the type into a binary. Use service for ordinary robot participants, driver for a participant launched once per robot.components entry, tool for host-side utilities, and simulator for simulation-only participants.

§Author a participant

A participant is a Config struct, an Api struct of typed bus handles, a state struct, and one annotated inherent impl. This is the whole getting-started surface:

use phoxal_api::v1;
use phoxal::prelude::*;

#[derive(serde::Deserialize, phoxal::Config)]
struct Config {}

#[derive(phoxal::Api)]
struct Api {
    state:  Latest<v1::drive::State>,    // keep-last view of the drive state
    target: Publisher<v1::drive::Target>, // commanded drive target
}

#[phoxal::service(id = "avoid-obstacles")]
struct AvoidObstacles;

#[phoxal::behavior]
impl AvoidObstacles {
    #[setup]
    async fn setup(ctx: &mut SetupContext<Self>, _config: Self::Config) -> Result<(Self, Self::Api)> {
        Ok((Self, Self::Api {
            state:  ctx.latest(v1::topic::new().drive().state()).await?,
            target: ctx.publisher(v1::topic::new().drive().target()).await?,
        }))
    }

    #[step(hz = 50)]
    async fn step(&mut self, api: &mut Self::Api, step: StepContext) -> Result<()> {
        let now = step.time();
        api.target.publish_at(now, v1::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::v1; brings the versioned API module into scope; Api struct fields name version-qualified bodies (v1::drive::Target) directly, so a participant may mix versions across fields with no version-ceiling attribute to keep in sync.
  • #[derive(phoxal::Api)] derives the bus-facing contract surface from the Api struct’s handle fields (Publisher<T>, Latest<T>, Subscriber<T>, Querier<Req, Resp>, Server<Req, Resp>).
  • #[phoxal::service(id = "…")] links the participant state struct to its Config/Api types and records its identity.
  • All handles are built in #[setup] from api-local topic builders (v1::topic::new().drive().state()) and returned as the Api value alongside the participant state.
  • #[step(hz = ...)] is the scheduled control loop; the runner owns timing and delivers logical time via StepContext, and &mut Self::Api alongside &mut self. 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, call phoxal::tokio::run::<R>().await.

The four authoring kinds share the same metadata path but describe different runtime roles:

  • service is the ordinary typed participant surface.
  • driver is launched once per robot.components entry. Only a driver can call SetupContextDriverExt::component to read the bound component instance.
  • tool is for host-side utilities that inspect the robot model through SetupContextApiExt::robot. Privileged raw-bus access lives under raw so it is never part of the default checked participant surface.
  • simulator is a normal participant for simulation-only processes. It carries a distinct kind and marker for simulation clock ownership.

Worked examples live in phoxal/examples/.

§Where to look next

  • The phoxal-api crate (phoxal_api::v1, …) - the versioned API modules: version-local wire bodies, the ApiVersion / ContractBody traits, and the api-local topic builders, all generated by phoxal_api_tree!. A participant imports it directly with use phoxal_api::v1 as api;. The runner also links it for framework-owned out-of-band infrastructure contracts such as bus logs.
  • prelude - everything a participant author imports with use phoxal::prelude::*;: the handle types, SetupContext / StepContext, and Result.
  • participant - the authoring surface behind the macros: the static metadata traits, the contexts, the clock and scheduler, and the runner (run / tokio::run).
  • bus - the typed contract vocabulary normal participants need: the key scheme, MessagePack codec, BusMetadata attachment, body-typed handles, and side-branded Topic values.
  • raw - the explicit privileged/tooling surface for opening a raw bus, accessing the underlying session, or embedding runtimes on a caller-owned bus.
  • model - the authored manifest schemas (robot.yaml, structure.urdf, component.yaml, …) that participants and the CLI parse.
  • The official service set ships alongside this crate in the workspace service/ tree (drive, localize, map, safety, …): full platform participants authored on exactly this surface, useful as reference reading.

Re-exports§

pub use participant::run;

Modules§

bus
Typed contract and handle vocabulary for normal participant authoring.
catalog
The shared phoxal.catalog/v0 wire schema.
check
Graph validation for Phoxal participant graphs (D59/D63/D1), plus the deployment coherence pass (coherence-gate design doc, organization tmp/coherence-gate/readme.md).
model
Authored manifest model.
participant
The participant engine: static metadata traits, contexts, clock, launch contract, and the runner.
prelude
Everything a participant author imports with use phoxal::prelude::*;.
raw
Explicit raw/permissive bus surface for privileged participants, tooling, bridges, and framework tests.
tokio
Async host runner entrypoint for custom Tokio mains (phoxal::tokio::run::<Participant>().await).
util

Macros§

phoxal_api_tree
Declare a versioned API tree of version-local wire bodies + topics.

Type Aliases§

Result
The framework result type (anyhow-backed). Authoring code uses bare Result<T> via the prelude. Result<T, Error>

Attribute Macros§

behavior
The bare #[phoxal::behavior] attribute for a participant’s inherent impl. The bare #[phoxal::behavior] attribute on a participant’s inherent impl.
driver
Link a participant state struct to its Config/Api types as a component driver. The driver-shaped counterpart to [service].
service
Link a participant state struct to its Config/Api types as a checked service. Link a participant state struct to its Config/Api types as a checked service. Defaults to the local Config/Api type names; override with #[phoxal::service(id = "…", config = Type, api = Type)].
simulator
Link a participant state struct to its Config/Api types as a simulation participant. The simulator-shaped counterpart to [service].
tool
Link a participant state struct to its Config as a raw-bus tool (Api defaults to () - tools stay raw-bus only). The tool-shaped counterpart to [service]. Api and Config default to () - tools stay raw-bus only (decided 2026-07-09), and a configless tool can launch without PHOXAL_CONFIG. An explicit config = Type remains required at launch unless that type itself accepts null (for example, Option<T>).

Derive Macros§

Api
Derive the bus-facing contract surface from an Api handle struct’s fields. See phoxal::participant::api. Derive the bus-facing contract surface from an Api handle struct. See phoxal::participant::api for the trait shape.
Config
Derive participant config identity from a Config struct (schema materialization is a later slice - see phoxal::participant::api::ParticipantConfig). Derive a compile-time Draft 2020-12 JSON Schema from the same supported #[serde(...)] attributes used by Deserialize: rename, rename_all, default, and deny_unknown_fields. Unsupported Serde attributes are a compile error rather than an approximate schema.