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 plan {
438 /// One pose along a planned path.
439 struct PathPose {
440 x_m: f64,
441 y_m: f64,
442 yaw_rad: Option<f64>,
443 }
444
445 /// A planned path, tagged with the map revision it was built on.
446 struct Path {
447 poses: Vec<PathPose>,
448 map_revision: Option<u64>,
449 }
450
451 /// Why the planner declined to produce a path.
452 #[derive(Copy, Eq)]
453 #[serde(rename_all = "snake_case")]
454 enum Refusal {
455 MissionInactive,
456 NoGoal,
457 NoMap,
458 NoLocalization,
459 Unreachable,
460 NonPlanarGoalUnsupported,
461 LocalizationInitializing,
462 LocalizationLost,
463 LocalizationRelocalizing,
464 UnsupportedLocalizationMode,
465 NoLocalizationPose,
466 NoLocalizationRevision,
467 GoalMapRevisionMismatch,
468 MapLocalizeRevisionMismatch,
469 }
470
471 /// The planner's published state.
472 struct State {
473 has_path: bool,
474 refusal: Option<Refusal>,
475 }
476
477 topic path: state Path;
478 topic state: state State;
479 }
480
481 follow {
482 /// The velocity the path follower wants next, with provenance.
483 struct Target {
484 map_revision: Option<u64>,
485 built_from_localize_revision: Option<u64>,
486 frame_id: String,
487 linear_x_mps: f64,
488 angular_z_radps: f64,
489 }
490
491 /// The path follower's published state.
492 struct State {
493 active: bool,
494 target_index: Option<u32>,
495 finished: bool,
496 }
497
498 topic target: state Target;
499 topic state: state State;
500 }
501
502 explore {
503 /// A candidate frontier to explore, with a size and a ranking score.
504 struct Frontier {
505 x_m: f64,
506 y_m: f64,
507 size: u32,
508 score: f32,
509 }
510
511 /// The current frontier set, tagged with its map revision.
512 struct Frontiers {
513 frontiers: Vec<Frontier>,
514 map_revision: Option<u64>,
515 }
516
517 /// The exploration participant's published state.
518 struct State {
519 exploring: bool,
520 selected: Option<Frontier>,
521 }
522
523 topic frontiers: state Frontiers;
524 topic state: state State;
525 }
526
527 perception {
528 /// A single detected object: class, confidence, and pose in a frame.
529 struct Detection {
530 class_id: String,
531 confidence: f32,
532 position_m: [f64; 3],
533 frame_id: String,
534 track_id: Option<u64>,
535 }
536
537 /// A batch of detections from one perception cycle.
538 struct Detections {
539 detections: Vec<Detection>,
540 stamp_ns: Option<u64>,
541 }
542
543 /// The perception participant's published health.
544 struct State {
545 healthy: bool,
546 detector: String,
547 }
548
549 topic detections: state Detections;
550 topic state: state State;
551 }
552
553 video {
554 /// Ask to open a video stream for a capability at an optional size.
555 struct OpenRequest {
556 capability: String,
557 width_px: Option<u32>,
558 height_px: Option<u32>,
559 }
560
561 /// The id of the stream that was opened.
562 struct OpenResponse {
563 stream_id: String,
564 }
565
566 topic open: query OpenRequest => OpenResponse;
567
568 stream(stream) {
569 /// Where one open video stream is in its lifecycle.
570 #[derive(Copy, Eq)]
571 #[serde(rename_all = "snake_case")]
572 enum StreamPhase {
573 Starting,
574 Active,
575 Stopped,
576 }
577
578 /// The published state of one video stream: its lifecycle phase
579 /// and the number of source frames seen so far. The video participant
580 /// publishes it per stream; clients subscribe, hence `state`.
581 struct StreamState {
582 phase: StreamPhase,
583 frames_seen: u64,
584 }
585
586 topic state: state StreamState;
587 }
588 }
589
590 simulation {
591 /// The simulator clock: current time and whether it is advancing.
592 struct Clock {
593 now_ns: u64,
594 running: bool,
595 }
596
597 /// A command to the simulator's run loop.
598 #[derive(Copy, Eq)]
599 enum Control {
600 Pause,
601 Resume,
602 Reset,
603 }
604
605 /// The simulated robot's ground-truth planar pose.
606 struct RobotPose {
607 x_m: f64,
608 y_m: f64,
609 yaw_rad: f64,
610 }
611
612 /// Whether the simulated robot is in contact, with optional detail.
613 struct Contact {
614 in_contact: bool,
615 detail: Option<String>,
616 }
617
618 topic clock: state Clock;
619 topic control: command Control;
620 topic robot_pose: state RobotPose;
621 topic contact: state Contact;
622 }
623
624 // Per-instance component capabilities (D17/D38: framework participant / driver
625 // territory). `component(instance)` selects a manifest-declared component;
626 // each child `kind(capability)` is a self-contained node whose key is
627 // `component/{instance}/<kind>/{capability}/<leaf>`. Nodes duplicate any
628 // types they share by design - the node path disambiguates, so the names
629 // are path-local.
630 component(instance) {
631 motor(capability) {
632 /// A per-actuator command.
633 enum Command {
634 Velocity(f32),
635 Torque(f32),
636 Stop,
637 }
638
639 topic command: command Command;
640 }
641
642 encoder(capability) {
643 /// Per-encoder sample on a dynamic per-instance key.
644 struct Sample {
645 position_rad: f64,
646 velocity_radps: f32,
647 }
648
649 topic sample: state Sample;
650 }
651
652 accelerometer(capability) {
653 /// Raw accelerometer sample in the sensor-local frame in m/s^2.
654 struct Sample {
655 linear_acceleration: [f32; 3],
656 }
657
658 topic sample: state Sample;
659 }
660
661 gyroscope(capability) {
662 /// Raw angular velocity sample in the sensor-local frame in rad/s.
663 struct Sample {
664 angular_velocity: [f32; 3],
665 }
666
667 topic sample: state Sample;
668 }
669
670 magnetometer(capability) {
671 /// Raw magnetic-field sample in the sensor-local frame.
672 struct Sample {
673 magnetic_field: [f32; 3],
674 }
675
676 topic sample: state Sample;
677 }
678
679 imu(capability) {
680 #[derive(Copy, Eq)]
681 #[serde(rename_all = "snake_case")]
682 enum SensorHealth {
683 Nominal,
684 Degraded,
685 Fault,
686 }
687
688 #[derive(Copy)]
689 struct Bias {
690 angular_velocity_radps: [f32; 3],
691 linear_acceleration_mps2: [f32; 3],
692 }
693
694 struct Sample {
695 orientation: Option<[f32; 4]>,
696 angular_velocity_radps: [f32; 3],
697 linear_acceleration_mps2: [f32; 3],
698 covariance: Option<[f32; 9]>,
699 noise_density: Option<[f32; 3]>,
700 sensor_frame_id: Option<String>,
701 measured_at_ns: Option<u64>,
702 health: SensorHealth,
703 bias: Option<Bias>,
704 }
705
706 topic sample: state Sample;
707 }
708
709 range(capability) {
710 #[derive(Copy, Eq)]
711 #[serde(rename_all = "snake_case")]
712 enum SensorHealth {
713 Nominal,
714 Degraded,
715 Fault,
716 }
717
718 #[derive(Copy)]
719 struct Limits {
720 min_m: f32,
721 max_m: f32,
722 }
723
724 #[derive(Copy)]
725 struct SampleQuality {
726 valid: bool,
727 confidence: Option<f32>,
728 }
729
730 struct Sample {
731 distance_m: f32,
732 limits: Option<Limits>,
733 measured_at_ns: Option<u64>,
734 quality: Option<SampleQuality>,
735 health: SensorHealth,
736 }
737
738 topic sample: state Sample;
739 }
740
741 gnss(capability) {
742 /// A GNSS fix: geodetic position plus a 3x3 position covariance.
743 struct Sample {
744 latitude: f64,
745 longitude: f64,
746 altitude: f64,
747 position_covariance: [f64; 9],
748 }
749
750 topic sample: state Sample;
751 }
752
753 camera(capability) {
754 #[derive(Copy, Eq)]
755 #[serde(rename_all = "snake_case")]
756 enum Encoding {
757 Jpeg,
758 Png,
759 L8,
760 Rgb8,
761 Rgba8,
762 }
763
764 #[derive(Copy)]
765 struct Intrinsics {
766 fx: f32,
767 fy: f32,
768 cx: f32,
769 cy: f32,
770 }
771
772 struct Distortion {
773 model: String,
774 coefficients: Vec<f32>,
775 }
776
777 #[derive(Copy)]
778 struct ExposureTiming {
779 exposure_start_ns: Option<u64>,
780 exposure_duration_ns: Option<u64>,
781 }
782
783 struct CalibrationIdentity {
784 id: String,
785 version: String,
786 }
787
788 /// One camera frame: encoded pixel bytes plus optional calibration
789 /// and timing metadata.
790 struct Frame {
791 width: u32,
792 height: u32,
793 encoding: Encoding,
794 intrinsics: Option<Intrinsics>,
795 distortion: Option<Distortion>,
796 exposure: Option<ExposureTiming>,
797 measured_at_ns: Option<u64>,
798 calibration: Option<CalibrationIdentity>,
799 #[serde(with = "serde_bytes")]
800 data: Vec<u8>,
801 }
802
803 topic frame: state Frame;
804 }
805
806 depth(capability) {
807 #[derive(Copy, Eq)]
808 #[serde(rename_all = "snake_case")]
809 enum Encoding {
810 U16Millimeters,
811 }
812
813 #[derive(Copy, Eq)]
814 #[serde(rename_all = "snake_case")]
815 enum InvalidSamplePolicy {
816 ZeroIsInvalid,
817 NonFiniteIsInvalid,
818 }
819
820 #[derive(Copy)]
821 struct Intrinsics {
822 fx: f32,
823 fy: f32,
824 cx: f32,
825 cy: f32,
826 }
827
828 struct Distortion {
829 model: String,
830 coefficients: Vec<f32>,
831 }
832
833 #[derive(Copy)]
834 struct ExposureTiming {
835 exposure_start_ns: Option<u64>,
836 exposure_duration_ns: Option<u64>,
837 }
838
839 struct CalibrationIdentity {
840 id: String,
841 version: String,
842 }
843
844 /// One depth frame: per-pixel millimetre samples plus optional
845 /// calibration and timing metadata.
846 struct Frame {
847 samples_mm: Vec<u16>,
848 encoding: Encoding,
849 invalid_sample_policy: InvalidSamplePolicy,
850 width: Option<u32>,
851 height: Option<u32>,
852 intrinsics: Option<Intrinsics>,
853 distortion: Option<Distortion>,
854 exposure: Option<ExposureTiming>,
855 measured_at_ns: Option<u64>,
856 calibration: Option<CalibrationIdentity>,
857 }
858
859 topic frame: state Frame;
860 }
861
862 lidar(capability) {
863 #[derive(Copy, Eq)]
864 #[serde(rename_all = "snake_case")]
865 enum SensorHealth {
866 Nominal,
867 Degraded,
868 Fault,
869 }
870
871 #[derive(Copy)]
872 struct ScanGeometry {
873 angle_min_rad: f32,
874 angle_increment_rad: f32,
875 }
876
877 #[derive(Copy)]
878 struct RangeLimits {
879 min_m: f32,
880 max_m: f32,
881 }
882
883 #[derive(Copy)]
884 struct ScanQuality {
885 valid_points: u32,
886 }
887
888 struct Ranges {
889 ranges: Vec<f32>,
890 geometry: Option<ScanGeometry>,
891 limits: Option<RangeLimits>,
892 measured_at_ns: Option<u64>,
893 quality: Option<ScanQuality>,
894 health: SensorHealth,
895 }
896
897 struct Points {
898 points: Vec<[f32; 3]>,
899 limits: Option<RangeLimits>,
900 measured_at_ns: Option<u64>,
901 quality: Option<ScanQuality>,
902 health: SensorHealth,
903 }
904
905 /// One lidar scan, either as polar ranges or as cartesian points.
906 #[serde(tag = "kind", rename_all = "snake_case")]
907 enum Scan {
908 Ranges(Ranges),
909 Points(Points),
910 }
911
912 topic scan: state Scan;
913 }
914
915 mmwave(capability) {
916 /// One mmWave radar detection: position, velocity, and SNR.
917 #[derive(Copy)]
918 struct Detection {
919 position: [f32; 3],
920 velocity: [f32; 3],
921 snr: f32,
922 }
923
924 /// One mmWave radar scan as a set of detections.
925 struct Scan {
926 detections: Vec<Detection>,
927 }
928
929 topic scan: state Scan;
930 }
931
932 microphone(capability) {
933 /// One audio frame as raw encoded bytes.
934 struct Frame {
935 data: Vec<u8>,
936 }
937
938 topic frame: state Frame;
939 }
940
941 led(capability) {
942 /// A per-LED on/off command.
943 #[derive(Copy, Eq)]
944 enum Command {
945 On,
946 Off,
947 }
948
949 topic command: command Command;
950 }
951
952 emergency_stop(capability) {
953 /// Per-instance emergency-stop state.
954 #[derive(Eq)]
955 struct State {
956 engaged: bool,
957 }
958
959 topic state: state State;
960 }
961 }
962
963 odometry {
964 /// A planar pose + twist estimate in the odometry frame.
965 struct State {
966 x_m: f64,
967 y_m: f64,
968 yaw_rad: f64,
969 linear_x_mps: f32,
970 angular_z_radps: f32,
971 }
972
973 topic state: state State;
974 }
975
976 localize {
977 /// A planar localization estimate in the map frame.
978 struct LocalizationState {
979 x_m: f64,
980 y_m: f64,
981 yaw_rad: f64,
982 confidence: f32,
983 }
984
985 topic state: state LocalizationState;
986 }
987
988 presence {
989 /// Per-participant liveness + readiness beacon.
990 enum Readiness {
991 NotStarted,
992 Initializing,
993 Ready,
994 Degraded,
995 Failed,
996 }
997
998 /// A single participant's liveness beacon. Participants publish their
999 /// own heartbeat; the presence participant subscribes them as its control
1000 /// input, hence the `command` role.
1001 struct Heartbeat {
1002 participant: String,
1003 readiness: Readiness,
1004 }
1005
1006 /// The presence participant's aggregate readiness across all participants.
1007 /// Presence publishes it; clients subscribe, hence the `state` role.
1008 struct State {
1009 readiness: Readiness,
1010 }
1011
1012 topic heartbeat: command Heartbeat;
1013 topic state: state State;
1014 }
1015
1016 map {
1017 /// A published map revision marker.
1018 struct Revision {
1019 revision: u64,
1020 resolution_m: f32,
1021 }
1022
1023 /// Request a rectangular submap window (map-frame metres).
1024 struct SubmapRequest {
1025 min_x_m: f64,
1026 min_y_m: f64,
1027 max_x_m: f64,
1028 max_y_m: f64,
1029 }
1030
1031 /// An occupancy-grid window: row-major cells, 0..=100 + 255 = unknown.
1032 struct SubmapResponse {
1033 width: u32,
1034 height: u32,
1035 resolution_m: f32,
1036 cells: Vec<u8>,
1037 }
1038
1039 topic revision: state Revision;
1040 topic submap: query SubmapRequest => SubmapResponse;
1041 }
1042
1043 asset {
1044 /// Fetch a stored asset by path.
1045 struct GetRequest {
1046 path: String,
1047 }
1048
1049 /// The asset bytes, a not-found marker, or a rejected path.
1050 enum GetResponse {
1051 Found { bytes: Vec<u8> },
1052 Missing,
1053 InvalidPath,
1054 }
1055
1056 topic get: query GetRequest => GetResponse;
1057 }
1058 }
1059}
1060
1061#[cfg(test)]
1062mod tests;