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::new()`.
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 declares its bus surface with a companion
34//! `#[derive(phoxal::Api)]` handle struct.
35//! Official handle fields name types through the complete train-selected facade.
36//! The derive records each field's resolved [`ContractBody::VERSION`] and
37//! [`ContractBody::CONTRACT`] in the participant's embedded metadata and does
38//! not declare a participant-wide API version.
39//! Across the graph, compatibility is **name identity** (D1) - two participants
40//! interoperate on a contract iff they use the exact same version-qualified name
41//! (`v0.1::drive::Target`), which is real on the wire because the revision
42//! is folded into the key ([`ContractBody::TOPIC`]). There is no `schema_id`:
43//! from 1.0 onward a stable contract type is immutable, so the name alone is
44//! the whole identity. Before 1.0, that identity is train-scoped and an
45//! in-place edit requires the whole robot graph to upgrade together.
46//!
47//! # Plain serde wire bodies, provenance in metadata
48//!
49//! A wire body is just its serde encoding - there is no `{"v":…}` envelope or any
50//! other version tag inside the payload (D62). Identity lives entirely in the
51//! Zenoh key (the version-qualified [`ContractBody::TOPIC`]); the bus metadata
52//! alongside the encoded body carries only provenance (source + logical time) and
53//! the codec that produced the bytes - never schema/family/version. Keeping
54//! identity out of both the payload and the metadata means the body bytes for an
55//! unchanged contract are identical across codecs, and a receiver's per-key
56//! subscription is the whole fast-reject.
57//!
58//! # Topic
59//!
60//! [`ContractBody::TOPIC`] is derived from the contract node's path in the tree,
61//! never written by hand: the version, then the `/`-joined node path plus the
62//! topic leaf, with each dynamic node contributing a `{var}` placeholder, e.g.
63//! `v0.1/component/{instance}/motor/{capability}/command`. A fully static path
64//! has a literal key (`v0.1/drive/state`). Folding the revision into the key
65//! (D1) is what makes two differently-versioned contracts physically distinct
66//! Zenoh keys - they cannot collide, so there is no `SCHEMA_ID`/`FAMILY` needed to
67//! disambiguate them.
68//!
69//! # The api-local topic builder
70//!
71//! Each version module exposes a `topic` builder that mirrors the node tree:
72//! `api::topic::new()` returns a root, one method per top-level node walks down the
73//! tree, a dynamic node's method takes its variable as `impl Display`, and a leaf
74//! method binds the topic's side-branded kind to its version-local body. For
75//! example `api::topic::new().drive().state()` yields a
76//! `Topic<Subscribe<drive::State>>` (the CLIENT observes the owner's `state`) over
77//! the version-qualified key `v0.1/drive/state`, and
78//! `api::topic::new().component("base").motor("left").command()` fills the dynamic
79//! segments to produce `v0.1/component/base/motor/left/command`. Because the
80//! builder is generated from the same tree as `TOPIC`, the built key and the
81//! documented key stay in lockstep.
82//!
83//! ## Owner side: `topic::internal`
84//!
85//! The PUBLIC `topic::new()...` chain above is the **client** side. The matching
86//! **owner** side lives at `api::topic::internal::new(cap)...` (L1 + L2, plan #00):
87//! the same node tree and keys, but the leaf brands flip so the owner gets the side
88//! it must take - `api::topic::internal::new(cap).drive().state()` is
89//! `Topic<Publish<drive::State>>` (the owner publishes its telemetry), and
90//! `api::topic::internal::new(cap).drive().target()` is `Topic<Subscribe<drive::Target>>`
91//! (the owner reads its command input). A query owner reaches its `ServeQuery`
92//! brand the same way. The `internal` chain is the deliberate, greppable owner
93//! opt-in; a participant acquires the topics of its OWN node through it and everything
94//! it consumes through the public chain.
95//!
96//! The `internal::new` entry requires the runner-minted owner capability
97//! ([`OwnerCap`](phoxal_bus::OwnerCap), Layer 2): a participant obtains it from
98//! `phoxal::SetupContext::owner_capability()` and passes it in. On the documented
99//! surface, owning a topic therefore cannot happen by accident - only with a
100//! capability the runner mints.
101
102use phoxal_macros::phoxal_api_tree;
103
104/// The contract primitive traits, re-exported from the `phoxal-bus` crate (the
105/// ABI floor) so they stay addressable at `phoxal_api::ApiVersion` /
106/// `phoxal_api::ContractBody`.
107///
108/// - [`ApiVersion`] is the marker trait identifying one API version (D60),
109/// implemented only by the zero-variant `enum Api {}` that [`phoxal_api_tree!`]
110/// generates inside each revision module; its `ID` is the concrete dotted
111/// wire identity (for example `"v0.1"`).
112/// - [`ContractBody`] is a version-local wire body (D61): a plain serde type
113/// bound to exactly one [`ApiVersion`] and one contract topic. Every body
114/// declared inside a [`phoxal_api_tree!`] node gets a generated impl; handles,
115/// `SetupContext` builders, and the `Service`/`Driver` derive assertions key
116/// off its `Api`/`TOPIC`. `TOPIC` is version-qualified (D1) and *is* the
117/// compatibility key - there is no `SCHEMA_ID`/`FAMILY`; its serde encoding
118/// *is* the wire payload, with no version envelope (D62).
119pub use phoxal_bus::{ApiVersion, ContractBody};
120
121phoxal_api_tree! {
122 version v0_1 {
123 drive {
124 /// Why actuation authority is in its current state.
125 enum StopReason {
126 /// Nothing is live: no target has been accepted, the producer
127 /// has gone silent past the host deadline, or the held command
128 /// exceeded its logical hold horizon. All three are the same
129 /// fact to a consumer - the drive is not being commanded.
130 TargetStale,
131 TargetNotFinite,
132 ActuatorCommandNotFinite,
133 Inactive,
134 EmergencyStop,
135 Fault,
136 }
137
138 /// Whether the drive is actively commanding the actuators.
139 enum ActuatorAuthority {
140 Active,
141 Stopped,
142 }
143
144 /// A requested or limited planar velocity.
145 struct Target {
146 linear_x_mps: f32,
147 angular_z_radps: f32,
148 curvature_limit_radpm: Option<f32>,
149 }
150
151 /// The drive participant's published control state.
152 struct State {
153 target: Target,
154 limited_target: Target,
155 actuator_authority: ActuatorAuthority,
156 stop_reason: Option<StopReason>,
157 }
158
159 topic target: command Target;
160 topic state: state State;
161 }
162
163 joint(joint) {
164 /// Per-joint position/velocity (and optional effort) on a dynamic
165 /// per-joint key.
166 struct JointState {
167 position_rad: f64,
168 velocity_radps: f64,
169 effort_nm: Option<f64>,
170 }
171
172 topic state: state JointState;
173 }
174
175 frame {
176 /// A parent → child rigid transform (translation + xyzw quaternion).
177 struct FrameTransform {
178 parent_frame_id: String,
179 child_frame_id: String,
180 translation_m: [f64; 3],
181 rotation_quat_xyzw: [f64; 4],
182 /// When this transform was observed. Absent for a static
183 /// transform, which is configuration rather than observation.
184 stamp: Option<::phoxal_bus::RobotInstant>,
185 }
186
187 /// Transforms that do not change over time.
188 struct StaticTransforms {
189 transforms: Vec<FrameTransform>,
190 }
191
192 /// The current transform tree.
193 struct Tree {
194 transforms: Vec<FrameTransform>,
195 }
196
197 /// Ask for the transform between two frames, optionally at a time.
198 struct LookupRequest {
199 target_frame_id: String,
200 source_frame_id: String,
201 /// The instant to resolve at. Absent asks for the latest.
202 at: Option<::phoxal_bus::RobotInstant>,
203 }
204
205 /// The resolved transform, or `None` if it is not available.
206 struct LookupResponse {
207 transform: Option<FrameTransform>,
208 }
209
210 topic tree: state Tree;
211 topic static_transforms: state StaticTransforms;
212 topic lookup: query LookupRequest => LookupResponse;
213 }
214
215 power {
216 /// A platform power command.
217 #[derive(Copy, Eq)]
218 enum Command {
219 Reboot,
220 Shutdown,
221 }
222
223 /// Where the power participant is in handling a command.
224 #[derive(Copy, Eq)]
225 enum Status {
226 Idle,
227 Rebooting,
228 ShuttingDown,
229 Failed,
230 }
231
232 /// Why a power command was rejected outright.
233 #[derive(Copy, Eq)]
234 #[serde(rename_all = "snake_case")]
235 enum RejectedReason {
236 HostIntegrationUnavailable,
237 CommandRejected,
238 }
239
240 /// Why an accepted power command later failed.
241 #[derive(Copy, Eq)]
242 #[serde(rename_all = "snake_case")]
243 enum FailedReason {
244 HostCommandFailed,
245 }
246
247 /// The power participant's published state.
248 struct State {
249 status: Status,
250 detail: Option<String>,
251 }
252
253 topic command: command Command;
254 topic state: state State;
255 }
256
257 motion {
258 struct Target {
259 linear_x_mps: f32,
260 angular_z_radps: f32,
261 curvature_limit_radpm: Option<f32>,
262 }
263
264 #[derive(Copy, Eq)]
265 #[serde(rename_all = "snake_case")]
266 enum Source {
267 Manual,
268 Navigation,
269 EmergencyStop,
270 }
271
272 #[derive(Copy, Eq)]
273 #[serde(rename_all = "snake_case")]
274 enum ZeroReason {
275 NoCandidate,
276 NavigationCandidateStale,
277 ManualCandidateNotFinite,
278 NavigationCandidateNotFinite,
279 EmergencyStopEngaged,
280 SafetyConstraintsUnavailable,
281 SafetyProtectiveStop,
282 }
283
284 #[derive(Copy, Eq)]
285 #[serde(rename_all = "snake_case")]
286 enum SafetyRuntime {
287 Absent,
288 Present,
289 }
290
291 struct ManualCommand {
292 linear_x_mps: f64,
293 angular_z_radps: f64,
294 }
295
296 struct State {
297 /// How long ago motion observed the live manual command, on
298 /// its own host clock. `None` when no manual command is live.
299 manual_observed_age_ns: Option<u64>,
300 autonomous_candidate_age_ns: Option<u64>,
301 safety_constraints_age_ns: Option<u64>,
302 selected_source: Option<Source>,
303 final_target: Target,
304 zero_reason: Option<ZeroReason>,
305 safety_runtime: SafetyRuntime,
306 component_estop_blocked: bool,
307 active_safety_constraints: Vec<super::safety::Constraint>,
308 }
309
310 topic manual: command ManualCommand;
311 topic state: state State;
312 }
313
314 safety {
315 /// Why safety is stopping or limiting body motion.
316 #[derive(Copy, Eq)]
317 #[serde(rename_all = "snake_case")]
318 enum ConstraintReason {
319 WorldUnavailable,
320 MapUnavailable,
321 DrivableSpaceUnavailable,
322 LocalizationUnavailable,
323 LocalizationUncertain,
324 ObstacleProximity,
325 RangeSensorFault,
326 DriveFault,
327 BatteryLow,
328 BatteryCritical,
329 SpeedZone,
330 OperatorPolicy,
331 }
332
333 /// Typed origin of one constraint, suitable for operator diagnosis.
334 #[derive(Copy, Eq)]
335 #[serde(rename_all = "snake_case")]
336 enum ConstraintSourceKind {
337 WorldModel,
338 Map,
339 Localization,
340 Range,
341 Drive,
342 Battery,
343 Operator,
344 }
345
346 struct ConstraintSource {
347 kind: ConstraintSourceKind,
348 participant_id: String,
349 component_id: Option<String>,
350 capability_id: Option<String>,
351 }
352
353 struct Constraint {
354 reason: ConstraintReason,
355 source: ConstraintSource,
356 stop: bool,
357 max_linear_speed_mps: Option<f32>,
358 max_angular_speed_radps: Option<f32>,
359 observed_value: Option<f32>,
360 /// The instant this constraint starts applying, on the
361 /// publisher's timeline. A consumer on another timeline gets a
362 /// checked error, never a silently wrong comparison.
363 valid_from: ::phoxal_bus::RobotInstant,
364 /// The instant this constraint stops applying.
365 expires_at: ::phoxal_bus::RobotInstant,
366 }
367
368 /// The sole safety-to-motion control product. Motion accepts it only
369 /// on the same timeline and before `expires_at`.
370 struct MotionConstraints {
371 sequence: u64,
372 stop: bool,
373 max_linear_speed_mps: Option<f32>,
374 max_angular_speed_radps: Option<f32>,
375 constraints: Vec<Constraint>,
376 expires_at: ::phoxal_bus::RobotInstant,
377 }
378
379 /// Operator-facing state mirrors the exact product consumed by motion.
380 struct State {
381 clear: bool,
382 motion: MotionConstraints,
383 }
384
385 topic constraints: state MotionConstraints;
386 topic state: state State;
387 }
388
389 navigation {
390 #[derive(Eq)]
391 struct RequestId {
392 value: String,
393 }
394
395 struct Pose {
396 x_m: f64,
397 y_m: f64,
398 yaw_rad: Option<f64>,
399 }
400
401 struct Path {
402 poses: Vec<Pose>,
403 map_revision: Option<u64>,
404 }
405
406 enum RequestKind {
407 GotoPose(Pose),
408 FollowPath(Path),
409 Cancel(RequestId),
410 }
411
412 struct Request {
413 request_id: RequestId,
414 kind: RequestKind,
415 }
416
417 enum State {
418 Idle,
419 Accepted(RequestId),
420 Running(RequestId),
421 }
422
423 #[derive(Copy, Eq)]
424 #[serde(rename_all = "snake_case")]
425 enum FailureReason {
426 LocalizationUnavailable,
427 MapUnavailable,
428 MapChanged,
429 NoPath,
430 Blocked,
431 Internal,
432 }
433
434 #[derive(Copy, Eq)]
435 #[serde(rename_all = "snake_case")]
436 enum RefusalReason {
437 Busy,
438 InvalidRequest,
439 Unsupported,
440 }
441
442 enum Outcome {
443 Succeeded,
444 Failed(FailureReason),
445 Refused(RefusalReason),
446 Cancelled,
447 TimedOut,
448 }
449
450 struct Progress {
451 request_id: RequestId,
452 distance_remaining_m: f64,
453 path_index: u32,
454 }
455
456 struct Result {
457 request_id: RequestId,
458 outcome: Outcome,
459 }
460
461 struct Candidate {
462 request_id: RequestId,
463 linear_x_mps: f32,
464 angular_z_radps: f32,
465 }
466
467 struct FrontierRequest {
468 map_revision: Option<u64>,
469 }
470
471 struct Frontier {
472 x_m: f64,
473 y_m: f64,
474 score: f32,
475 size: u32,
476 }
477
478 struct FrontierResponse {
479 frontier: Option<Frontier>,
480 map_revision: Option<u64>,
481 }
482
483 topic request: command Request;
484 topic state: state State;
485 topic progress: state Progress;
486 topic result: state Result;
487 topic candidate: state Candidate;
488 topic next_frontier: query FrontierRequest => FrontierResponse;
489 }
490
491 behavior {
492 #[derive(Eq)]
493 struct RequestId {
494 value: String,
495 }
496
497 #[derive(Copy, Eq)]
498 #[serde(rename_all = "snake_case")]
499 enum ConflictPolicy {
500 Reject,
501 Queue,
502 Interrupt,
503 }
504
505 enum Value {
506 Bool(bool),
507 Integer(i64),
508 Number(f64),
509 String(String),
510 Pose(super::navigation::Pose),
511 }
512
513 struct Request {
514 request_id: RequestId,
515 behavior_id: String,
516 args: ::std::collections::BTreeMap<String, Value>,
517 priority: u8,
518 conflict_policy: ConflictPolicy,
519 }
520
521 enum Command {
522 Pause,
523 Resume,
524 Cancel,
525 }
526
527 #[derive(Copy, Eq)]
528 #[serde(rename_all = "snake_case")]
529 enum ExecutionStatus {
530 Idle,
531 Running,
532 Paused,
533 Succeeded,
534 Failed,
535 Cancelled,
536 Abandoned,
537 }
538
539 #[derive(Copy, Eq)]
540 #[serde(rename_all = "snake_case")]
541 enum NodeStatus {
542 Idle,
543 Running,
544 Succeeded,
545 Failed,
546 Skipped,
547 Waiting,
548 Cancelling,
549 }
550
551 #[derive(Copy, Eq)]
552 #[serde(rename_all = "snake_case")]
553 enum FailureReason {
554 MissingCapability,
555 ActionRefused,
556 ActionFailed,
557 ActionTimedOut,
558 ActionCancelled,
559 ConditionFailed,
560 SafetyStopped,
561 EmergencyStopped,
562 ResourceConflict,
563 InvalidArgument,
564 InvalidBlackboardValue,
565 SubtreeFailed,
566 ExecutionAbandoned,
567 InternalError,
568 }
569
570 struct Failure {
571 reason: FailureReason,
572 detail: Option<String>,
573 node_path: Option<String>,
574 action_id: Option<String>,
575 }
576
577 struct DefinitionRef {
578 id: String,
579 version: String,
580 content_hash: String,
581 }
582
583 struct State {
584 execution_id: Option<String>,
585 root_behavior_id: Option<String>,
586 active_request_id: Option<RequestId>,
587 active_behavior_id: Option<String>,
588 status: ExecutionStatus,
589 active_node_path: Option<String>,
590 failure: Option<Failure>,
591 }
592
593 struct Snapshot {
594 execution_id: Option<String>,
595 root: Option<DefinitionRef>,
596 definition_stack: Vec<DefinitionRef>,
597 active_request_id: Option<RequestId>,
598 active_behavior_id: Option<String>,
599 status: ExecutionStatus,
600 node_statuses: ::std::collections::BTreeMap<String, NodeStatus>,
601 active_node_path: Option<String>,
602 blackboard: ::std::collections::BTreeMap<String, Value>,
603 args: ::std::collections::BTreeMap<String, Value>,
604 /// When the active execution started, on the publisher's
605 /// timeline. The envelope carries when this snapshot was
606 /// produced; that instant is never duplicated here.
607 started_at: Option<::phoxal_bus::RobotInstant>,
608 failure: Option<Failure>,
609 }
610
611 enum EventKind {
612 ExecutionStarted,
613 ExecutionPaused,
614 ExecutionResumed,
615 ExecutionCompleted,
616 ExecutionCancelled,
617 ExecutionAbandoned,
618 NodeTransition(NodeStatus),
619 RequestAccepted,
620 RequestCompleted(ExecutionStatus),
621 RequestRejected(FailureReason),
622 }
623
624 struct Event {
625 sequence: u64,
626 execution_id: Option<String>,
627 request_id: Option<RequestId>,
628 behavior_id: Option<String>,
629 content_hash: Option<String>,
630 node_path: Option<String>,
631 kind: EventKind,
632 failure: Option<Failure>,
633 participant_id: String,
634 }
635
636 topic command: command Command;
637 topic request: command Request;
638 topic state: state State;
639 topic snapshot: state Snapshot;
640 topic event: state Event;
641 }
642
643 logs(participant_id) {
644 /// Wall-clock timestamp carried by a structured bus log event.
645 struct Timestamp {
646 unix_seconds: i64,
647 nanos: u32,
648 }
649
650 /// The severity level of a structured bus log event.
651 #[derive(Copy, Eq)]
652 #[serde(rename_all = "snake_case")]
653 enum Level {
654 Error,
655 Warn,
656 Info,
657 Debug,
658 Trace,
659 }
660
661 /// A scalar tracing field value captured from a log event.
662 #[serde(untagged)]
663 enum LogValue {
664 Bool(bool),
665 I64(i64),
666 U64(u64),
667 F64(f64),
668 String(String),
669 }
670
671 /// One structured runner log event published out-of-band.
672 struct Event {
673 seq: u64,
674 time: Timestamp,
675 level: Level,
676 target: String,
677 message: String,
678 fields: ::std::collections::BTreeMap<String, LogValue>,
679 /// Complete records lost before publication because a bounded
680 /// queue or publish attempt was saturated.
681 dropped: u32,
682 /// Values or fields truncated inside this published record to
683 /// keep its wire representation bounded.
684 #[serde(default)]
685 truncated: u32,
686 }
687
688 topic self: diagnostic Event;
689 }
690
691 tool {
692 /// Opaque identity for one retention-tool process together with a
693 /// position in its completed follow stream. A snapshot cursor
694 /// covers the retained completed items; a bus snapshot's optional
695 /// `current` window deliberately has the next sequence. Consumers
696 /// compare `generation` for equality only and must never parse or
697 /// order it.
698 #[derive(Eq)]
699 struct Cursor {
700 generation: String,
701 sequence: u64,
702 }
703
704 /// Which side of one participant-local bus buffer a runtime row
705 /// measures. The version-qualified `topic` field remains the wire
706 /// identity; direction is never inferred from its spelling.
707 #[derive(Copy, Eq, Ord, PartialOrd)]
708 #[serde(rename_all = "snake_case")]
709 enum RuntimeDirection {
710 Publish,
711 Subscribe,
712 /// Used only by the bounded overflow row, which may combine
713 /// omitted rows from both directions.
714 Mixed,
715 }
716
717 /// The concrete bounded buffer whose pressure a runtime row
718 /// measures.
719 #[derive(Copy, Eq, Ord, PartialOrd)]
720 #[serde(rename_all = "snake_case")]
721 enum RuntimeBufferKind {
722 /// Per-topic view of the one shared process outbound queue.
723 /// Its sample capacity is repeated on each row and must not be
724 /// summed; the queue's separate byte pressure is not in v0.1.
725 Outbound,
726 /// Keep-last slot. Depth `1` means occupied, never backlog.
727 Latest,
728 Subscriber,
729 /// Used only by the bounded overflow row.
730 Mixed,
731 }
732
733 /// Host-monotonic scheduled-step work completed during one rollup
734 /// window. An unscheduled participant reports `None` instead.
735 struct RuntimeStep {
736 target_period_ns: u64,
737 completed: u64,
738 errors: u64,
739 mean_duration_ns: u64,
740 max_duration_ns: u64,
741 mean_lateness_ns: u64,
742 max_lateness_ns: u64,
743 missed_ticks: u64,
744 overruns: u64,
745 }
746
747 /// One exact version-qualified topic/direction/buffer row. These
748 /// are process-lifetime setup declarations: dropping an authoring
749 /// handle does not dynamically unregister a row. Empty `topic`
750 /// plus `Mixed` direction/kind identifies the explicit overflow
751 /// row; `overflowed_rows` is zero on normal rows.
752 struct RuntimeTopic {
753 topic: String,
754 direction: RuntimeDirection,
755 buffer_kind: RuntimeBufferKind,
756 count: u64,
757 /// Finite, non-negative message rate. Retention tools clamp
758 /// malformed non-finite inputs before they reach snapshots.
759 rate_hz: f32,
760 drops: u64,
761 latest_overwrites: u64,
762 bounded_evictions: u64,
763 /// Sample capacity. Outbound rows repeat the shared process
764 /// queue capacity and are non-additive; byte pressure is not
765 /// represented. Latest capacity/depth describe slot occupancy.
766 capacity: u64,
767 current_depth: u64,
768 high_water_depth: u64,
769 decode_errors: u64,
770 /// Samples discarded because they belonged to a retired world
771 /// history, or because a quarantined candidate timeline was
772 /// purged when a different one became authoritative.
773 timeline_filtered: u64,
774 overflowed_rows: u32,
775 }
776
777 log {
778 /// Requests tool-log's complete current bounded snapshot. The
779 /// first protocol version intentionally has no pagination or
780 /// filtering surface.
781 struct SnapshotRequest {}
782
783 /// Wall-clock timestamp copied from one participant-originated
784 /// structured `v0.1::logs` event.
785 struct Timestamp {
786 unix_seconds: i64,
787 nanos: u32,
788 }
789
790 #[derive(Copy, Eq)]
791 #[serde(rename_all = "snake_case")]
792 enum Level {
793 Error,
794 Warn,
795 Info,
796 Debug,
797 Trace,
798 }
799
800 #[serde(untagged)]
801 enum LogValue {
802 Bool(bool),
803 I64(i64),
804 U64(u64),
805 F64(f64),
806 String(String),
807 }
808
809 /// One retained participant log. `sequence` is assigned by
810 /// tool-log at ingest and is independent of the producer's
811 /// `source_sequence`.
812 struct Record {
813 sequence: u64,
814 participant_id: String,
815 source_sequence: u64,
816 time: Timestamp,
817 level: Level,
818 target: String,
819 message: String,
820 fields: ::std::collections::BTreeMap<String, LogValue>,
821 dropped: u32,
822 truncated: u32,
823 }
824
825 /// The complete bounded tool-log state at `cursor`.
826 struct Snapshot {
827 cursor: crate::v0_1::tool::Cursor,
828 /// Cumulative structured log samples evicted from
829 /// tool-log's bounded ingest subscriber in this process.
830 /// An increase is observable, unrecoverable source loss;
831 /// it is distinct from producer-side `Record::dropped`.
832 ingest_dropped: u64,
833 records: Vec<Record>,
834 }
835
836 /// One live record following the snapshot query. A consumer
837 /// must re-query when the generation changes or the sequence is
838 /// not exactly one after its installed cursor.
839 struct Follow {
840 cursor: crate::v0_1::tool::Cursor,
841 /// Current cumulative tool-log ingest loss counter.
842 ingest_dropped: u64,
843 record: Record,
844 }
845
846 topic snapshot: query SnapshotRequest => Snapshot;
847 topic follow: diagnostic Follow;
848 }
849
850 bus {
851 /// Requests tool-bus's complete current bounded snapshot. The
852 /// first protocol version intentionally has no pagination or
853 /// filtering surface.
854 struct SnapshotRequest {}
855
856 /// One topic-producer pair measured during a one-second window.
857 /// Empty topic and producer identify the bounded overflow row.
858 struct TopicMetric {
859 topic: String,
860 from_participant: String,
861 ingress_rate_hz: f32,
862 count: u64,
863 }
864
865 /// One retained bus-rate window. `window_ns` is a duration, not
866 /// a production timestamp.
867 struct Window {
868 sequence: u64,
869 topics: Vec<TopicMetric>,
870 throughput_msg_s: f32,
871 window_ns: u64,
872 }
873
874 /// The current partial counters plus the bounded recent
875 /// completed-window history. The partial `current` window has
876 /// the next sequence that its eventual follow item will use;
877 /// the snapshot cursor covers only `windows`.
878 struct Snapshot {
879 cursor: crate::v0_1::tool::Cursor,
880 current: Option<Window>,
881 windows: Vec<Window>,
882 }
883
884 /// One newly completed bus-rate window.
885 struct Follow {
886 cursor: crate::v0_1::tool::Cursor,
887 window: Window,
888 }
889
890 topic snapshot: query SnapshotRequest => Snapshot;
891 topic follow: diagnostic Follow;
892 }
893
894 device {
895 /// One mounted filesystem capacity observation. The contract
896 /// intentionally excludes OS-specific device names and I/O
897 /// counters; an unavailable storage inventory is represented
898 /// by `Sample::disks == None`, never fabricated zero rows.
899 struct Disk {
900 mount_point: String,
901 file_system: String,
902 used_bytes: u64,
903 total_bytes: u64,
904 }
905
906 /// Portable whole-device observations produced by one
907 /// runner-owned tool-device. Every optional field is `None`
908 /// when the current platform cannot provide a truthful value.
909 /// These totals must never be attributed to a runtime.
910 struct Sample {
911 cpu_pct: Option<f32>,
912 ram_used_bytes: Option<u64>,
913 ram_total_bytes: Option<u64>,
914 swap_used_bytes: Option<u64>,
915 swap_total_bytes: Option<u64>,
916 load_1m: Option<f32>,
917 load_5m: Option<f32>,
918 load_15m: Option<f32>,
919 uptime_s: Option<u64>,
920 disks: Option<Vec<Disk>>,
921 /// Host-monotonic duration between sampler refreshes.
922 window_ns: u64,
923 }
924
925 /// Requests tool-telemetry's bounded device history.
926 struct SnapshotRequest {
927 /// Maximum newest records to return. Zero selects the
928 /// tool's bounded default.
929 limit: u32,
930 /// Exclusive global ingest-sequence upper bound.
931 before_sequence: Option<u64>,
932 }
933
934 struct Record {
935 sequence: u64,
936 sample: Sample,
937 /// Text fields or rows truncated by tool-telemetry.
938 truncated: u32,
939 }
940
941 struct Snapshot {
942 cursor: crate::v0_1::tool::Cursor,
943 records: Vec<Record>,
944 /// Records evicted by the absolute capacity bound before
945 /// their five-minute age horizon elapsed.
946 capacity_evictions: u64,
947 next_before_sequence: Option<u64>,
948 }
949
950 struct Follow {
951 cursor: crate::v0_1::tool::Cursor,
952 record: Record,
953 }
954
955 topic sample: diagnostic Sample;
956 topic snapshot: query SnapshotRequest => Snapshot;
957 topic follow: diagnostic Follow;
958 }
959
960 runtime {
961 /// Runner-originated portable performance rollup. The runner
962 /// publishes at most one per host-monotonic grid interval;
963 /// envelope provenance identifies the participant. Interval
964 /// counters are best-effort sequential atomic samples, not a
965 /// transactional stop-the-world boundary, so concurrent queue
966 /// activity may land on either neighboring rollup.
967 struct Rollup {
968 window_ns: u64,
969 step: Option<crate::v0_1::tool::RuntimeStep>,
970 topics: Vec<crate::v0_1::tool::RuntimeTopic>,
971 overflow: Option<crate::v0_1::tool::RuntimeTopic>,
972 }
973
974 /// Requests tool-telemetry's current bounded five-minute
975 /// runtime history.
976 struct SnapshotRequest {
977 /// Optional exact participant filter. `None` selects the
978 /// complete per-robot history.
979 participant_id: Option<String>,
980 /// Maximum newest records to return. Zero selects the
981 /// tool's bounded default.
982 limit: u32,
983 /// Exclusive global ingest-sequence upper bound for
984 /// backward pagination. `None` starts at the newest record.
985 before_sequence: Option<u64>,
986 }
987
988 /// One retained rollup. `sequence` is assigned by
989 /// tool-telemetry at ingest, independent of producer metadata.
990 /// Duplicate normal topic keys are deterministically
991 /// re-aggregated before row bounds are applied.
992 struct Record {
993 sequence: u64,
994 participant_id: String,
995 /// Text values truncated by tool-telemetry's ingest bound.
996 /// Oversized/excess topic identities are not truncated;
997 /// they are aggregated into the explicit overflow row.
998 truncated: u32,
999 window_ns: u64,
1000 step: Option<crate::v0_1::tool::RuntimeStep>,
1001 topics: Vec<crate::v0_1::tool::RuntimeTopic>,
1002 overflow: Option<crate::v0_1::tool::RuntimeTopic>,
1003 }
1004
1005 struct Snapshot {
1006 cursor: crate::v0_1::tool::Cursor,
1007 records: Vec<Record>,
1008 /// Records evicted by the absolute memory cap before their
1009 /// five-minute age horizon elapsed.
1010 capacity_evictions: u64,
1011 /// Pass this as the next request's `before_sequence` to
1012 /// continue backward. `None` means the retained matching
1013 /// history is complete.
1014 next_before_sequence: Option<u64>,
1015 }
1016
1017 struct Follow {
1018 cursor: crate::v0_1::tool::Cursor,
1019 record: Record,
1020 }
1021
1022 topic rollup: diagnostic Rollup;
1023 topic snapshot: query SnapshotRequest => Snapshot;
1024 topic follow: diagnostic Follow;
1025 }
1026 }
1027
1028 bus {
1029 uplink {
1030 /// Reserved observable state for a future authenticated router
1031 /// uplink. Phase 1 has no publisher for this contract.
1032 #[derive(Copy, Eq)]
1033 #[serde(rename_all = "snake_case")]
1034 enum UplinkPhase {
1035 Disabled,
1036 Connecting,
1037 Connected,
1038 /// Reserved for future retry telemetry.
1039 Retrying,
1040 }
1041
1042 /// Reserved router-owned state for a future optional site uplink.
1043 struct State {
1044 phase: UplinkPhase,
1045 connect: Option<String>,
1046 /// Reserved for future retry telemetry.
1047 retry_attempt: u32,
1048 /// Optional human-readable diagnostic while not connected,
1049 /// such as DNS failure or waiting for the configured remote
1050 /// link. Invalid endpoints fail configuration before startup.
1051 detail: Option<String>,
1052 }
1053
1054 topic state: diagnostic State;
1055 }
1056 }
1057
1058
1059
1060
1061 perception {
1062 /// A single detected object: class, confidence, and pose in a frame.
1063 struct Detection {
1064 class_id: String,
1065 confidence: f32,
1066 position_m: [f64; 3],
1067 frame_id: String,
1068 track_id: Option<u64>,
1069 }
1070
1071 /// A batch of detections from one perception cycle.
1072 struct Detections {
1073 detections: Vec<Detection>,
1074 /// The frame instant these detections were derived from.
1075 stamp: Option<::phoxal_bus::RobotInstant>,
1076 }
1077
1078 /// The perception participant's published health.
1079 struct State {
1080 healthy: bool,
1081 detector: String,
1082 }
1083
1084 topic detections: state Detections;
1085 topic state: state State;
1086 }
1087
1088 video {
1089 /// Ask to open a video stream for a capability at an optional size.
1090 struct OpenRequest {
1091 capability: String,
1092 width_px: Option<u32>,
1093 height_px: Option<u32>,
1094 }
1095
1096 /// The id of the stream that was opened.
1097 struct OpenResponse {
1098 stream_id: String,
1099 }
1100
1101 topic open: query OpenRequest => OpenResponse;
1102
1103 stream(stream) {
1104 /// Where one open video stream is in its lifecycle.
1105 #[derive(Copy, Eq)]
1106 #[serde(rename_all = "snake_case")]
1107 enum StreamPhase {
1108 Starting,
1109 Active,
1110 Stopped,
1111 }
1112
1113 /// The published state of one video stream: its lifecycle phase
1114 /// and the number of source frames seen so far. The video participant
1115 /// publishes it per stream; clients subscribe, hence `state`.
1116 struct StreamState {
1117 phase: StreamPhase,
1118 frames_seen: u64,
1119 }
1120
1121 topic state: state StreamState;
1122 }
1123 }
1124
1125 simulation {
1126 /// The authoritative advancing simulation clock. Publication means
1127 /// the world advanced; silence means it did not.
1128 ///
1129 /// The timeline and instant ride in the envelope, like every other
1130 /// `state` publication - the world authority stamps them with a
1131 /// world step token. The body carries only the step counter, which
1132 /// is not derivable from the envelope.
1133 struct Clock {
1134 step: u64,
1135 }
1136
1137 topic clock: state Clock;
1138 }
1139
1140 // Per-instance component capabilities (D17/D38: framework participant / driver
1141 // territory). `component(instance)` selects a manifest-declared component;
1142 // each child `kind(capability)` is a self-contained node whose key is
1143 // `component/{instance}/<kind>/{capability}/<leaf>`. Nodes duplicate any
1144 // types they share by design - the node path disambiguates, so the names
1145 // are path-local.
1146 component(instance) {
1147 motor(capability) {
1148 /// A per-actuator command.
1149 enum Command {
1150 Velocity(f32),
1151 Torque(f32),
1152 Stop,
1153 }
1154
1155 topic command: command Command;
1156 }
1157
1158 encoder(capability) {
1159 /// Per-encoder sample on a dynamic per-instance key.
1160 struct Sample {
1161 position_rad: f64,
1162 velocity_radps: f32,
1163 }
1164
1165 topic sample: measurement Sample;
1166 }
1167
1168 accelerometer(capability) {
1169 /// Raw accelerometer sample in the sensor-local frame in m/s^2.
1170 struct Sample {
1171 linear_acceleration: [f32; 3],
1172 }
1173
1174 topic sample: measurement Sample;
1175 }
1176
1177 gyroscope(capability) {
1178 /// Raw angular velocity sample in the sensor-local frame in rad/s.
1179 struct Sample {
1180 angular_velocity: [f32; 3],
1181 }
1182
1183 topic sample: measurement Sample;
1184 }
1185
1186 magnetometer(capability) {
1187 /// Raw magnetic-field sample in the sensor-local frame.
1188 struct Sample {
1189 magnetic_field: [f32; 3],
1190 }
1191
1192 topic sample: measurement Sample;
1193 }
1194
1195 imu(capability) {
1196 #[derive(Copy, Eq)]
1197 #[serde(rename_all = "snake_case")]
1198 enum SensorHealth {
1199 Nominal,
1200 Degraded,
1201 Fault,
1202 }
1203
1204 #[derive(Copy)]
1205 struct Bias {
1206 angular_velocity_radps: [f32; 3],
1207 linear_acceleration_mps2: [f32; 3],
1208 }
1209
1210 struct Sample {
1211 orientation: Option<[f32; 4]>,
1212 angular_velocity_radps: [f32; 3],
1213 linear_acceleration_mps2: [f32; 3],
1214 covariance: Option<[f32; 9]>,
1215 noise_density: Option<[f32; 3]>,
1216 sensor_frame_id: Option<String>,
1217 health: SensorHealth,
1218 bias: Option<Bias>,
1219 }
1220
1221 topic sample: measurement Sample;
1222 }
1223
1224 range(capability) {
1225 #[derive(Copy, Eq)]
1226 #[serde(rename_all = "snake_case")]
1227 enum SensorHealth {
1228 Nominal,
1229 Degraded,
1230 Fault,
1231 }
1232
1233 #[derive(Copy)]
1234 struct Limits {
1235 min_m: f32,
1236 max_m: f32,
1237 }
1238
1239 #[derive(Copy)]
1240 struct SampleQuality {
1241 valid: bool,
1242 confidence: Option<f32>,
1243 }
1244
1245 struct Sample {
1246 distance_m: f32,
1247 limits: Option<Limits>,
1248 quality: Option<SampleQuality>,
1249 health: SensorHealth,
1250 }
1251
1252 topic sample: measurement Sample;
1253 }
1254
1255 gnss(capability) {
1256 /// A GNSS fix: geodetic position plus a 3x3 position covariance.
1257 struct Sample {
1258 latitude: f64,
1259 longitude: f64,
1260 altitude: f64,
1261 position_covariance: [f64; 9],
1262 }
1263
1264 topic sample: measurement Sample;
1265 }
1266
1267 camera(capability) {
1268 #[derive(Copy, Eq)]
1269 #[serde(rename_all = "snake_case")]
1270 enum Encoding {
1271 Jpeg,
1272 Png,
1273 L8,
1274 Rgb8,
1275 Rgba8,
1276 }
1277
1278 #[derive(Copy)]
1279 struct Intrinsics {
1280 fx: f32,
1281 fy: f32,
1282 cx: f32,
1283 cy: f32,
1284 }
1285
1286 struct Distortion {
1287 model: String,
1288 coefficients: Vec<f32>,
1289 }
1290
1291 #[derive(Copy)]
1292 struct ExposureTiming {
1293 exposure_start_ns: Option<u64>,
1294 exposure_duration_ns: Option<u64>,
1295 }
1296
1297 struct CalibrationIdentity {
1298 id: String,
1299 version: String,
1300 }
1301
1302 /// One camera frame: encoded pixel bytes plus optional calibration
1303 /// and timing metadata.
1304 struct Frame {
1305 width: u32,
1306 height: u32,
1307 encoding: Encoding,
1308 intrinsics: Option<Intrinsics>,
1309 distortion: Option<Distortion>,
1310 exposure: Option<ExposureTiming>,
1311 calibration: Option<CalibrationIdentity>,
1312 #[serde(with = "serde_bytes")]
1313 data: Vec<u8>,
1314 }
1315
1316 topic frame: measurement Frame;
1317 }
1318
1319 depth(capability) {
1320 #[derive(Copy, Eq)]
1321 #[serde(rename_all = "snake_case")]
1322 enum Encoding {
1323 U16Millimeters,
1324 }
1325
1326 #[derive(Copy, Eq)]
1327 #[serde(rename_all = "snake_case")]
1328 enum InvalidSamplePolicy {
1329 ZeroIsInvalid,
1330 NonFiniteIsInvalid,
1331 }
1332
1333 #[derive(Copy)]
1334 struct Intrinsics {
1335 fx: f32,
1336 fy: f32,
1337 cx: f32,
1338 cy: f32,
1339 }
1340
1341 struct Distortion {
1342 model: String,
1343 coefficients: Vec<f32>,
1344 }
1345
1346 #[derive(Copy)]
1347 struct ExposureTiming {
1348 exposure_start_ns: Option<u64>,
1349 exposure_duration_ns: Option<u64>,
1350 }
1351
1352 struct CalibrationIdentity {
1353 id: String,
1354 version: String,
1355 }
1356
1357 /// One depth frame: per-pixel millimetre samples plus optional
1358 /// calibration and timing metadata.
1359 struct Frame {
1360 samples_mm: Vec<u16>,
1361 encoding: Encoding,
1362 invalid_sample_policy: InvalidSamplePolicy,
1363 width: Option<u32>,
1364 height: Option<u32>,
1365 intrinsics: Option<Intrinsics>,
1366 distortion: Option<Distortion>,
1367 exposure: Option<ExposureTiming>,
1368 calibration: Option<CalibrationIdentity>,
1369 }
1370
1371 topic frame: measurement Frame;
1372 }
1373
1374 lidar(capability) {
1375 #[derive(Copy, Eq)]
1376 #[serde(rename_all = "snake_case")]
1377 enum SensorHealth {
1378 Nominal,
1379 Degraded,
1380 Fault,
1381 }
1382
1383 #[derive(Copy)]
1384 struct ScanGeometry {
1385 angle_min_rad: f32,
1386 angle_increment_rad: f32,
1387 }
1388
1389 #[derive(Copy)]
1390 struct RangeLimits {
1391 min_m: f32,
1392 max_m: f32,
1393 }
1394
1395 #[derive(Copy)]
1396 struct ScanQuality {
1397 valid_points: u32,
1398 }
1399
1400 struct Ranges {
1401 ranges: Vec<f32>,
1402 geometry: Option<ScanGeometry>,
1403 limits: Option<RangeLimits>,
1404 quality: Option<ScanQuality>,
1405 health: SensorHealth,
1406 }
1407
1408 struct Points {
1409 points: Vec<[f32; 3]>,
1410 limits: Option<RangeLimits>,
1411 quality: Option<ScanQuality>,
1412 health: SensorHealth,
1413 }
1414
1415 /// One lidar scan, either as polar ranges or as cartesian points.
1416 #[serde(tag = "kind", rename_all = "snake_case")]
1417 enum Scan {
1418 Ranges(Ranges),
1419 Points(Points),
1420 }
1421
1422 topic scan: measurement Scan;
1423 }
1424
1425 mmwave(capability) {
1426 /// One mmWave radar detection: position, velocity, and SNR.
1427 #[derive(Copy)]
1428 struct Detection {
1429 position: [f32; 3],
1430 velocity: [f32; 3],
1431 snr: f32,
1432 }
1433
1434 /// One mmWave radar scan as a set of detections.
1435 struct Scan {
1436 detections: Vec<Detection>,
1437 }
1438
1439 topic scan: measurement Scan;
1440 }
1441
1442 microphone(capability) {
1443 /// One audio frame as raw encoded bytes.
1444 struct Frame {
1445 data: Vec<u8>,
1446 }
1447
1448 topic frame: measurement Frame;
1449 }
1450
1451 led(capability) {
1452 /// A per-LED on/off command.
1453 #[derive(Copy, Eq)]
1454 enum Command {
1455 On,
1456 Off,
1457 }
1458
1459 topic command: command Command;
1460 }
1461
1462 emergency_stop(capability) {
1463 /// Per-instance emergency-stop state.
1464 #[derive(Eq)]
1465 struct State {
1466 engaged: bool,
1467 }
1468
1469 topic state: state State;
1470 }
1471 }
1472
1473 odometry {
1474 /// A planar pose + twist estimate in the odometry frame.
1475 struct State {
1476 x_m: f64,
1477 y_m: f64,
1478 yaw_rad: f64,
1479 linear_x_mps: f32,
1480 angular_z_radps: f32,
1481 }
1482
1483 topic state: state State;
1484 }
1485
1486 localize {
1487 /// A planar localization estimate in the map frame.
1488 struct LocalizationState {
1489 x_m: f64,
1490 y_m: f64,
1491 yaw_rad: f64,
1492 confidence: f32,
1493 }
1494
1495 topic state: state LocalizationState;
1496 }
1497
1498 map {
1499 /// A published map revision marker.
1500 struct Revision {
1501 revision: u64,
1502 resolution_m: f32,
1503 }
1504
1505 /// Request a rectangular submap window (map-frame metres).
1506 struct SubmapRequest {
1507 min_x_m: f64,
1508 min_y_m: f64,
1509 max_x_m: f64,
1510 max_y_m: f64,
1511 }
1512
1513 /// An occupancy-grid window: row-major cells, 0..=100 + 255 = unknown.
1514 struct SubmapResponse {
1515 width: u32,
1516 height: u32,
1517 resolution_m: f32,
1518 cells: Vec<u8>,
1519 }
1520
1521 topic revision: state Revision;
1522 topic submap: query SubmapRequest => SubmapResponse;
1523 }
1524
1525 asset {
1526 /// Fetch a stored asset by path.
1527 struct GetRequest {
1528 path: String,
1529 }
1530
1531 /// The asset bytes, a not-found marker, or a rejected path.
1532 enum GetResponse {
1533 Found { bytes: Vec<u8> },
1534 Missing,
1535 InvalidPath,
1536 }
1537
1538 topic get: query GetRequest => GetResponse;
1539 }
1540
1541 battery {
1542 /// Battery state reported by the active simulator or hardware owner.
1543 struct State {
1544 voltage_v: f32,
1545 current_a: f32,
1546 charge_ratio: f32,
1547 }
1548
1549 topic state: state State;
1550 }
1551
1552 joypad {
1553 /// Whether an observed controller is ready for the fixed manual
1554 /// input preset, disconnected, or connected without a compatible
1555 /// control mapping.
1556 enum DeviceStatus {
1557 Ready,
1558 Disconnected,
1559 Unsupported,
1560 }
1561
1562 /// One gamepad the tool can see. `id` is a STABLE wire id the tool
1563 /// assigns (name/guid-derived) - NOT a process-local gilrs id.
1564 struct Device {
1565 id: String,
1566 name: String,
1567 status: DeviceStatus,
1568 }
1569
1570 /// The joypad tool's published device state.
1571 struct Devices {
1572 available: Vec<Device>,
1573 selected: Option<String>,
1574 enabled: bool,
1575 /// Structural reason manual input cannot be enabled in this
1576 /// session (for example robot-model or backend limitations),
1577 /// independent of transient device/request errors.
1578 unavailable_reason: Option<String>,
1579 /// One-shot acknowledgement of a failed select/enable/rescan
1580 /// request. Event-driven consumers may show it once; periodic
1581 /// state heartbeats omit it. The tool also writes the failure
1582 /// to its log stream for durable diagnostics.
1583 last_error: Option<String>,
1584 }
1585
1586 /// Client asks the tool to select a device by its stable id.
1587 struct Select {
1588 id: String,
1589 }
1590
1591 /// Client asks the tool to enable or disable manual input.
1592 struct SetEnabled {
1593 enabled: bool,
1594 }
1595
1596 /// Client asks the tool to re-enumerate devices.
1597 struct Rescan {}
1598
1599 topic devices: diagnostic Devices;
1600 topic select: command Select;
1601 topic set_enabled: command SetEnabled;
1602 topic rescan: command Rescan;
1603 }
1604 }
1605 latest v0_1;
1606}
1607
1608#[cfg(test)]
1609mod tests;