# phoxal

A production-oriented framework for autonomous robots, shipped as one crate.

Phoxal gives a robot a small, strongly-typed core: a contract bus over Zenoh, a single dated API-version contract set, and a derive-based participant authoring model.
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.

> **Greenfield rewrite in progress** (`tmp/framework-rewrite`).
> The bus + participant machinery and the macros are new; the official service/simulator/tool crates are being re-ported onto this surface and some are temporarily out of the workspace.

```toml
[dependencies]
phoxal = "0.17"      # the engine: runner, derives, SetupContext, prelude
phoxal-api = "0.17"  # the contract tree: `use phoxal_api::y2026_1 as api;`
```

## The authoring model

A service is **one struct of typed handles + one annotated inherent impl**.
The struct declares the contracts it uses (as handle fields) and the one API version it runs against; the impl declares the lifecycle.

```rust,ignore
use phoxal_api::y2026_1 as api;
use phoxal::prelude::*;

#[derive(phoxal::Service)]
#[phoxal(id = "avoid-obstacles", api = y2026_1, config = Config)]
struct AvoidObstacles {
    state:  Latest<api::drive::State>,
    target: Publisher<api::drive::Target>,
    cruise_linear_x_mps: f32,
}

#[derive(serde::Deserialize, phoxal::schemars::JsonSchema)]
struct Config {
    cruise_linear_x_mps: f32,
}

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

    #[step(hz = 50)]
    async fn step(&mut self, step: StepContext) -> Result<()> {
        let now = step.time();
        // read inputs, publish version-local bodies
        self.target.publish_at(now, api::drive::Target {
            linear_x_mps: self.cruise_linear_x_mps,
            angular_z_radps: 0.0,
            curvature_limit_radpm: None,
        }).await?;
        Ok(())
    }
}

fn main() -> phoxal::Result<()> { phoxal::run::<AvoidObstacles>() }
```

Key rules the example shows:

- Import exactly one dated API module (`use phoxal_api::y2026_1 as api;`) and declare it on the derive (`#[phoxal(api = y2026_1)]`, **mandatory**).
- Handle fields use version-local bodies: `Publisher<api::drive::Target>`, `Latest<api::drive::State>`.
  The derive emits a `ContractBody<Api = R::Api>` assertion, so a body from another API version is a **compile error**.
- Topics are api-local: `api::topic::new().drive().state()`.
- The wire body is the plain payload; `api_version`, family, and codec ride bus metadata.
  Normal participants never open Zenoh - the runner opens the launch-selected bus profile before `#[setup]`.
- `config` is for **user participants only**.
  Official participants take no `config` param and read the robot model through `ctx.robot()`.
- `cargo run --example runtime_control_loop emit-apis` prints the participant's static metadata as one JSON document (the `emit-apis` subcommand) and exits.

The runner also owns the rest of the lifecycle: `#[step(hz = ...)]` is the scheduled control loop, `#[server]` / `#[server_snapshot]` serve queries, and `#[shutdown]` runs graceful park/stop/flush before the bus closes.

## Modules

A participant depends on two crates: the `phoxal` engine (the modules below) and the `phoxal-api` contract tree.

| Module | What it is |
|---|---|
| `phoxal_api` (separate crate) | The dated API-version modules (`phoxal_api::y2026_1`, …) generated by `phoxal_api_tree!` from a tree of nested nodes: the marker `enum Api` (`ApiVersion`), version-local wire bodies + their `ContractBody` impls, and the api-local `topic` builders. Imported directly with `use phoxal_api::y2026_1 as api;`. |
| [`phoxal::prelude`] | Everything a participant author imports with `use phoxal::prelude::*;`: the handle types, `SetupContext`/`StepContext`, and `Result`. |
| [`phoxal::bus`] | Re-export of the `phoxal-bus` crate (the ABI floor): the Zenoh-native `bus_abi` boundary: the key scheme `<namespace>/robots/<robot-id>/<topic>`, the MessagePack codec, the `BusMetadata` attachment, and the body-typed handles `Publisher`/`Subscriber`/`Latest`/`Querier`. |
| [`phoxal::participant`] | The static metadata traits the macros target, `SetupContext`/`StepContext`/`ShutdownContext`, the clock (`RealClock`/`TestClock`) + scheduler, `ParticipantLaunch`, `emit-apis`, and the runner (`run` / `tokio::run`). |
| [`phoxal::model`] | Authored manifest schemas (`robot.yaml`, `structure.urdf`, `component.yaml`, …). |

## Authoring the API tree

The `phoxal_api` modules (in the `phoxal-api` crate) are generated by `phoxal_api_tree!` from a tree of **nested nodes**.
A node is `name { … }` (static) or `name(var) { … }` (dynamic, where `var` is a key segment filled at build time), nestable to any depth.
A node body holds any mix of `struct`/`enum` type declarations, `topic` declarations, and child nodes.
Each topic declares a **role**: `topic <leaf>: command <Body>;` (a control input the owning service subscribes), `topic <leaf>: state <Body>;` (telemetry the owning service publishes), or `topic <leaf>: query <Req> => <Resp>;` (request/response).
`command` and `state` are both pub/sub on the wire; the role selects the side brand of the generated builders (L1), so the public client builder (`api::topic::new()`) and the `internal` owner builder (`api::topic::internal::new(cap)`) return different branded topics (`Publish`/`Subscribe`) and taking the wrong side does not compile.
The owner builder additionally requires the runner-minted `OwnerCap` (L2): pass `ctx.owner_capability()`, so owning a topic is a deliberate, capability-gated opt-in rather than something that can happen by accident.
There are no key-template strings and no topic parameter lists; dynamism comes entirely from the `(var)` nodes on the path.

```rust,ignore
phoxal_api_tree! {
    version y2026_1 {
        drive {                                  // static node
            struct Target { linear_x_mps: f32, angular_z_radps: f32 }
            topic target: command Target;        // key drive/target, FAMILY drive::Target
            struct State { /* … */ }
            topic state: state State;            // owner-published telemetry
        }
        component(instance) {                    // literal "component" + var {instance}
            motor(capability) {                  // literal "motor"     + var {capability}
                enum Command { Velocity(f32), Torque(f32), Stop }
                topic command: command Command;
            }
        }
    }
}
```

Everything a topic exposes is **derived from the node path** `n1 … nk` to its leaf (each `ni` has a `name` and an optional `var`):

- **Topic key** (`ContractBody::TOPIC`, versionless): each node emits `name`, or `name/{var}` for a dynamic node, joined by `/`, then `/<leaf>`.
  So `drive` → `target` is `drive/target`, and `component(instance)` → `motor(capability)` → `command` is `component/{instance}/motor/{capability}/command`.
- **Module path / type location**: each node becomes a nested `pub mod name`, and a node's types + their `ContractBody` impls live in that module.
  Variables never appear in the module path, so the body above is `phoxal_api::y2026_1::component::motor::Command`.
- **FAMILY** (`ContractBody::FAMILY`): the node names joined by `::`, then `::TypeName` - e.g. `drive::Target`, `component::motor::Command`.
- **Builder**: `api::topic::new().n1(var?)…nk(var?).leaf()`.
  A dynamic node's method takes its var as `impl Display` (`*` / `**` stay valid for subscribe); the leaf method yields the typed `Topic`.
  So `api::topic::new().component("front_left").motor("drive").command()` builds the key `component/front_left/motor/drive/command`.

Each node is **self-contained**: it declares its own copy of every type it uses, with **no `super::`** and no shared/common module.
Because the node path disambiguates, names are path-local - `component::imu::Sample` and `component::range::Sample` are distinct types that may safely repeat field names.
Duplicating an identical type across sibling nodes is intentional, not a smell.

A later `version yNNNN_M extends yNNNN_K { … }` re-emits the parent tree as **fresh types** under the child version (same FAMILY/TOPIC, a different `Api`), overriding a type by ident or a topic by leaf and appending wholly new nodes - so an inherited node never needs to be retyped.

## Entrypoints

- Default (blocking): `fn main() -> phoxal::Result<()> { phoxal::run::<R>() }`.
- Advanced (async): `phoxal::tokio::run::<R>().await` for custom Tokio mains.

## Official service set

The complete platform participant set ships alongside this crate in the workspace `service/` tree (`drive`, `localize`, `map`, `safety`, …).
They are full official participants authored on exactly this surface, and they double as worked reference reading.
Neutral teaching examples live in `phoxal/examples/` (`runtime_control_loop`, `runtime_query_server`, `runtime_snapshot_server`, `runtime_async_entrypoint`).

## Status

Pre-1.0, building in public.
One API version per robot graph (`#[phoxal(api = y2026_1)]`); the wire body is the plain payload and the version identity rides bus metadata, never the key.
Compatibility is checked at build/check time (`emit-apis` + `phoxal-cli check`), never by introspecting a running binary.

## License

AGPL-3.0-only.
A commercial license is available - see the repository.
