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