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