Skip to content

feat!: cut the controller over to phoxal and SimulatorSession - #7

Merged
jBernavaPrah merged 4 commits into
mainfrom
refactor/single-crate
Aug 19, 2026
Merged

feat!: cut the controller over to phoxal and SimulatorSession#7
jBernavaPrah merged 4 commits into
mainfrom
refactor/single-crate

Conversation

@jBernavaPrah

Copy link
Copy Markdown
Contributor

What

The Webots controller depends on the framework as one library with the simulator profile:

phoxal = { version = "0.66", default-features = false, features = ["simulator"] }
  • phoxal-bus, phoxal-protocol, phoxal-model, phoxal-bundle are gone (framework PR refactor(framework)!: one phoxal library with consumer profiles, a Rust-declared api tree, and phoxal::session framework#466).
  • Raw bus ownership (BusOwner, BusConfig::for_external, probe_routers, TimelineAuthority, WorldClockPublisher, delegated Ready declaration) is replaced by phoxal::simulator::SimulatorSession and WorldTime (taken once, owned by the step-loop thread, exactly as before).
  • Typed component IO stays typed over phoxal::api::topics().component(..)?.<capability>(..)?.<leaf>().owner(); FixedSourceLease stays the adapter's local admission policy.
  • Webots-owned world stepping is unchanged: outputs before the matching clock, rewind replaces the timeline, every stop parks the world, deterministic close (presence dropped, then session closed). argv (--bundle-root, --connect, --version) unchanged; still a Webots-launched, non-participant process.
  • The private sample_schedule.rs copy (which existed only because this crate avoided phoxal) is replaced by phoxal::SampleSchedule.
  • The step-loop test runs against a real router-less session (SimulatorSession::in_process); the enqueue-order proof lives framework-side (world_session_tests), which is where the profile keeps client-side observation.

Evidence (against the local 0.66 worktree overlay)

cargo fmt --check, cargo check/clippy --all-targets -D warnings, cargo test (35): green.

Live Webots run launched by the 0.65-line CLI (phoxal 0.40.0) with PHOXAL_FRAMEWORK_PATH at the 0.66 framework worktree and PHOXAL_SIMULATOR_WEBOTS_PATH at this branch, on a throwaway robot-rover: Webots launched the controller with unchanged argv; webots controller ready … capabilities={accelerometer 2, camera 3, depth 1, encoder 4, gnss 1, gyroscope 2, imu 2, motor 4, range 1} presented=8; phoxal status lifecycle: ready, 20/20 processes present with the 8 driver instances present by delegated presence; world clock advancing on one timeline (step=1 … step=2453, 12 ms basicTimeStep, no spurious rewind); motor lease refused SourceAbsent on the park path; phoxal stop and a real Ctrl+C in the TUI both exit 0 with zero leftover processes.

Landing

Draft until phoxal 0.66.0 is published: the manifest pins the unpublished train and Cargo.lock must be regenerated against it before landing (the CLI installs this package --locked).

The controller depended on four framework library packages and drove the
transport itself: it opened a `BusOwner` from a `BusConfig::for_external`,
probed the routers for its execution, minted a `TimelineAuthority` and a
`WorldClockPublisher`, and declared each component driver's presence with
`declare_participant_ready_as`.
None of that is a simulator's job, and after the 0.66 topology none of it is
reachable either: raw bus ownership is `pub(crate)` in the one `phoxal` crate.

So the dependency is now one library at its external-simulator profile:

    phoxal = { version = "0.66", default-features = false, features = ["simulator"] }

and the plumbing is `phoxal::simulator::SimulatorSession`.
`connect` learns the execution and opens the session; `sample_publisher`,
`state_publisher` and `setpoint_receiver` build the typed component handles on
the endpoint's owner side; `participant_ready_events` observes the Ready
evidence the motor's fixed-source lease stands on; `present` declares one
delegated lease per component instance that declares a `driver` block; and
`close` drops that presence before it lets go of the transport.

`WorldTime` is taken once and moves onto the step-loop thread, exactly as the
authority and the clock publisher did: the world advances, `completed_step`
mints the one token for that advance, every capability publishes with it, and
`publish_clock` closes the step.
A rewind replaces the timeline, every stop still parks the world, and argv is
unchanged.

Payloads are endpoints now, so `SimulatedSensor` names one associated type
instead of a body and a descriptor, and the vendored copy of
`phoxal::SampleSchedule` goes: it was carried across only because depending on
`phoxal` used to mean depending on the participant runner, which
`default-features = false` settles.

The step-loop test is not carried over. It needs a `WorldTime`, which only a
connected `SimulatorSession` hands out, and the `simulator` profile publishes no
session that opens without a live router; the framework proves the world-step
ordering in-crate. The `StepWorld` seam stays for the test to return to.
`SimulatorSession::in_process` is published now, so the loop's own proof comes
back: a scripted world publishes one encoder sample per step on a handle taken
from a real session, and the loop drives it with the `WorldTime` that session
hands out - the same two values `controller::run` wires together.

Four ways the loop can stop, one test, because a process may mint exactly one
world time and each scenario closes its session before the next one opens: a
quit world, a device that refuses a step's outputs, a host signal, and a failing
advance. Every one of them leaves the world parked, the two failures reach the
caller, and a step's outputs carry the exact instant of the advance that
produced them.

The enqueue order - a step's outputs on the outbound lane before the clock that
closes it - is not asserted here, and the test says so. Reading it needs both
sequence numbers from the client side, and the `simulator` profile hands an
adapter owner-side handles only; the framework proves it in
`world_session_tests.rs` with a real subscriber on each key. Swapping the two
publishes in `step_once` leaves every assertion here passing, which is exactly
why claiming it would have been worse than not claiming it. The three that do
hold were each checked by mutation.
…ose evidence

The step thread's join result no longer short-circuits the close: a panicked
loop still drops the delegated presence and the transport, and the session's
close evidence is reported beside the loop's outcome.
@jBernavaPrah

Copy link
Copy Markdown
Contributor Author

Review correction applied (2a94302): the session is closed on every exit path of the step thread, a panic included, and its close evidence is reported beside the loop's outcome (adopts the framework's SimulatorSession::close -> Result<(), SimulatorCloseError>). Verified with the overlay: clippy -D warnings, 35/35 tests.

@jBernavaPrah
jBernavaPrah marked this pull request as ready for review August 19, 2026 16:55
@jBernavaPrah
jBernavaPrah merged commit 13fa49b into main Aug 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant