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;