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. Normal participants import the
7//! train-selected facade with `use phoxal::api`; concrete modules such as
8//! `phoxal_api::v0_1` remain available to compatibility adapters.
9//!
10//! # Concrete API revisions
11//!
12//! An API revision is a conventional `vM_N` module generated by
13//! [`phoxal_api_tree!`]. Each version module carries:
14//!
15//! - a zero-variant marker `enum Api {}` implementing [`ApiVersion`], whose
16//!   [`ApiVersion::ID`] is the concrete wire identity (for example `"v0.1"`);
17//! - the version-local wire bodies, one `pub mod` per contract node holding plain
18//!   serde structs/enums and their [`ContractBody`] impls;
19//! - an api-local `topic` builder rooted at `topic::client()`.
20//!
21//! From 1.0, published concrete revisions are immutable. Before 1.0 the
22//! framework may make an approved in-place breaking edit without adding a shim
23//! or a new revision; every participant on a robot must move as one train
24//! because mixed pre-1.0 framework trains are unsupported. A child may extend
25//! one earlier revision; the generator materializes the complete child tree
26//! with its own identity. Exactly one `latest` alias is selected for each
27//! framework train.
28//!
29//! [`Api`]: v0_1::Api
30//!
31//! # Train-selected revision and per-contract identity
32//!
33//! A participant creates typed bus handles during
34//! `phoxal::Participant::setup`. Official participants
35//! name contract types through the complete train-selected facade. Embedded
36//! participant metadata carries `{id, config_schema}`.
37//! Across the graph, compatibility is **name identity** (D1) - two participants
38//! interoperate on a contract iff they use the exact same version-qualified name
39//! (`v0.1::drive::Target`), which is real on the wire because the revision
40//! is folded into the key ([`ContractBody::TOPIC`]). From 1.0 onward a stable
41//! contract type is immutable, so the name is the whole identity. Before 1.0,
42//! that identity is train-scoped and an in-place edit requires the whole robot
43//! graph to upgrade together.
44//!
45//! # Plain serde wire bodies, provenance in metadata
46//!
47//! A wire body is just its serde encoding - there is no `{"v":…}` envelope or any
48//! other version tag inside the payload (D62). Identity lives entirely in the
49//! Zenoh key (the version-qualified [`ContractBody::TOPIC`]); the bus metadata
50//! alongside the encoded body carries only provenance (source + logical time) and
51//! the codec that produced the bytes - never schema/family/version. Keeping
52//! identity out of both the payload and the metadata means the body bytes for an
53//! unchanged contract are identical across codecs, and a receiver's per-key
54//! subscription is the whole fast-reject.
55//!
56//! # Topic
57//!
58//! [`ContractBody::TOPIC`] is derived from the contract node's path in the tree,
59//! never written by hand: the version, then the `/`-joined node path plus the
60//! topic leaf, with each dynamic node contributing a `{var}` placeholder, e.g.
61//! `v0.1/component/{instance}/motor/{capability}/command`. A fully static path
62//! has a literal key (`v0.1/drive/state`). Folding the revision into the key
63//! (D1) is what makes two differently-versioned contracts physically distinct
64//! Zenoh keys, so they cannot collide.
65//!
66//! # The api-local topic builder
67//!
68//! Each version module exposes a `topic` builder that mirrors the node tree:
69//! `api::topic::client()` returns a root, one method per top-level node walks down the
70//! tree, a dynamic node's method takes its variable as `impl Display`, and a leaf
71//! method binds the topic's side-branded kind to its version-local body. For
72//! example `api::topic::client().drive().state()` yields a
73//! `Topic<Subscribe<drive::State>>` (the CLIENT observes the owner's `state`) over
74//! the version-qualified key `v0.1/drive/state`, and
75//! `api::topic::client().component("base").motor("left").command()` fills the dynamic
76//! segments to produce `v0.1/component/base/motor/left/command`. Because the
77//! builder is generated from the same tree as `TOPIC`, the built key and the
78//! documented key stay in lockstep.
79//!
80//! ## Owner side: `topic::owner`
81//!
82//! The PUBLIC `topic::client()...` chain above is the **client** side. The matching
83//! **owner** side lives at `api::topic::owner()...`:
84//! the same node tree and keys, but the leaf brands flip so the owner gets the side
85//! it must take - `api::topic::owner().drive().state()` is
86//! `Topic<Publish<drive::State>>` (the owner publishes its telemetry), and
87//! `api::topic::owner().drive().target()` is `Topic<Subscribe<drive::Target>>`
88//! (the owner reads its command input). A query owner reaches its `ServeQuery`
89//! brand the same way. The owner chain makes that ownership explicit; a
90//! participant acquires the topics of its OWN node through it and everything it
91//! consumes through the client chain.
92
93use phoxal_macros::phoxal_api_tree;
94
95/// The contract primitive traits, re-exported from the `phoxal-bus` crate (the
96/// ABI floor) so they stay addressable at `phoxal_api::ApiVersion` /
97/// `phoxal_api::ContractBody`.
98///
99/// - [`ApiVersion`] is the marker trait identifying one API version (D60),
100///   implemented only by the zero-variant `enum Api {}` that [`phoxal_api_tree!`]
101///   generates inside each revision module; its `ID` is the concrete dotted
102///   wire identity (for example `"v0.1"`).
103/// - [`ContractBody`] is a version-local wire body (D61): a plain serde type
104///   bound to exactly one [`ApiVersion`] and one contract topic. Every body
105///   declared inside a [`phoxal_api_tree!`] node gets a generated impl; handles,
106///   `SetupContext` builders, and the `Service`/`Driver` derive assertions key
107///   off its `Api`/`TOPIC`. `TOPIC` is version-qualified (D1) and is the
108///   compatibility key; its serde encoding is the wire payload, with no version
109///   envelope (D62).
110pub use phoxal_bus::{ApiVersion, ContractBody};
111
112phoxal_api_tree! {
113    version v0_1 {
114        drive {
115            /// Why actuation authority is in its current state.
116            enum StopReason {
117                /// Nothing is live: no target has been accepted, the producer
118                /// has gone silent past the host deadline, or the held command
119                /// exceeded its logical hold horizon. All three are the same
120                /// fact to a consumer - the drive is not being commanded.
121                TargetStale,
122                TargetNotFinite,
123                ActuatorCommandNotFinite,
124                Inactive,
125                EmergencyStop,
126                Fault,
127            }
128
129            /// Whether the drive is actively commanding the actuators.
130            enum ActuatorAuthority {
131                Active,
132                Stopped,
133            }
134
135            /// A requested or limited planar velocity.
136            struct Target {
137                linear_x_mps: f32,
138                angular_z_radps: f32,
139                curvature_limit_radpm: Option<f32>,
140            }
141
142            /// The drive participant's published control state.
143            struct State {
144                target: Target,
145                limited_target: Target,
146                actuator_authority: ActuatorAuthority,
147                stop_reason: Option<StopReason>,
148            }
149
150            topic target: command Target;
151            topic state: state State;
152        }
153
154        joint(joint) {
155            /// Per-joint position/velocity (and optional effort) on a dynamic
156            /// per-joint key.
157            struct JointState {
158                position_rad: f64,
159                velocity_radps: f64,
160                effort_nm: Option<f64>,
161            }
162
163            topic state: state JointState;
164        }
165
166        frame {
167            /// A parent → child rigid transform (translation + xyzw quaternion).
168            struct FrameTransform {
169                parent_frame_id: String,
170                child_frame_id: String,
171                translation_m: [f64; 3],
172                rotation_quat_xyzw: [f64; 4],
173                /// When this transform was observed. Absent for a static
174                /// transform, which is configuration rather than observation.
175                stamp: Option<::phoxal_bus::RobotInstant>,
176            }
177
178            /// Transforms that do not change over time.
179            struct StaticTransforms {
180                transforms: Vec<FrameTransform>,
181            }
182
183            /// The current transform tree.
184            struct Tree {
185                transforms: Vec<FrameTransform>,
186            }
187
188            /// Ask for the transform between two frames, optionally at a time.
189            struct LookupRequest {
190                target_frame_id: String,
191                source_frame_id: String,
192                /// The instant to resolve at. Absent asks for the latest.
193                at: Option<::phoxal_bus::RobotInstant>,
194            }
195
196            /// The resolved transform, or `None` if it is not available.
197            struct LookupResponse {
198                transform: Option<FrameTransform>,
199            }
200
201            topic tree: state Tree;
202            topic static_transforms: state StaticTransforms;
203            topic lookup: query LookupRequest => LookupResponse;
204        }
205
206        power {
207            /// A platform power command.
208            #[derive(Copy, Eq)]
209            enum Command {
210                Reboot,
211                Shutdown,
212            }
213
214            /// Where the power participant is in handling a command.
215            #[derive(Copy, Eq)]
216            enum Status {
217                Idle,
218                Rebooting,
219                ShuttingDown,
220                Failed,
221            }
222
223            /// Why a power command was rejected outright.
224            #[derive(Copy, Eq)]
225            #[serde(rename_all = "snake_case")]
226            enum RejectedReason {
227                HostIntegrationUnavailable,
228                CommandRejected,
229            }
230
231            /// Why an accepted power command later failed.
232            #[derive(Copy, Eq)]
233            #[serde(rename_all = "snake_case")]
234            enum FailedReason {
235                HostCommandFailed,
236            }
237
238            /// The power participant's published state.
239            struct State {
240                status: Status,
241                detail: Option<String>,
242            }
243
244            topic command: command Command;
245            topic state: state State;
246        }
247
248        motion {
249            struct Target {
250                linear_x_mps: f32,
251                angular_z_radps: f32,
252                curvature_limit_radpm: Option<f32>,
253            }
254
255            #[derive(Copy, Eq)]
256            #[serde(rename_all = "snake_case")]
257            enum Source {
258                Manual,
259                Navigation,
260                EmergencyStop,
261            }
262
263            #[derive(Copy, Eq)]
264            #[serde(rename_all = "snake_case")]
265            enum ZeroReason {
266                NoCandidate,
267                NavigationCandidateStale,
268                ManualCandidateNotFinite,
269                NavigationCandidateNotFinite,
270                EmergencyStopEngaged,
271                SafetyConstraintsUnavailable,
272                SafetyProtectiveStop,
273            }
274
275            #[derive(Copy, Eq)]
276            #[serde(rename_all = "snake_case")]
277            enum SafetyRuntime {
278                Absent,
279                Present,
280            }
281
282            struct ManualCommand {
283                linear_x_mps: f64,
284                angular_z_radps: f64,
285            }
286
287            struct State {
288                /// How long ago motion observed the live manual command, on
289                /// its own host clock. `None` when no manual command is live.
290                manual_observed_age_ns: Option<u64>,
291                autonomous_candidate_age_ns: Option<u64>,
292                safety_constraints_age_ns: Option<u64>,
293                selected_source: Option<Source>,
294                final_target: Target,
295                zero_reason: Option<ZeroReason>,
296                safety_runtime: SafetyRuntime,
297                component_estop_blocked: bool,
298                active_safety_constraints: Vec<super::safety::Constraint>,
299            }
300
301            topic manual: command ManualCommand;
302            topic state: state State;
303        }
304
305        safety {
306            /// Why safety is stopping or limiting body motion.
307            #[derive(Copy, Eq)]
308            #[serde(rename_all = "snake_case")]
309            enum ConstraintReason {
310                WorldUnavailable,
311                MapUnavailable,
312                DrivableSpaceUnavailable,
313                LocalizationUnavailable,
314                LocalizationUncertain,
315                ObstacleProximity,
316                RangeSensorFault,
317                DriveFault,
318                BatteryLow,
319                BatteryCritical,
320                SpeedZone,
321                OperatorPolicy,
322            }
323
324            /// Typed origin of one constraint, suitable for operator diagnosis.
325            #[derive(Copy, Eq)]
326            #[serde(rename_all = "snake_case")]
327            enum ConstraintSourceKind {
328                WorldModel,
329                Map,
330                Localization,
331                Range,
332                Drive,
333                Battery,
334                Operator,
335            }
336
337            struct ConstraintSource {
338                kind: ConstraintSourceKind,
339                participant_id: String,
340                component_id: Option<String>,
341                capability_id: Option<String>,
342            }
343
344            struct Constraint {
345                reason: ConstraintReason,
346                source: ConstraintSource,
347                stop: bool,
348                max_linear_speed_mps: Option<f32>,
349                max_angular_speed_radps: Option<f32>,
350                observed_value: Option<f32>,
351                /// The instant this constraint starts applying, on the
352                /// publisher's timeline. A consumer on another timeline gets a
353                /// checked error, never a silently wrong comparison.
354                valid_from: ::phoxal_bus::RobotInstant,
355                /// The instant this constraint stops applying.
356                expires_at: ::phoxal_bus::RobotInstant,
357            }
358
359            /// The sole safety-to-motion control product. Motion accepts it only
360            /// on the same timeline and before `expires_at`.
361            struct MotionConstraints {
362                sequence: u64,
363                stop: bool,
364                max_linear_speed_mps: Option<f32>,
365                max_angular_speed_radps: Option<f32>,
366                constraints: Vec<Constraint>,
367                expires_at: ::phoxal_bus::RobotInstant,
368            }
369
370            /// Operator-facing state mirrors the exact product consumed by motion.
371            struct State {
372                clear: bool,
373                motion: MotionConstraints,
374            }
375
376            topic constraints: state MotionConstraints;
377            topic state: state State;
378        }
379
380        navigation {
381            #[derive(Eq)]
382            struct RequestId {
383                value: String,
384            }
385
386            struct Pose {
387                x_m: f64,
388                y_m: f64,
389                yaw_rad: Option<f64>,
390            }
391
392            struct Path {
393                poses: Vec<Pose>,
394                map_revision: Option<u64>,
395            }
396
397            enum RequestKind {
398                GotoPose(Pose),
399                FollowPath(Path),
400                Cancel(RequestId),
401            }
402
403            struct Request {
404                request_id: RequestId,
405                kind: RequestKind,
406            }
407
408            enum State {
409                Idle,
410                Accepted(RequestId),
411                Running(RequestId),
412            }
413
414            #[derive(Copy, Eq)]
415            #[serde(rename_all = "snake_case")]
416            enum FailureReason {
417                LocalizationUnavailable,
418                MapUnavailable,
419                MapChanged,
420                NoPath,
421                Blocked,
422                Internal,
423            }
424
425            #[derive(Copy, Eq)]
426            #[serde(rename_all = "snake_case")]
427            enum RefusalReason {
428                Busy,
429                InvalidRequest,
430                Unsupported,
431            }
432
433            enum Outcome {
434                Succeeded,
435                Failed(FailureReason),
436                Refused(RefusalReason),
437                Cancelled,
438                TimedOut,
439            }
440
441            struct Progress {
442                request_id: RequestId,
443                distance_remaining_m: f64,
444                path_index: u32,
445            }
446
447            struct Result {
448                request_id: RequestId,
449                outcome: Outcome,
450            }
451
452            struct Candidate {
453                request_id: RequestId,
454                linear_x_mps: f32,
455                angular_z_radps: f32,
456            }
457
458            struct FrontierRequest {
459                map_revision: Option<u64>,
460            }
461
462            struct Frontier {
463                x_m: f64,
464                y_m: f64,
465                score: f32,
466                size: u32,
467            }
468
469            struct FrontierResponse {
470                frontier: Option<Frontier>,
471                map_revision: Option<u64>,
472            }
473
474            topic request: command Request;
475            topic state: state State;
476            topic progress: state Progress;
477            topic result: state Result;
478            topic candidate: state Candidate;
479            topic next_frontier: query FrontierRequest => FrontierResponse;
480        }
481
482        logs(participant_id) {
483            /// Wall-clock timestamp carried by a structured bus log event.
484            struct Timestamp {
485                unix_seconds: i64,
486                nanos: u32,
487            }
488
489            /// The severity level of a structured bus log event.
490            #[derive(Copy, Eq)]
491            #[serde(rename_all = "snake_case")]
492            enum Level {
493                Error,
494                Warn,
495                Info,
496                Debug,
497                Trace,
498            }
499
500            /// A scalar tracing field value captured from a log event.
501            #[serde(untagged)]
502            enum LogValue {
503                Bool(bool),
504                I64(i64),
505                U64(u64),
506                F64(f64),
507                String(String),
508            }
509
510            /// One structured runner log event published out-of-band.
511            struct Event {
512                seq: u64,
513                time: Timestamp,
514                level: Level,
515                target: String,
516                message: String,
517                fields: ::std::collections::BTreeMap<String, LogValue>,
518                /// Complete records lost before publication because a bounded
519                /// queue or publish attempt was saturated.
520                dropped: u32,
521                /// Values or fields truncated inside this published record to
522                /// keep its wire representation bounded.
523                #[serde(default)]
524                truncated: u32,
525            }
526
527            topic self: diagnostic Event;
528        }
529
530
531
532
533
534
535        perception {
536            /// A single detected object: class, confidence, and pose in a frame.
537            struct Detection {
538                class_id: String,
539                confidence: f32,
540                position_m: [f64; 3],
541                frame_id: String,
542                track_id: Option<u64>,
543            }
544
545            /// A batch of detections from one perception cycle.
546            struct Detections {
547                detections: Vec<Detection>,
548                /// The frame instant these detections were derived from.
549                stamp: Option<::phoxal_bus::RobotInstant>,
550            }
551
552            /// The perception participant's published health.
553            struct State {
554                healthy: bool,
555                detector: String,
556            }
557
558            topic detections: state Detections;
559            topic state: state State;
560        }
561
562        video {
563            /// Ask to open a video stream for a capability at an optional size.
564            struct OpenRequest {
565                capability: String,
566                width_px: Option<u32>,
567                height_px: Option<u32>,
568            }
569
570            /// The id of the stream that was opened.
571            struct OpenResponse {
572                stream_id: String,
573            }
574
575            topic open: query OpenRequest => OpenResponse;
576
577            stream(stream) {
578                /// Where one open video stream is in its lifecycle.
579                #[derive(Copy, Eq)]
580                #[serde(rename_all = "snake_case")]
581                enum StreamPhase {
582                    Starting,
583                    Active,
584                    Stopped,
585                }
586
587                /// The published state of one video stream: its lifecycle phase
588                /// and the number of source frames seen so far. The video participant
589                /// publishes it per stream; clients subscribe, hence `state`.
590                struct StreamState {
591                    phase: StreamPhase,
592                    frames_seen: u64,
593                }
594
595                topic state: state StreamState;
596            }
597        }
598
599        simulation {
600            /// The authoritative advancing simulation clock. Publication means
601            /// the world advanced; silence means it did not.
602            ///
603            /// The timeline and instant ride in the envelope, like every other
604            /// `state`-shaped publication - the world authority stamps them with
605            /// a world step token. The body carries only the step counter, which
606            /// is not derivable from the envelope.
607            struct Clock {
608                step: u64,
609            }
610
611            // `world_clock`, not `state`: only the world-authority participant
612            // (`#[phoxal::simulator]`) may publish it, enforced at compile time
613            // by the disjoint `WorldClockContract` this role generates instead
614            // of `StateContract`; see
615            // `phoxal_bus::contract::WorldClockContract`'s docs.
616            topic clock: world_clock Clock;
617        }
618
619        // Per-instance component capabilities (D17/D38: framework participant / driver
620        // territory). `component(instance)` selects a manifest-declared component;
621        // each child `kind(capability)` is a self-contained node whose key is
622        // `component/{instance}/<kind>/{capability}/<leaf>`. Nodes duplicate any
623        // types they share by design - the node path disambiguates, so the names
624        // are path-local.
625        component(instance) {
626            motor(capability) {
627                /// A per-actuator command.
628                enum Command {
629                    Velocity(f32),
630                    Torque(f32),
631                    Stop,
632                }
633
634                topic command: command Command;
635            }
636
637            encoder(capability) {
638                /// Per-encoder sample on a dynamic per-instance key.
639                struct Sample {
640                    position_rad: f64,
641                    velocity_radps: f32,
642                }
643
644                topic sample: measurement Sample;
645            }
646
647            accelerometer(capability) {
648                /// Raw accelerometer sample in the sensor-local frame in m/s^2.
649                struct Sample {
650                    linear_acceleration: [f32; 3],
651                }
652
653                topic sample: measurement Sample;
654            }
655
656            gyroscope(capability) {
657                /// Raw angular velocity sample in the sensor-local frame in rad/s.
658                struct Sample {
659                    angular_velocity: [f32; 3],
660                }
661
662                topic sample: measurement Sample;
663            }
664
665            magnetometer(capability) {
666                /// Raw magnetic-field sample in the sensor-local frame.
667                struct Sample {
668                    magnetic_field: [f32; 3],
669                }
670
671                topic sample: measurement Sample;
672            }
673
674            imu(capability) {
675                #[derive(Copy, Eq)]
676                #[serde(rename_all = "snake_case")]
677                enum SensorHealth {
678                    Nominal,
679                    Degraded,
680                    Fault,
681                }
682
683                #[derive(Copy)]
684                struct Bias {
685                    angular_velocity_radps: [f32; 3],
686                    linear_acceleration_mps2: [f32; 3],
687                }
688
689                struct Sample {
690                    orientation: Option<[f32; 4]>,
691                    angular_velocity_radps: [f32; 3],
692                    linear_acceleration_mps2: [f32; 3],
693                    covariance: Option<[f32; 9]>,
694                    noise_density: Option<[f32; 3]>,
695                    sensor_frame_id: Option<String>,
696                    health: SensorHealth,
697                    bias: Option<Bias>,
698                }
699
700                topic sample: measurement Sample;
701            }
702
703            range(capability) {
704                #[derive(Copy, Eq)]
705                #[serde(rename_all = "snake_case")]
706                enum SensorHealth {
707                    Nominal,
708                    Degraded,
709                    Fault,
710                }
711
712                #[derive(Copy)]
713                struct Limits {
714                    min_m: f32,
715                    max_m: f32,
716                }
717
718                #[derive(Copy)]
719                struct SampleQuality {
720                    valid: bool,
721                    confidence: Option<f32>,
722                }
723
724                struct Sample {
725                    distance_m: f32,
726                    limits: Option<Limits>,
727                    quality: Option<SampleQuality>,
728                    health: SensorHealth,
729                }
730
731                topic sample: measurement Sample;
732            }
733
734            gnss(capability) {
735                /// A GNSS fix: geodetic position plus a 3x3 position covariance.
736                struct Sample {
737                    latitude: f64,
738                    longitude: f64,
739                    altitude: f64,
740                    position_covariance: [f64; 9],
741                }
742
743                topic sample: measurement Sample;
744            }
745
746            camera(capability) {
747                #[derive(Copy, Eq)]
748                #[serde(rename_all = "snake_case")]
749                enum Encoding {
750                    Jpeg,
751                    Png,
752                    L8,
753                    Rgb8,
754                    Rgba8,
755                }
756
757                #[derive(Copy)]
758                struct Intrinsics {
759                    fx: f32,
760                    fy: f32,
761                    cx: f32,
762                    cy: f32,
763                }
764
765                struct Distortion {
766                    model: String,
767                    coefficients: Vec<f32>,
768                }
769
770                #[derive(Copy)]
771                struct ExposureTiming {
772                    exposure_start_ns: Option<u64>,
773                    exposure_duration_ns: Option<u64>,
774                }
775
776                struct CalibrationIdentity {
777                    id: String,
778                    version: String,
779                }
780
781                /// One camera frame: encoded pixel bytes plus optional calibration
782                /// and timing metadata.
783                struct Frame {
784                    width: u32,
785                    height: u32,
786                    encoding: Encoding,
787                    intrinsics: Option<Intrinsics>,
788                    distortion: Option<Distortion>,
789                    exposure: Option<ExposureTiming>,
790                    calibration: Option<CalibrationIdentity>,
791                    #[serde(with = "serde_bytes")]
792                    data: Vec<u8>,
793                }
794
795                topic frame: measurement Frame;
796            }
797
798            depth(capability) {
799                #[derive(Copy, Eq)]
800                #[serde(rename_all = "snake_case")]
801                enum Encoding {
802                    U16Millimeters,
803                }
804
805                #[derive(Copy, Eq)]
806                #[serde(rename_all = "snake_case")]
807                enum InvalidSamplePolicy {
808                    ZeroIsInvalid,
809                    NonFiniteIsInvalid,
810                }
811
812                #[derive(Copy)]
813                struct Intrinsics {
814                    fx: f32,
815                    fy: f32,
816                    cx: f32,
817                    cy: f32,
818                }
819
820                struct Distortion {
821                    model: String,
822                    coefficients: Vec<f32>,
823                }
824
825                #[derive(Copy)]
826                struct ExposureTiming {
827                    exposure_start_ns: Option<u64>,
828                    exposure_duration_ns: Option<u64>,
829                }
830
831                struct CalibrationIdentity {
832                    id: String,
833                    version: String,
834                }
835
836                /// One depth frame: per-pixel millimetre samples plus optional
837                /// calibration and timing metadata.
838                struct Frame {
839                    samples_mm: Vec<u16>,
840                    encoding: Encoding,
841                    invalid_sample_policy: InvalidSamplePolicy,
842                    width: Option<u32>,
843                    height: Option<u32>,
844                    intrinsics: Option<Intrinsics>,
845                    distortion: Option<Distortion>,
846                    exposure: Option<ExposureTiming>,
847                    calibration: Option<CalibrationIdentity>,
848                }
849
850                topic frame: measurement Frame;
851            }
852
853            lidar(capability) {
854                #[derive(Copy, Eq)]
855                #[serde(rename_all = "snake_case")]
856                enum SensorHealth {
857                    Nominal,
858                    Degraded,
859                    Fault,
860                }
861
862                #[derive(Copy)]
863                struct ScanGeometry {
864                    angle_min_rad: f32,
865                    angle_increment_rad: f32,
866                }
867
868                #[derive(Copy)]
869                struct RangeLimits {
870                    min_m: f32,
871                    max_m: f32,
872                }
873
874                #[derive(Copy)]
875                struct ScanQuality {
876                    valid_points: u32,
877                }
878
879                struct Ranges {
880                    ranges: Vec<f32>,
881                    geometry: Option<ScanGeometry>,
882                    limits: Option<RangeLimits>,
883                    quality: Option<ScanQuality>,
884                    health: SensorHealth,
885                }
886
887                struct Points {
888                    points: Vec<[f32; 3]>,
889                    limits: Option<RangeLimits>,
890                    quality: Option<ScanQuality>,
891                    health: SensorHealth,
892                }
893
894                /// One lidar scan, either as polar ranges or as cartesian points.
895                #[serde(tag = "kind", rename_all = "snake_case")]
896                enum Scan {
897                    Ranges(Ranges),
898                    Points(Points),
899                }
900
901                topic scan: measurement Scan;
902            }
903
904            mmwave(capability) {
905                /// One mmWave radar detection: position, velocity, and SNR.
906                #[derive(Copy)]
907                struct Detection {
908                    position: [f32; 3],
909                    velocity: [f32; 3],
910                    snr: f32,
911                }
912
913                /// One mmWave radar scan as a set of detections.
914                struct Scan {
915                    detections: Vec<Detection>,
916                }
917
918                topic scan: measurement Scan;
919            }
920
921            microphone(capability) {
922                /// One audio frame as raw encoded bytes.
923                struct Frame {
924                    data: Vec<u8>,
925                }
926
927                topic frame: measurement Frame;
928            }
929
930            led(capability) {
931                /// A per-LED on/off command.
932                #[derive(Copy, Eq)]
933                enum Command {
934                    On,
935                    Off,
936                }
937
938                topic command: command Command;
939            }
940
941            speaker(capability) {
942                /// One chunk of an audio stream to play on this speaker.
943                ///
944                /// `Some(bytes)` carries WAV-coded audio: the first chunk of a
945                /// stream starts with the standard WAV header, later chunks
946                /// continue its data. `None` ends the stream and is what tells
947                /// the owner the sound is complete.
948                struct Chunk {
949                    stream: Option<Vec<u8>>,
950                }
951
952                topic stream: command Chunk;
953            }
954
955            battery(capability) {
956                /// Battery state reported by the pack's owner - the simulator
957                /// backing this capability, or the real driver.
958                struct State {
959                    voltage_v: f32,
960                    current_a: f32,
961                    charge_ratio: f32,
962                }
963
964                topic state: state State;
965            }
966
967            emergency_stop(capability) {
968                /// Per-instance emergency-stop state.
969                #[derive(Eq)]
970                struct State {
971                    engaged: bool,
972                }
973
974                topic state: state State;
975            }
976        }
977
978        odometry {
979            /// A planar pose + twist estimate in the odometry frame.
980            struct State {
981                x_m: f64,
982                y_m: f64,
983                yaw_rad: f64,
984                linear_x_mps: f32,
985                angular_z_radps: f32,
986            }
987
988            topic state: state State;
989        }
990
991        localize {
992            /// A planar localization estimate in the map frame.
993            struct LocalizationState {
994                x_m: f64,
995                y_m: f64,
996                yaw_rad: f64,
997                confidence: f32,
998            }
999
1000            topic state: state LocalizationState;
1001        }
1002
1003        map {
1004            /// A published map revision marker.
1005            struct Revision {
1006                revision: u64,
1007                resolution_m: f32,
1008            }
1009
1010            /// Request a rectangular submap window (map-frame metres).
1011            struct SubmapRequest {
1012                min_x_m: f64,
1013                min_y_m: f64,
1014                max_x_m: f64,
1015                max_y_m: f64,
1016            }
1017
1018            /// An occupancy-grid window: row-major cells, 0..=100 + 255 = unknown.
1019            struct SubmapResponse {
1020                width: u32,
1021                height: u32,
1022                resolution_m: f32,
1023                cells: Vec<u8>,
1024            }
1025
1026            topic revision: state Revision;
1027            topic submap: query SubmapRequest => SubmapResponse;
1028        }
1029
1030        // Contracts the supervisor itself answers. The node is part of the
1031        // wire key, so a reader can tell from the key alone that the supervisor
1032        // is the authority - and a stale participant sitting on an old key
1033        // physically cannot answer one of these (organization#978).
1034        supervisor {
1035            /// Opaque identity for one supervisor collector together with a
1036            /// position in its completed follow stream. A snapshot cursor
1037            /// covers the retained completed items; a bus snapshot's optional
1038            /// `current` window deliberately has the next sequence. Consumers
1039            /// compare `generation` for equality only and must never parse or
1040            /// order it.
1041            #[derive(Eq)]
1042            struct Cursor {
1043                generation: String,
1044                sequence: u64,
1045            }
1046
1047            /// Which side of one participant-local bus buffer a runtime row
1048            /// measures. The version-qualified `topic` field remains the wire
1049            /// identity; direction is never inferred from its spelling.
1050            #[derive(Copy, Eq, Ord, PartialOrd)]
1051            #[serde(rename_all = "snake_case")]
1052            enum RuntimeDirection {
1053                Publish,
1054                Subscribe,
1055                /// Used only by the bounded overflow row, which may combine
1056                /// omitted rows from both directions.
1057                Mixed,
1058            }
1059
1060            /// The concrete bounded buffer whose pressure a runtime row
1061            /// measures.
1062            #[derive(Copy, Eq, Ord, PartialOrd)]
1063            #[serde(rename_all = "snake_case")]
1064            enum RuntimeBufferKind {
1065                /// Per-topic view of the one shared process outbound queue.
1066                /// Its sample capacity is repeated on each row and must not be
1067                /// summed; the queue's separate byte pressure is not in v0.1.
1068                Outbound,
1069                /// Keep-last slot. Depth `1` means occupied, never backlog.
1070                Latest,
1071                Subscriber,
1072                /// Used only by the bounded overflow row.
1073                Mixed,
1074            }
1075
1076            /// Host-monotonic scheduled-step work completed during one rollup
1077            /// window. An unscheduled participant reports `None` instead.
1078            struct RuntimeStep {
1079                target_period_ns: u64,
1080                completed: u64,
1081                errors: u64,
1082                mean_duration_ns: u64,
1083                max_duration_ns: u64,
1084                mean_lateness_ns: u64,
1085                max_lateness_ns: u64,
1086                missed_ticks: u64,
1087                overruns: u64,
1088            }
1089
1090            /// One exact version-qualified topic/direction/buffer row. These
1091            /// are process-lifetime setup declarations: dropping an authoring
1092            /// handle does not dynamically unregister a row. Empty `topic`
1093            /// plus `Mixed` direction/kind identifies the explicit overflow
1094            /// row; `overflowed_rows` is zero on normal rows.
1095            struct RuntimeTopic {
1096                topic: String,
1097                direction: RuntimeDirection,
1098                buffer_kind: RuntimeBufferKind,
1099                count: u64,
1100                /// Finite, non-negative message rate. Retention tools clamp
1101                /// malformed non-finite inputs before they reach snapshots.
1102                rate_hz: f32,
1103                drops: u64,
1104                latest_overwrites: u64,
1105                bounded_evictions: u64,
1106                /// Sample capacity. Outbound rows repeat the shared process
1107                /// queue capacity and are non-additive; byte pressure is not
1108                /// represented. Latest capacity/depth describe slot occupancy.
1109                capacity: u64,
1110                current_depth: u64,
1111                high_water_depth: u64,
1112                decode_errors: u64,
1113                /// Samples discarded because they belonged to a retired world
1114                /// history, or because a quarantined candidate timeline was
1115                /// purged when a different one became authoritative.
1116                timeline_filtered: u64,
1117                overflowed_rows: u32,
1118            }
1119
1120
1121
1122
1123            telemetry {
1124                /// Runner-originated portable performance rollup. The runner
1125                /// publishes at most one per host-monotonic grid interval;
1126                /// envelope provenance identifies the participant. Interval
1127                /// counters are best-effort sequential atomic samples, not a
1128                /// transactional stop-the-world boundary, so concurrent queue
1129                /// activity may land on either neighboring rollup.
1130                struct Rollup {
1131                    window_ns: u64,
1132                    step: Option<crate::v0_1::supervisor::RuntimeStep>,
1133                    topics: Vec<crate::v0_1::supervisor::RuntimeTopic>,
1134                    overflow: Option<crate::v0_1::supervisor::RuntimeTopic>,
1135                }
1136
1137                /// Requests the supervisor's current bounded five-minute
1138                /// runtime history.
1139                struct SnapshotRequest {
1140                    /// Optional exact participant filter. `None` selects the
1141                    /// complete per-robot history.
1142                    participant_id: Option<String>,
1143                    /// Maximum newest records to return. Zero selects the
1144                    /// supervisor's bounded default.
1145                    limit: u32,
1146                    /// Exclusive global ingest-sequence upper bound for
1147                    /// backward pagination. `None` starts at the newest record.
1148                    before_sequence: Option<u64>,
1149                }
1150
1151                /// One retained rollup. `sequence` is assigned by
1152                /// the supervisor at ingest, independent of producer metadata.
1153                /// Duplicate normal topic keys are deterministically
1154                /// re-aggregated before row bounds are applied.
1155                struct Record {
1156                    sequence: u64,
1157                    participant_id: String,
1158                    /// Text values truncated by the supervisor's ingest bound.
1159                    /// Oversized/excess topic identities are not truncated;
1160                    /// they are aggregated into the explicit overflow row.
1161                    truncated: u32,
1162                    window_ns: u64,
1163                    step: Option<crate::v0_1::supervisor::RuntimeStep>,
1164                    topics: Vec<crate::v0_1::supervisor::RuntimeTopic>,
1165                    overflow: Option<crate::v0_1::supervisor::RuntimeTopic>,
1166                }
1167
1168                struct Snapshot {
1169                    cursor: crate::v0_1::supervisor::Cursor,
1170                    records: Vec<Record>,
1171                    /// Records evicted by the absolute memory cap before their
1172                    /// five-minute age horizon elapsed.
1173                    capacity_evictions: u64,
1174                    /// Pass this as the next request's `before_sequence` to
1175                    /// continue backward. `None` means the retained matching
1176                    /// history is complete.
1177                    next_before_sequence: Option<u64>,
1178                }
1179
1180                struct Follow {
1181                    cursor: crate::v0_1::supervisor::Cursor,
1182                    record: Record,
1183                }
1184
1185                topic rollup: diagnostic Rollup;
1186                topic snapshot: query SnapshotRequest => Snapshot;
1187                topic follow: diagnostic Follow;
1188            }
1189                log {
1190                    /// Requests the supervisor's complete current bounded log snapshot. The
1191                    /// first protocol version intentionally has no pagination or
1192                    /// filtering surface.
1193                    struct SnapshotRequest {}
1194
1195                    /// Wall-clock timestamp copied from one participant-originated
1196                    /// structured `v0.1::logs` event.
1197                    struct Timestamp {
1198                        unix_seconds: i64,
1199                        nanos: u32,
1200                    }
1201
1202                    #[derive(Copy, Eq)]
1203                    #[serde(rename_all = "snake_case")]
1204                    enum Level {
1205                        Error,
1206                        Warn,
1207                        Info,
1208                        Debug,
1209                        Trace,
1210                    }
1211
1212                    #[serde(untagged)]
1213                    enum LogValue {
1214                        Bool(bool),
1215                        I64(i64),
1216                        U64(u64),
1217                        F64(f64),
1218                        String(String),
1219                    }
1220
1221                    /// One retained participant log. `sequence` is assigned by
1222                    /// the supervisor at ingest and is independent of the producer's
1223                    /// `source_sequence`.
1224                    struct Record {
1225                        sequence: u64,
1226                        participant_id: String,
1227                        source_sequence: u64,
1228                        time: Timestamp,
1229                        level: Level,
1230                        target: String,
1231                        message: String,
1232                        fields: ::std::collections::BTreeMap<String, LogValue>,
1233                        dropped: u32,
1234                        truncated: u32,
1235                    }
1236
1237                    /// The complete bounded log state at `cursor`.
1238                    struct Snapshot {
1239                        cursor: crate::v0_1::supervisor::Cursor,
1240                        /// Cumulative structured log samples evicted from
1241                        /// the supervisor's bounded ingest subscriber in this process.
1242                        /// An increase is observable, unrecoverable source loss;
1243                        /// it is distinct from producer-side `Record::dropped`.
1244                        ingest_dropped: u64,
1245                        records: Vec<Record>,
1246                    }
1247
1248                    /// One live record following the snapshot query. A consumer
1249                    /// must re-query when the generation changes or the sequence is
1250                    /// not exactly one after its installed cursor.
1251                    struct Follow {
1252                        cursor: crate::v0_1::supervisor::Cursor,
1253                        /// Current cumulative the supervisor's log collector ingest loss counter.
1254                        ingest_dropped: u64,
1255                        record: Record,
1256                    }
1257
1258                    topic snapshot: query SnapshotRequest => Snapshot;
1259                    topic follow: diagnostic Follow;
1260                }
1261            asset {
1262                /// Fetch a stored asset by path.
1263                struct GetRequest {
1264                    path: String,
1265                }
1266
1267                /// The asset bytes, a not-found marker, or a rejected path.
1268                enum GetResponse {
1269                    Found { bytes: Vec<u8> },
1270                    Missing,
1271                    InvalidPath,
1272                }
1273
1274                topic get: query GetRequest => GetResponse;
1275            }
1276        }
1277
1278    }
1279    latest v0_1;
1280}
1281
1282#[cfg(test)]
1283mod tests;