Skip to main content

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//!
28//! [`Api`]: y2026_1::Api
29//!
30//! # One API version per participant, one per graph
31//!
32//! A participant authors against exactly one version module
33//! (`use phoxal_api::y2026_1 as api;`) and declares it on the derive
34//! (`#[phoxal(api = y2026_1)]`). Every handle body is bound
35//! `ContractBody<Api = R::Api>`, so passing a body from another API version is a
36//! compile error (D59/D60). At deploy time a single graph runs a single API
37//! version: all of its participants share one `Api`, so contracts on the bus are
38//! mutually consistent by construction (D59).
39//!
40//! # Plain serde wire bodies, schema in metadata
41//!
42//! A wire body is just its serde encoding - there is no `{"v":…}` envelope or any
43//! other version tag inside the payload (D62). The four pieces of routing
44//! identity travel as bus metadata alongside the encoded body, not in it:
45//!
46//! - the **schema id** ([`ContractBody::SCHEMA_ID`]), the compatibility key,
47//! - the **version** ([`ApiVersion::ID`], e.g. `"y2026_1"`), informational,
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//! schema id, 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 participant 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 participant 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 is carried in bus metadata as informational provenance.
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 `Service`/`Driver` derive
115///   assertions key off its `Api`/`FAMILY`/`SCHEMA_ID`/`TOPIC`. Runtime decode
116///   compatibility keys off `SCHEMA_ID`; its serde encoding *is* the wire payload,
117///   with 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 participant'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 participant 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 participant 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 participant'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 participant'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 participant'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 participant 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 participant'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 participant'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 participant'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 participant
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 participant / 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 participant 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 participant'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;