Skip to main content

phoxal_api/
lib.rs

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