Skip to main content

phoxal_api/
lib.rs

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