phoxal_api/lib.rs
1//! The single API layer (D60/D61).
2//!
3//! This crate is the dated API contract tree. It depends only on the
4//! [`phoxal-bus`](phoxal_bus) ABI floor (the contract primitive traits and the
5//! typed-topic builders) and the [`phoxal-macros`](phoxal_macros) proc-macros; it
6//! does **not** depend on the `phoxal` engine. A runtime depends on both crates
7//! and imports the API tree directly with `use phoxal_api::y2026_1 as api;`, so
8//! `phoxal_api::y2026_1`, `phoxal_api::ApiVersion`, and `phoxal_api::ContractBody`
9//! are the canonical paths.
10//!
11//! # Dated API versions
12//!
13//! An API version is a **dated module** (`phoxal_api::y2026_1`, …) generated by
14//! [`phoxal_api_tree!`]. Each version module carries:
15//!
16//! - a zero-variant marker `enum Api {}` implementing [`ApiVersion`], whose
17//! [`ApiVersion::ID`] is the dated module name (`"y2026_1"`) - the canonical
18//! version identity;
19//! - the version-local wire bodies, one `pub mod` per contract node holding plain
20//! serde structs/enums and their [`ContractBody`] impls;
21//! - an api-local `topic` builder rooted at `topic::new()`.
22//!
23//! Versions can build on earlier ones with `version y2026_2 extends y2026_1`:
24//! every inherited node and type is re-emitted *fresh* under the new module - same
25//! [`ContractBody::FAMILY`] and [`ContractBody::TOPIC`], a different [`Api`] - so an
26//! unchanged contract stays wire-identical while the compile-time `Api` bound stays
27//! distinct. See [`phoxal_api_tree!`] for the inheritance and shape-identity rules.
28//!
29//! [`Api`]: y2026_1::Api
30//!
31//! # One API version per runtime, one per graph
32//!
33//! A runtime authors against exactly one version module
34//! (`use phoxal_api::y2026_1 as api;`) and declares it on the derive
35//! (`#[phoxal(api = y2026_1)]`). Every handle body is bound
36//! `ContractBody<Api = R::Api>`, so passing a body from another API version is a
37//! compile error (D59/D60). At deploy time a single graph runs a single API
38//! version: all of its runtimes share one `Api`, so contracts on the bus are
39//! mutually consistent by construction (D59).
40//!
41//! # Plain serde wire bodies, version in metadata
42//!
43//! A wire body is just its serde encoding - there is no `{"v":…}` envelope or any
44//! other version tag inside the payload (D62). The three pieces of routing
45//! identity travel as bus metadata alongside the encoded body, not in it:
46//!
47//! - the **version** ([`ApiVersion::ID`], e.g. `"y2026_1"`),
48//! - the **family** ([`ContractBody::FAMILY`], e.g. `"drive::State"`),
49//! - the **codec** that produced the bytes.
50//!
51//! Keeping these out of the payload means the body bytes for an unchanged contract
52//! are identical across versions and codecs; the envelope/metadata layer owns
53//! version and family.
54//!
55//! # Family and topic
56//!
57//! Both identifiers are derived from the contract node's path in the tree, never
58//! written by hand:
59//!
60//! - [`ContractBody::FAMILY`] is the `::`-joined node path plus the body type name,
61//! e.g. `component::motor::Command`. It excludes dynamic variables, so every
62//! instance of a per-instance node shares one family.
63//! - [`ContractBody::TOPIC`] is the versionless `/`-joined key with each dynamic
64//! node contributing a `{var}` placeholder, e.g.
65//! `component/{instance}/motor/{capability}/command`. A fully static path has a
66//! literal key (`drive/state`).
67//!
68//! # The api-local topic builder
69//!
70//! Each version module exposes a `topic` builder that mirrors the node tree:
71//! `api::topic::new()` returns a root, one method per top-level node walks down the
72//! tree, a dynamic node's method takes its variable as `impl Display`, and a leaf
73//! method binds the topic's side-branded kind to its version-local body. For
74//! example `api::topic::new().drive().state()` yields a
75//! `Topic<Subscribe<drive::State>>` (the CLIENT observes the owner's `state`) over
76//! the static key `drive/state`, and
77//! `api::topic::new().component("base").motor("left").command()` fills the dynamic
78//! segments to produce `component/base/motor/left/command`. Because the builder is
79//! generated from the same tree as `FAMILY`/`TOPIC`, the built key and the
80//! documented key stay in lockstep.
81//!
82//! ## Owner side: `topic::internal`
83//!
84//! The PUBLIC `topic::new()...` chain above is the **client** side. The matching
85//! **owner** side lives at `api::topic::internal::new(cap)...` (L1 + L2, plan #00):
86//! the same node tree and keys, but the leaf brands flip so the owner gets the side
87//! it must take - `api::topic::internal::new(cap).drive().state()` is
88//! `Topic<Publish<drive::State>>` (the owner publishes its telemetry), and
89//! `api::topic::internal::new(cap).drive().target()` is `Topic<Subscribe<drive::Target>>`
90//! (the owner reads its command input). A query owner reaches its `ServeQuery`
91//! brand the same way. The `internal` chain is the deliberate, greppable owner
92//! opt-in; a runtime acquires the topics of its OWN node through it and everything
93//! it consumes through the public chain.
94//!
95//! The `internal::new` entry requires the runner-minted owner capability
96//! ([`OwnerCap`](phoxal_bus::OwnerCap), Layer 2): a runtime obtains it from
97//! `phoxal::SetupContext::owner_capability()` and passes it in. On the documented
98//! surface, owning a topic therefore cannot happen by accident - only with a
99//! capability the runner mints.
100
101use phoxal_macros::phoxal_api_tree;
102
103/// The contract primitive traits, re-exported from the `phoxal-bus` crate (the
104/// ABI floor) so they stay addressable at `phoxal_api::ApiVersion` /
105/// `phoxal_api::ContractBody`.
106///
107/// - [`ApiVersion`] is the marker trait identifying one dated API version (D60),
108/// implemented only by the zero-variant `enum Api {}` that [`phoxal_api_tree!`]
109/// generates inside each version module; its `ID` is the dated module name
110/// (`"y2026_1"`) and the canonical version identity, carried in bus metadata.
111/// - [`ContractBody`] is a version-local wire body (D61): a plain serde type
112/// bound to exactly one [`ApiVersion`] and one contract family/topic. Every
113/// body declared inside a [`phoxal_api_tree!`] node gets a generated impl;
114/// handles, `SetupContext` builders, and the `#[derive(Runtime)]` assertions
115/// key off its `Api`/`FAMILY`/`TOPIC` to reject a body from the wrong API
116/// version at compile time. Its serde encoding *is* the wire payload; there is
117/// no version envelope (D62).
118pub use phoxal_bus::{ApiVersion, ContractBody};
119
120phoxal_api_tree! {
121 version y2026_1 {
122 drive {
123 /// Why actuation authority is in its current state.
124 enum StopReason {
125 NoTarget,
126 EmergencyStop,
127 Fault,
128 }
129
130 /// Whether the drive is actively commanding the actuators.
131 enum ActuatorAuthority {
132 Active,
133 Stopped,
134 }
135
136 /// A requested or limited planar velocity.
137 struct Target {
138 linear_x_mps: f32,
139 angular_z_radps: f32,
140 curvature_limit_radpm: Option<f32>,
141 }
142
143 /// The drive runtime's published control state.
144 struct State {
145 target: Target,
146 limited_target: Target,
147 actuator_authority: ActuatorAuthority,
148 stop_reason: Option<StopReason>,
149 }
150
151 topic target: command Target;
152 topic state: state State;
153 }
154
155 battery {
156 /// Battery state - a family that exists only from y2026_1 on.
157 struct State {
158 voltage_v: f32,
159 current_a: f32,
160 charge_ratio: f32,
161 }
162
163 topic state: state State;
164 }
165
166 safety {
167 /// Safety runtime decision for a candidate motion command.
168 #[derive(Copy, Eq)]
169 enum SafetyDecision {
170 Allow,
171 Slow,
172 Stop,
173 EmergencyStop,
174 UnknownConservative,
175 }
176
177 /// An inclusive `[min, max]` bound on a scalar control axis.
178 struct Constraint {
179 min: f64,
180 max: f64,
181 }
182
183 /// Per-axis velocity bounds the safety runtime will allow.
184 struct MotionConstraint {
185 linear_x_mps: Constraint,
186 angular_z_radps: Constraint,
187 }
188
189 #[derive(Copy, Eq)]
190 #[serde(rename_all = "snake_case")]
191 enum SafetyReasonCode {
192 ObstacleDetected,
193 BatteryLow,
194 BatteryCritical,
195 DriveFault,
196 LocalizationLost,
197 SourceStale,
198 EmergencyStopEngaged,
199 Unknown,
200 }
201
202 /// A coded reason behind a safety decision, with optional detail text.
203 struct SafetyReason {
204 code: SafetyReasonCode,
205 detail: Option<String>,
206 }
207
208 /// Revisions of the inputs a safety decision was computed against.
209 struct SafetySourceRevision {
210 localization: Option<u64>,
211 map: Option<u64>,
212 }
213
214 /// The safety runtime's authorization for downstream motion: the
215 /// decision, the motion it approves, why, and when it expires.
216 struct SafetyAuthorization {
217 decision: SafetyDecision,
218 approved_motion: MotionConstraint,
219 reasons: Vec<SafetyReason>,
220 source_revision: SafetySourceRevision,
221 expires_at_ns: Option<u64>,
222 }
223
224 /// The safety runtime's published status: current decision + reasons.
225 struct Status {
226 decision: SafetyDecision,
227 active_reasons: Vec<SafetyReason>,
228 }
229
230 /// A request to engage or release the emergency stop.
231 #[derive(Eq)]
232 struct EmergencyStopRequest {
233 engaged: bool,
234 }
235
236 topic authorization: state SafetyAuthorization;
237 topic state: state Status;
238 topic estop: command EmergencyStopRequest;
239 }
240
241 mission {
242 /// A target pose in the map frame (yaw optional).
243 struct Goal {
244 x_m: f64,
245 y_m: f64,
246 yaw_rad: Option<f64>,
247 }
248
249 /// A command driving the mission lifecycle.
250 enum Command {
251 Start(Goal),
252 Pause,
253 Resume,
254 Cancel,
255 }
256
257 /// Where the mission is in its lifecycle.
258 #[derive(Copy, Eq)]
259 enum Phase {
260 Idle,
261 Active,
262 Paused,
263 Succeeded,
264 Failed,
265 }
266
267 /// The mission runtime's published state.
268 struct State {
269 phase: Phase,
270 goal: Option<Goal>,
271 detail: Option<String>,
272 }
273
274 topic command: command Command;
275 topic goal: state Goal;
276 topic state: state State;
277 }
278
279 joint(joint) {
280 /// Per-joint position/velocity (and optional effort) on a dynamic
281 /// per-joint key.
282 struct JointState {
283 position_rad: f64,
284 velocity_radps: f64,
285 effort_nm: Option<f64>,
286 }
287
288 topic state: state JointState;
289 }
290
291 frame {
292 /// A parent → child rigid transform (translation + xyzw quaternion).
293 struct FrameTransform {
294 parent_frame_id: String,
295 child_frame_id: String,
296 translation_m: [f64; 3],
297 rotation_quat_xyzw: [f64; 4],
298 stamp_ns: Option<u64>,
299 }
300
301 /// Transforms that do not change over time.
302 struct StaticTransforms {
303 transforms: Vec<FrameTransform>,
304 }
305
306 /// The current transform tree.
307 struct Tree {
308 transforms: Vec<FrameTransform>,
309 }
310
311 /// Ask for the transform between two frames, optionally at a time.
312 struct LookupRequest {
313 target_frame_id: String,
314 source_frame_id: String,
315 at_ns: Option<u64>,
316 }
317
318 /// The resolved transform, or `None` if it is not available.
319 struct LookupResponse {
320 transform: Option<FrameTransform>,
321 }
322
323 topic tree: state Tree;
324 topic static_transforms: state StaticTransforms;
325 topic lookup: query LookupRequest => LookupResponse;
326 }
327
328 power {
329 /// A platform power command.
330 #[derive(Copy, Eq)]
331 enum Command {
332 Reboot,
333 Shutdown,
334 }
335
336 /// Where the power runtime is in handling a command.
337 #[derive(Copy, Eq)]
338 enum Status {
339 Idle,
340 Rebooting,
341 ShuttingDown,
342 Failed,
343 }
344
345 /// Why a power command was rejected outright.
346 #[derive(Copy, Eq)]
347 #[serde(rename_all = "snake_case")]
348 enum RejectedReason {
349 SupervisorUnavailable,
350 SupervisorReturnedHttp,
351 }
352
353 /// Why an accepted power command later failed.
354 #[derive(Copy, Eq)]
355 #[serde(rename_all = "snake_case")]
356 enum FailedReason {
357 SupervisorTransport,
358 }
359
360 /// The power runtime's published state.
361 struct State {
362 status: Status,
363 detail: Option<String>,
364 }
365
366 topic command: command Command;
367 topic state: state State;
368 }
369
370 motion {
371 /// A planar velocity the motion arbiter may select.
372 struct Target {
373 linear_x_mps: f32,
374 angular_z_radps: f32,
375 curvature_limit_radpm: Option<f32>,
376 }
377
378 /// The motion arbiter's local view of the active safety decision.
379 #[derive(Copy, Eq)]
380 #[serde(rename_all = "snake_case")]
381 enum SafetyDecision {
382 Allow,
383 Slow,
384 Stop,
385 EmergencyStop,
386 UnknownConservative,
387 }
388
389 /// Which input the motion arbiter is currently following.
390 #[derive(Copy, Eq)]
391 #[serde(rename_all = "snake_case")]
392 enum MotionSource {
393 Manual,
394 Follow,
395 MissionStop,
396 Recovery,
397 EmergencyStop,
398 }
399
400 /// Why the motion arbiter chose its current source/target.
401 #[derive(Copy, Eq)]
402 #[serde(rename_all = "snake_case")]
403 enum MotionReason {
404 SafetyEmergencyStop,
405 ManualEscapeUnderStop,
406 SafetyConstrained(SafetyDecision),
407 NoFollowTarget,
408 FollowTargetStale,
409 SafetyAuthorizationUnavailable,
410 }
411
412 /// A direct teleop velocity command.
413 struct ManualCommand {
414 linear_x_mps: f64,
415 angular_z_radps: f64,
416 }
417
418 /// The motion arbiter's published state.
419 struct State {
420 active_source: Option<MotionSource>,
421 selected: Option<Target>,
422 reason: Option<MotionReason>,
423 }
424
425 topic manual: command ManualCommand;
426 topic state: state State;
427 }
428
429 plan {
430 /// One pose along a planned path.
431 struct PathPose {
432 x_m: f64,
433 y_m: f64,
434 yaw_rad: Option<f64>,
435 }
436
437 /// A planned path, tagged with the map revision it was built on.
438 struct Path {
439 poses: Vec<PathPose>,
440 map_revision: Option<u64>,
441 }
442
443 /// Why the planner declined to produce a path.
444 #[derive(Copy, Eq)]
445 #[serde(rename_all = "snake_case")]
446 enum Refusal {
447 MissionInactive,
448 NoGoal,
449 NoMap,
450 NoLocalization,
451 Unreachable,
452 NonPlanarGoalUnsupported,
453 LocalizationInitializing,
454 LocalizationLost,
455 LocalizationRelocalizing,
456 UnsupportedLocalizationMode,
457 NoLocalizationPose,
458 NoLocalizationRevision,
459 GoalMapRevisionMismatch,
460 MapLocalizeRevisionMismatch,
461 }
462
463 /// The planner's published state.
464 struct State {
465 has_path: bool,
466 refusal: Option<Refusal>,
467 }
468
469 topic path: state Path;
470 topic state: state State;
471 }
472
473 follow {
474 /// The velocity the path follower wants next, with provenance.
475 struct Target {
476 map_revision: Option<u64>,
477 built_from_localize_revision: Option<u64>,
478 frame_id: String,
479 linear_x_mps: f64,
480 angular_z_radps: f64,
481 }
482
483 /// The path follower's published state.
484 struct State {
485 active: bool,
486 target_index: Option<u32>,
487 finished: bool,
488 }
489
490 topic target: state Target;
491 topic state: state State;
492 }
493
494 explore {
495 /// A candidate frontier to explore, with a size and a ranking score.
496 struct Frontier {
497 x_m: f64,
498 y_m: f64,
499 size: u32,
500 score: f32,
501 }
502
503 /// The current frontier set, tagged with its map revision.
504 struct Frontiers {
505 frontiers: Vec<Frontier>,
506 map_revision: Option<u64>,
507 }
508
509 /// The exploration runtime's published state.
510 struct State {
511 exploring: bool,
512 selected: Option<Frontier>,
513 }
514
515 topic frontiers: state Frontiers;
516 topic state: state State;
517 }
518
519 perception {
520 /// A single detected object: class, confidence, and pose in a frame.
521 struct Detection {
522 class_id: String,
523 confidence: f32,
524 position_m: [f64; 3],
525 frame_id: String,
526 track_id: Option<u64>,
527 }
528
529 /// A batch of detections from one perception cycle.
530 struct Detections {
531 detections: Vec<Detection>,
532 stamp_ns: Option<u64>,
533 }
534
535 /// The perception runtime's published health.
536 struct State {
537 healthy: bool,
538 detector: String,
539 }
540
541 topic detections: state Detections;
542 topic state: state State;
543 }
544
545 video {
546 /// Ask to open a video stream for a capability at an optional size.
547 struct OpenRequest {
548 capability: String,
549 width_px: Option<u32>,
550 height_px: Option<u32>,
551 }
552
553 /// The id of the stream that was opened.
554 struct OpenResponse {
555 stream_id: String,
556 }
557
558 topic open: query OpenRequest => OpenResponse;
559
560 stream(stream) {
561 /// Where one open video stream is in its lifecycle.
562 #[derive(Copy, Eq)]
563 #[serde(rename_all = "snake_case")]
564 enum StreamPhase {
565 Starting,
566 Active,
567 Stopped,
568 }
569
570 /// The published state of one video stream: its lifecycle phase
571 /// and the number of source frames seen so far. The video runtime
572 /// publishes it per stream; clients subscribe, hence `state`.
573 struct StreamState {
574 phase: StreamPhase,
575 frames_seen: u64,
576 }
577
578 topic state: state StreamState;
579 }
580 }
581
582 simulation {
583 /// The simulator clock: current time and whether it is advancing.
584 struct Clock {
585 now_ns: u64,
586 running: bool,
587 }
588
589 /// A command to the simulator's run loop.
590 #[derive(Copy, Eq)]
591 enum Control {
592 Pause,
593 Resume,
594 Reset,
595 }
596
597 /// The simulated robot's ground-truth planar pose.
598 struct RobotPose {
599 x_m: f64,
600 y_m: f64,
601 yaw_rad: f64,
602 }
603
604 /// Whether the simulated robot is in contact, with optional detail.
605 struct Contact {
606 in_contact: bool,
607 detail: Option<String>,
608 }
609
610 topic clock: state Clock;
611 topic control: command Control;
612 topic robot_pose: state RobotPose;
613 topic contact: state Contact;
614 }
615
616 // Per-instance component capabilities (D17/D38: framework-runtime / driver
617 // territory). `component(instance)` selects a manifest-declared component;
618 // each child `kind(capability)` is a self-contained node whose key is
619 // `component/{instance}/<kind>/{capability}/<leaf>`. Nodes duplicate any
620 // types they share by design - the node path disambiguates, so the names
621 // are path-local.
622 component(instance) {
623 motor(capability) {
624 /// A per-actuator command.
625 enum Command {
626 Velocity(f32),
627 Torque(f32),
628 Stop,
629 }
630
631 topic command: command Command;
632 }
633
634 encoder(capability) {
635 /// Per-encoder sample on a dynamic per-instance key.
636 struct Sample {
637 position_rad: f64,
638 velocity_radps: f32,
639 }
640
641 topic sample: state Sample;
642 }
643
644 accelerometer(capability) {
645 /// Raw accelerometer sample in the sensor-local frame in m/s^2.
646 struct Sample {
647 linear_acceleration: [f32; 3],
648 }
649
650 topic sample: state Sample;
651 }
652
653 gyroscope(capability) {
654 /// Raw angular velocity sample in the sensor-local frame in rad/s.
655 struct Sample {
656 angular_velocity: [f32; 3],
657 }
658
659 topic sample: state Sample;
660 }
661
662 magnetometer(capability) {
663 /// Raw magnetic-field sample in the sensor-local frame.
664 struct Sample {
665 magnetic_field: [f32; 3],
666 }
667
668 topic sample: state Sample;
669 }
670
671 imu(capability) {
672 #[derive(Copy, Eq)]
673 #[serde(rename_all = "snake_case")]
674 enum SensorHealth {
675 Nominal,
676 Degraded,
677 Fault,
678 }
679
680 #[derive(Copy)]
681 struct Bias {
682 angular_velocity_radps: [f32; 3],
683 linear_acceleration_mps2: [f32; 3],
684 }
685
686 struct Sample {
687 orientation: Option<[f32; 4]>,
688 angular_velocity_radps: [f32; 3],
689 linear_acceleration_mps2: [f32; 3],
690 covariance: Option<[f32; 9]>,
691 noise_density: Option<[f32; 3]>,
692 sensor_frame_id: Option<String>,
693 measured_at_ns: Option<u64>,
694 health: SensorHealth,
695 bias: Option<Bias>,
696 }
697
698 topic sample: state Sample;
699 }
700
701 range(capability) {
702 #[derive(Copy, Eq)]
703 #[serde(rename_all = "snake_case")]
704 enum SensorHealth {
705 Nominal,
706 Degraded,
707 Fault,
708 }
709
710 #[derive(Copy)]
711 struct Limits {
712 min_m: f32,
713 max_m: f32,
714 }
715
716 #[derive(Copy)]
717 struct SampleQuality {
718 valid: bool,
719 confidence: Option<f32>,
720 }
721
722 struct Sample {
723 distance_m: f32,
724 limits: Option<Limits>,
725 measured_at_ns: Option<u64>,
726 quality: Option<SampleQuality>,
727 health: SensorHealth,
728 }
729
730 topic sample: state Sample;
731 }
732
733 gnss(capability) {
734 /// A GNSS fix: geodetic position plus a 3x3 position covariance.
735 struct Sample {
736 latitude: f64,
737 longitude: f64,
738 altitude: f64,
739 position_covariance: [f64; 9],
740 }
741
742 topic sample: state Sample;
743 }
744
745 camera(capability) {
746 #[derive(Copy, Eq)]
747 #[serde(rename_all = "snake_case")]
748 enum Encoding {
749 Jpeg,
750 Png,
751 L8,
752 Rgb8,
753 Rgba8,
754 }
755
756 #[derive(Copy)]
757 struct Intrinsics {
758 fx: f32,
759 fy: f32,
760 cx: f32,
761 cy: f32,
762 }
763
764 struct Distortion {
765 model: String,
766 coefficients: Vec<f32>,
767 }
768
769 #[derive(Copy)]
770 struct ExposureTiming {
771 exposure_start_ns: Option<u64>,
772 exposure_duration_ns: Option<u64>,
773 }
774
775 struct CalibrationIdentity {
776 id: String,
777 version: String,
778 }
779
780 /// One camera frame: encoded pixel bytes plus optional calibration
781 /// and timing metadata.
782 struct Frame {
783 width: u32,
784 height: u32,
785 encoding: Encoding,
786 intrinsics: Option<Intrinsics>,
787 distortion: Option<Distortion>,
788 exposure: Option<ExposureTiming>,
789 measured_at_ns: Option<u64>,
790 calibration: Option<CalibrationIdentity>,
791 #[serde(with = "serde_bytes")]
792 data: Vec<u8>,
793 }
794
795 topic frame: state Frame;
796 }
797
798 depth(capability) {
799 #[derive(Copy, Eq)]
800 #[serde(rename_all = "snake_case")]
801 enum Encoding {
802 U16Millimeters,
803 }
804
805 #[derive(Copy, Eq)]
806 #[serde(rename_all = "snake_case")]
807 enum InvalidSamplePolicy {
808 ZeroIsInvalid,
809 NonFiniteIsInvalid,
810 }
811
812 #[derive(Copy)]
813 struct Intrinsics {
814 fx: f32,
815 fy: f32,
816 cx: f32,
817 cy: f32,
818 }
819
820 struct Distortion {
821 model: String,
822 coefficients: Vec<f32>,
823 }
824
825 #[derive(Copy)]
826 struct ExposureTiming {
827 exposure_start_ns: Option<u64>,
828 exposure_duration_ns: Option<u64>,
829 }
830
831 struct CalibrationIdentity {
832 id: String,
833 version: String,
834 }
835
836 /// One depth frame: per-pixel millimetre samples plus optional
837 /// calibration and timing metadata.
838 struct Frame {
839 samples_mm: Vec<u16>,
840 encoding: Encoding,
841 invalid_sample_policy: InvalidSamplePolicy,
842 width: Option<u32>,
843 height: Option<u32>,
844 intrinsics: Option<Intrinsics>,
845 distortion: Option<Distortion>,
846 exposure: Option<ExposureTiming>,
847 measured_at_ns: Option<u64>,
848 calibration: Option<CalibrationIdentity>,
849 }
850
851 topic frame: state Frame;
852 }
853
854 lidar(capability) {
855 #[derive(Copy, Eq)]
856 #[serde(rename_all = "snake_case")]
857 enum SensorHealth {
858 Nominal,
859 Degraded,
860 Fault,
861 }
862
863 #[derive(Copy)]
864 struct ScanGeometry {
865 angle_min_rad: f32,
866 angle_increment_rad: f32,
867 }
868
869 #[derive(Copy)]
870 struct RangeLimits {
871 min_m: f32,
872 max_m: f32,
873 }
874
875 #[derive(Copy)]
876 struct ScanQuality {
877 valid_points: u32,
878 }
879
880 struct Ranges {
881 ranges: Vec<f32>,
882 geometry: Option<ScanGeometry>,
883 limits: Option<RangeLimits>,
884 measured_at_ns: Option<u64>,
885 quality: Option<ScanQuality>,
886 health: SensorHealth,
887 }
888
889 struct Points {
890 points: Vec<[f32; 3]>,
891 limits: Option<RangeLimits>,
892 measured_at_ns: Option<u64>,
893 quality: Option<ScanQuality>,
894 health: SensorHealth,
895 }
896
897 /// One lidar scan, either as polar ranges or as cartesian points.
898 #[serde(tag = "kind", rename_all = "snake_case")]
899 enum Scan {
900 Ranges(Ranges),
901 Points(Points),
902 }
903
904 topic scan: state Scan;
905 }
906
907 mmwave(capability) {
908 /// One mmWave radar detection: position, velocity, and SNR.
909 #[derive(Copy)]
910 struct Detection {
911 position: [f32; 3],
912 velocity: [f32; 3],
913 snr: f32,
914 }
915
916 /// One mmWave radar scan as a set of detections.
917 struct Scan {
918 detections: Vec<Detection>,
919 }
920
921 topic scan: state Scan;
922 }
923
924 microphone(capability) {
925 /// One audio frame as raw encoded bytes.
926 struct Frame {
927 data: Vec<u8>,
928 }
929
930 topic frame: state Frame;
931 }
932
933 led(capability) {
934 /// A per-LED on/off command.
935 #[derive(Copy, Eq)]
936 enum Command {
937 On,
938 Off,
939 }
940
941 topic command: command Command;
942 }
943
944 emergency_stop(capability) {
945 /// Per-instance emergency-stop state.
946 #[derive(Eq)]
947 struct State {
948 engaged: bool,
949 }
950
951 topic state: state State;
952 }
953 }
954
955 odometry {
956 /// A planar pose + twist estimate in the odometry frame.
957 struct State {
958 x_m: f64,
959 y_m: f64,
960 yaw_rad: f64,
961 linear_x_mps: f32,
962 angular_z_radps: f32,
963 }
964
965 topic state: state State;
966 }
967
968 localize {
969 /// A planar localization estimate in the map frame.
970 struct LocalizationState {
971 x_m: f64,
972 y_m: f64,
973 yaw_rad: f64,
974 confidence: f32,
975 }
976
977 topic state: state LocalizationState;
978 }
979
980 presence {
981 /// Per-participant liveness + readiness beacon.
982 enum Readiness {
983 NotStarted,
984 Initializing,
985 Ready,
986 Degraded,
987 Failed,
988 }
989
990 /// A single participant's liveness beacon. Participants publish their
991 /// own heartbeat; the presence runtime subscribes them as its control
992 /// input, hence the `command` role.
993 struct Heartbeat {
994 participant: String,
995 readiness: Readiness,
996 }
997
998 /// The presence runtime's aggregate readiness across all participants.
999 /// Presence publishes it; clients subscribe, hence the `state` role.
1000 struct State {
1001 readiness: Readiness,
1002 }
1003
1004 topic heartbeat: command Heartbeat;
1005 topic state: state State;
1006 }
1007
1008 map {
1009 /// A published map revision marker.
1010 struct Revision {
1011 revision: u64,
1012 resolution_m: f32,
1013 }
1014
1015 /// Request a rectangular submap window (map-frame metres).
1016 struct SubmapRequest {
1017 min_x_m: f64,
1018 min_y_m: f64,
1019 max_x_m: f64,
1020 max_y_m: f64,
1021 }
1022
1023 /// An occupancy-grid window: row-major cells, 0..=100 + 255 = unknown.
1024 struct SubmapResponse {
1025 width: u32,
1026 height: u32,
1027 resolution_m: f32,
1028 cells: Vec<u8>,
1029 }
1030
1031 topic revision: state Revision;
1032 topic submap: query SubmapRequest => SubmapResponse;
1033 }
1034
1035 asset {
1036 /// Fetch a stored asset by path.
1037 struct GetRequest {
1038 path: String,
1039 }
1040
1041 /// The asset bytes, a not-found marker, or a rejected path.
1042 enum GetResponse {
1043 Found { bytes: Vec<u8> },
1044 Missing,
1045 InvalidPath,
1046 }
1047
1048 topic get: query GetRequest => GetResponse;
1049 }
1050 }
1051}
1052
1053#[cfg(test)]
1054mod tests;