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’sApihandle 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 noschema_id. - Participants are authored, not wired. You write a
Configstruct, anApihandle struct, a state struct, and animpl;#[derive(Config)]/#[derive(Api)]plus#[phoxal::service|driver|simulator|tool]and#[phoxal::behavior]derive the static metadata, andrunturns the type into a binary. Useservicefor ordinary robot participants,driverfor a participant launched once perrobot.componentsentry,toolfor host-side utilities, andsimulatorfor 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;Apistruct 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 theApistruct’s handle fields (Publisher<T>,Latest<T>,Subscriber<T>,Querier<Req, Resp>,Server<Req, Resp>).#[phoxal::service(id = "…")]links the participant state struct to itsConfig/Apitypes and records its identity.- All handles are built in
#[setup]from api-local topic builders (v1::topic::new().drive().state()) and returned as theApivalue alongside the participant state. #[step(hz = ...)]is the scheduled control loop; the runner owns timing and delivers logical time viaStepContext, and&mut Self::Apialongside&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, callphoxal::tokio::run::<R>().await.
The four authoring kinds share the same metadata path but describe different runtime roles:
serviceis the ordinary typed participant surface.driveris launched once perrobot.componentsentry. Only a driver can callSetupContextDriverExt::componentto read the bound component instance.toolis for host-side utilities that inspect the robot model throughSetupContextApiExt::robot. Privileged raw-bus access lives underrawso it is never part of the default checked participant surface.simulatoris 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-apicrate (phoxal_api::v1, …) - the versioned API modules: version-local wire bodies, theApiVersion/ContractBodytraits, and the api-local topic builders, all generated byphoxal_api_tree!. A participant imports it directly withuse 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 withuse phoxal::prelude::*;: the handle types,SetupContext/StepContext, andResult.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,BusMetadataattachment, body-typed handles, and side-brandedTopicvalues.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/v0wire schema. - check
- Graph validation for Phoxal participant graphs (D59/D63/D1), plus the
deployment coherence pass (coherence-gate design doc,
organizationtmp/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 bareResult<T>via theprelude.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/Apitypes as a component driver. The driver-shaped counterpart to [service]. - service
- Link a participant state struct to its
Config/Apitypes as a checked service. Link a participant state struct to itsConfig/Apitypes as a checked service. Defaults to the localConfig/Apitype names; override with#[phoxal::service(id = "…", config = Type, api = Type)]. - simulator
- Link a participant state struct to its
Config/Apitypes as a simulation participant. The simulator-shaped counterpart to [service]. - tool
- Link a participant state struct to its
Configas a raw-bus tool (Apidefaults to()- tools stay raw-bus only). The tool-shaped counterpart to [service].ApiandConfigdefault to()- tools stay raw-bus only (decided 2026-07-09), and a configless tool can launch withoutPHOXAL_CONFIG. An explicitconfig = Typeremains required at launch unless that type itself acceptsnull(for example,Option<T>).
Derive Macros§
- Api
- Derive the bus-facing contract surface from an
Apihandle struct’s fields. Seephoxal::participant::api. Derive the bus-facing contract surface from anApihandle struct. Seephoxal::participant::apifor the trait shape. - Config
- Derive participant config identity from a
Configstruct (schema materialization is a later slice - seephoxal::participant::api::ParticipantConfig). Derive a compile-time Draft 2020-12 JSON Schema from the same supported#[serde(...)]attributes used byDeserialize:rename,rename_all,default, anddeny_unknown_fields. Unsupported Serde attributes are a compile error rather than an approximate schema.