Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions docs/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,24 @@ the only source of keys, and the wire body never appears in the key

## Logical time

- Participants track time, watchdogs, and staleness with **logical time**
only - never wall time directly.
- Robot services, drivers, and simulators track state-transition time,
watchdogs, and synchronous-input staleness with **logical time** - never wall
time directly.
The runner owns one `ClockSource` and stamps every `StepContext` and every
`produced_at_ns` from it, so all participants share one time domain
`produced_at_ns` from it, so participants in the robot clock share one domain
([`phoxal/src/participant/clock.rs`](../phoxal/src/participant/clock.rs)).
- Tools are outside that clock. Their process launch contract has no `--clock`
flag or `PHOXAL_CLOCK` binding, and the normal embedding API accepts no clock
argument. They run from external events and host-monotonic timers in every
mode. Tool envelope metadata uses `phoxal::raw::host_time()`; the runner never
gives tools `StepContext` or enrolls them in logical scheduling. Official tool
sources are checked against simulation-clock imports; privileged user-authored
raw-bus tools must uphold the same rule and never decide freshness from robot
logical time.
- A logical-time consumer of asynchronous external input owns retention and
freshness. Keep only the latest bounded value, record its consumer-local
monotonic arrival instant, and sample that value at the logical step. A
logical pause must not accumulate a replay backlog.
- `LogicalTime` is `{ epoch, time_ns }`.
Within an epoch `time_ns` strictly increases; an epoch bump signals a reset.
`RealClock` reads the host-wide UNIX-epoch domain (so cross-process staleness
Expand Down Expand Up @@ -104,7 +117,8 @@ the only source of keys, and the wire body never appears in the key
`#[phoxal::driver]` for per-component-instance participants that can call
`ctx.component()`;
`#[phoxal::tool]` for host-side utilities that inspect `ctx.robot()` (`Api`
defaults to `()` - tools stay raw-bus only);
defaults to `()` - tools stay raw-bus only and host/event driven, with no
logical clock accessor or scheduled step);
`#[phoxal::simulator]` for simulation-only participants.
Each kind embeds its own JSON metadata (id, kind, contract surface) as a
static in a dedicated linker section on the compiled binary
Expand Down
16 changes: 11 additions & 5 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ examples/hello-rover/
robot.yaml # the manifest
robot.dev.yaml # dev-only overlay: local path pins
structure.urdf # the robot's own URDF (chassis + mounts)
worlds/default.wbt # minimal Webots world
components/wheel_drive/ # one robot-local component definition
component.yaml
structure.urdf
Expand All @@ -33,7 +34,7 @@ Both wheels are instances of the same `wheel_drive` component type. Motion is
commanded through the official manual or navigation candidate contracts; the
example intentionally carries no always-moving cruise service.
It is deliberately small: no sensors beyond what the kinematic model needs,
no simulation world, and no hardware driver.
one minimal simulation world, and no hardware driver.
Use it as the shape to copy from, not as a real robot.

## `robot.yaml` anatomy
Expand Down Expand Up @@ -288,10 +289,15 @@ phoxal-cli validate --report --allow-user-service-drift # lower-level structur
`check` resolves every official participant (services, tools, and any
catalog-sourced component drivers) plus every user service and prints
`ok: N participants validated` once the whole graph is coherent.
`hello-rover` has no Webots world checked in (its component uses primitive
URDF geometry, not meshes, so there is nothing to render yet); a project that
wants `phoxal-cli simulate <world>` needs a `worlds/<world>.wbt` file, the
same way [`robot-v1`](https://github.com/phoxal/robot-v1) does.
`hello-rover` includes a minimal Webots world:

```sh
phoxal-cli simulation run default --env dev
```

The CLI stages the authored world and injects the generated rover and Webots
controllers. Keep additional example worlds equally small; do not use a private
robot as the framework harness.

## Where to go from here

Expand Down
21 changes: 21 additions & 0 deletions examples/hello-rover/worlds/default.wbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#VRML_SIM R2025a utf8

EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/objects/floors/protos/RectangleArena.proto"
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/objects/backgrounds/protos/TexturedBackground.proto"
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/objects/backgrounds/protos/TexturedBackgroundLight.proto"

WorldInfo {
basicTimeStep 16
}
Viewpoint {
orientation -0.36 -0.22 0.91 5.05
position -2.5 3.5 2.2
}
TexturedBackground {
}
TexturedBackgroundLight {
}
RectangleArena {
floorSize 4 4
wallHeight 1
}
13 changes: 13 additions & 0 deletions phoxal-macros/src/authoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,17 @@ impl ParticipantKind {
}
}

fn launch_policy(self, phoxal: &TokenStream) -> TokenStream {
match self {
ParticipantKind::Tool => {
quote!(#phoxal::participant::launch::ToolParticipantLaunch)
}
ParticipantKind::Service | ParticipantKind::Driver | ParticipantKind::Simulator => {
quote!(#phoxal::participant::launch::ClockedParticipantLaunch)
}
}
}

/// Default `Api` type when `api = …` is not given: tools stay raw-bus
/// only (decided 2026-07-09 - no typed tool `Api` until a real need
/// appears), every other kind defaults to the local `Api` struct.
Expand Down Expand Up @@ -810,6 +821,7 @@ pub fn expand_participant(
let phoxal = phoxal();
let artifact_kind = kind.artifact_kind();
let participant_class = kind.participant_class();
let launch_policy = kind.launch_policy(&phoxal);
let marker = kind.marker_impl(&phoxal, struct_name);
let metadata_const_ident = Ident::new(
&format!(
Expand Down Expand Up @@ -841,6 +853,7 @@ pub fn expand_participant(
const KIND: &'static str = #artifact_kind;
const PARTICIPANT_CLASS: &'static str = #participant_class;
const ID: &'static str = #id;
type LaunchPolicy = #launch_policy;
type Config = #config_ty;
type Api = #api_ty;
}
Expand Down
12 changes: 12 additions & 0 deletions phoxal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ Key rules the example shows:

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.

`#[phoxal::tool]` is intentionally outside the robot clock. Tools use managed
host/event loops, host-monotonic timers for cadence and freshness, and
`phoxal::raw::host_time()` only when a generic bus envelope timestamp is
required. The macro rejects `#[step]`, the setup context exposes no clock, and
the tool process launch has no clock option. The normal embedding API likewise
accepts no clock argument; deterministic clock injection is restricted to typed
graph participants. Official tool sources are additionally checked against
importing or subscribing to simulation-clock surfaces; user-authored tools must
preserve the same boundary when using the privileged raw bus. A service that
consumes asynchronous tool input keeps a bounded latest value with
consumer-local monotonic arrival time and samples it at its own logical step.

## Modules

A participant depends on two crates: the `phoxal` engine (the modules below) and the `phoxal-api` contract tree.
Expand Down
15 changes: 14 additions & 1 deletion phoxal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,21 @@ pub mod bus {
/// graph checker still includes their contracts, but never lets their raw
/// access satisfy checked topology.
pub mod raw {
pub use crate::participant::runner::run_with_bus;
pub use crate::participant::runner::{run_with_bus, run_with_bus_clock};
pub use phoxal_bus::*;

/// Current host wall time for privileged tools and bridges that need an
/// envelope timestamp without joining a robot's logical clock.
///
/// This is metadata time only. Scheduling periodic work should use a host
/// monotonic timer such as [`std::time::Instant`] or
/// [`tokio::time::interval`].
pub fn host_time() -> LogicalTime {
let elapsed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
LogicalTime::new(0, u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX))
}
}

/// The framework result type (`anyhow`-backed). Authoring code uses bare
Expand Down
15 changes: 7 additions & 8 deletions phoxal/src/participant/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ pub trait Participant: Sized + Send + 'static {
const PARTICIPANT_CLASS: &'static str;
/// The participant id (`id = "…"`, default kebab of the type name).
const ID: &'static str;
/// The process launch contract. Tools use a clockless policy; checked graph
/// participants use the configurable robot-clock policy.
#[doc(hidden)]
type LaunchPolicy: crate::participant::launch::ParticipantLaunchPolicy;
/// The participant's typed config (`robot.yaml` input).
type Config: ParticipantConfig;
/// The participant's bus-facing contract surface (`()` for a raw-bus
Expand Down Expand Up @@ -660,23 +664,18 @@ impl<R: Participant + IsSimulator> SetupContextSimulatorExt for SetupContext<R>
}

/// Tool-only `SetupContext` accessor (`R: Participant + IsTool`). Tools stay
/// raw-bus only (decided 2026-07-09), so this is their sole IO seam.
/// raw-bus only (decided 2026-07-09), so this is their sole IO seam. Tools do
/// not receive the runner clock: host timers and [`crate::raw::host_time`] keep
/// them outside robot logical/simulation time.
pub trait SetupContextToolExt {
/// Clone the runner-owned raw bus for privileged tool internals. The bus is
/// already open from the launch contract, so a tool does not reparse launch
/// env or open an unrelated session.
fn raw_bus(&self) -> Bus;

/// Clone the logical clock owned by the participant runner.
fn clock(&self) -> std::sync::Arc<dyn crate::participant::clock::ClockSource>;
}

impl<R: Participant + IsTool> SetupContextToolExt for SetupContext<R> {
fn raw_bus(&self) -> Bus {
self.bus().clone()
}

fn clock(&self) -> std::sync::Arc<dyn crate::participant::clock::ClockSource> {
self.clock_source()
}
}
10 changes: 0 additions & 10 deletions phoxal/src/participant/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use std::time::Duration;

use crate::bus::{LogicalTime, OwnerCap};
use crate::model::v0::Robot;
use crate::participant::clock::ClockSource;
use crate::participant::managed::{ManagedTaskPolicy, ManagedTasks};
use phoxal_bus::Bus;

Expand All @@ -18,7 +17,6 @@ use phoxal_bus::Bus;
/// enforces.
pub struct SetupContext<R> {
bus: Bus,
clock: Arc<dyn ClockSource>,
owner_cap: OwnerCap,
robot: Option<Arc<Robot>>,
robot_root: Option<PathBuf>,
Expand All @@ -34,15 +32,13 @@ pub struct SetupContext<R> {
impl<R> SetupContext<R> {
pub(crate) fn new(
bus: Bus,
clock: Arc<dyn ClockSource>,
owner_cap: OwnerCap,
robot: Option<Arc<Robot>>,
robot_root: Option<PathBuf>,
component_instance: Option<String>,
) -> Self {
SetupContext {
bus,
clock,
owner_cap,
robot,
robot_root,
Expand Down Expand Up @@ -114,12 +110,6 @@ impl<R> SetupContext<R> {
&self.bus
}

/// Clone the runner-owned logical clock so privileged raw-bus tools can
/// stamp messages in the active real or simulation time domain.
pub(crate) fn clock_source(&self) -> Arc<dyn ClockSource> {
Arc::clone(&self.clock)
}

/// The runner-minted owner capability (plan #00 L2). In-crate accessor the
/// NEW model's `SetupContextApiExt::owner_capability` (`participant::api`)
/// reads the raw field through.
Expand Down
Loading
Loading