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//! 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;