Skip to main content

phoxal_api/
lib.rs

1//! The single API layer (D60/D61/D1).
2//!
3//! This crate is the versioned API contract tree. It depends only on the
4//! [`phoxal-bus`](phoxal_bus) ABI floor (the contract primitive traits and the
5//! typed-topic builders) and the [`phoxal-macros`](phoxal_macros) proc-macros; it
6//! does **not** depend on the `phoxal` engine. A participant depends on both crates
7//! and imports the API tree directly with `use phoxal_api::v1 as api;`, so
8//! `phoxal_api::v1`, `phoxal_api::ApiVersion`, and `phoxal_api::ContractBody`
9//! are the canonical paths.
10//!
11//! # Stable and preview API versions
12//!
13//! An API version is a conventional `vN` module generated by
14//! [`phoxal_api_tree!`]. Each version module carries:
15//!
16//! - a zero-variant marker `enum Api {}` implementing [`ApiVersion`], whose
17//!   [`ApiVersion::ID`] is the module name (`"v1"` or `"v2"`);
18//! - the version-local wire bodies, one `pub mod` per contract node holding plain
19//!   serde structs/enums and their [`ContractBody`] impls;
20//! - an api-local `topic` builder rooted at `topic::new()`.
21//!
22//! `v1` is the current production surface and may change in place while the
23//! pre-stability topology is simplified. `v2` is the single preview surface for
24//! contracts that have not entered production. Preview work does not mint a new
25//! public version for each change.
26//! `v2` is authored as `preview version v2 { … }` and is available when the
27//! `preview-v2` Cargo feature is enabled. [`ApiVersion::IS_PREVIEW`] records the
28//! lifecycle without changing topics or wire bytes.
29//!
30//! [`Api`]: v1::Api
31//!
32//! # Per-field versions and per-contract identity
33//!
34//! A participant declares its bus surface with a companion
35//! `#[derive(phoxal::Api)]` handle struct.
36//! Each handle field names its own version-qualified contract type, so one
37//! participant may freely mix fields from modules such as `v1` and
38//! `v2`.
39//! The derive records each field's resolved [`ContractBody::VERSION`] and
40//! [`ContractBody::CONTRACT`] in the participant's embedded metadata and does
41//! not declare a participant-wide API version.
42//! Across the graph, compatibility is **name identity** (D1) - two participants
43//! interoperate on a contract iff they use the exact same version-qualified name
44//! (`v1::drive::Target`), which is real on the wire because the version
45//! is folded into the key ([`ContractBody::TOPIC`]). There is no `schema_id`: a
46//! stable contract type is immutable, so the name alone is the whole identity.
47//!
48//! # Plain serde wire bodies, provenance in metadata
49//!
50//! A wire body is just its serde encoding - there is no `{"v":…}` envelope or any
51//! other version tag inside the payload (D62). Identity lives entirely in the
52//! Zenoh key (the version-qualified [`ContractBody::TOPIC`]); the bus metadata
53//! alongside the encoded body carries only provenance (source + logical time) and
54//! the codec that produced the bytes - never schema/family/version. Keeping
55//! identity out of both the payload and the metadata means the body bytes for an
56//! unchanged contract are identical across codecs, and a receiver's per-key
57//! subscription is the whole fast-reject.
58//!
59//! # Topic
60//!
61//! [`ContractBody::TOPIC`] is derived from the contract node's path in the tree,
62//! never written by hand: the version, then the `/`-joined node path plus the
63//! topic leaf, with each dynamic node contributing a `{var}` placeholder, e.g.
64//! `v1/component/{instance}/motor/{capability}/command`. A fully static path
65//! has a literal key (`v1/drive/state`). Folding the version into the key
66//! (D1) is what makes two differently-versioned contracts physically distinct
67//! Zenoh keys - they cannot collide, so there is no `SCHEMA_ID`/`FAMILY` needed to
68//! disambiguate them.
69//!
70//! # The api-local topic builder
71//!
72//! Each version module exposes a `topic` builder that mirrors the node tree:
73//! `api::topic::new()` returns a root, one method per top-level node walks down the
74//! tree, a dynamic node's method takes its variable as `impl Display`, and a leaf
75//! method binds the topic's side-branded kind to its version-local body. For
76//! example `api::topic::new().drive().state()` yields a
77//! `Topic<Subscribe<drive::State>>` (the CLIENT observes the owner's `state`) over
78//! the version-qualified key `v1/drive/state`, and
79//! `api::topic::new().component("base").motor("left").command()` fills the dynamic
80//! segments to produce `v1/component/base/motor/left/command`. Because the
81//! builder is generated from the same tree as `TOPIC`, the built key and the
82//! documented key stay in lockstep.
83//!
84//! ## Owner side: `topic::internal`
85//!
86//! The PUBLIC `topic::new()...` chain above is the **client** side. The matching
87//! **owner** side lives at `api::topic::internal::new(cap)...` (L1 + L2, plan #00):
88//! the same node tree and keys, but the leaf brands flip so the owner gets the side
89//! it must take - `api::topic::internal::new(cap).drive().state()` is
90//! `Topic<Publish<drive::State>>` (the owner publishes its telemetry), and
91//! `api::topic::internal::new(cap).drive().target()` is `Topic<Subscribe<drive::Target>>`
92//! (the owner reads its command input). A query owner reaches its `ServeQuery`
93//! brand the same way. The `internal` chain is the deliberate, greppable owner
94//! opt-in; a participant acquires the topics of its OWN node through it and everything
95//! it consumes through the public chain.
96//!
97//! The `internal::new` entry requires the runner-minted owner capability
98//! ([`OwnerCap`](phoxal_bus::OwnerCap), Layer 2): a participant obtains it from
99//! `phoxal::SetupContext::owner_capability()` and passes it in. On the documented
100//! surface, owning a topic therefore cannot happen by accident - only with a
101//! capability the runner mints.
102
103use phoxal_macros::phoxal_api_tree;
104
105/// The contract primitive traits, re-exported from the `phoxal-bus` crate (the
106/// ABI floor) so they stay addressable at `phoxal_api::ApiVersion` /
107/// `phoxal_api::ContractBody`.
108///
109/// - [`ApiVersion`] is the marker trait identifying one API version (D60),
110///   implemented only by the zero-variant `enum Api {}` that [`phoxal_api_tree!`]
111///   generates inside each version module; its `ID` is the module name (`"v1"`).
112///   Its `IS_PREVIEW` const is lifecycle metadata only.
113/// - [`ContractBody`] is a version-local wire body (D61): a plain serde type
114///   bound to exactly one [`ApiVersion`] and one contract topic. Every body
115///   declared inside a [`phoxal_api_tree!`] node gets a generated impl; handles,
116///   `SetupContext` builders, and the `Service`/`Driver` derive assertions key
117///   off its `Api`/`TOPIC`. `TOPIC` is version-qualified (D1) and *is* the
118///   compatibility key - there is no `SCHEMA_ID`/`FAMILY`; its serde encoding
119///   *is* the wire payload, with no version envelope (D62).
120pub use phoxal_bus::{ApiVersion, ContractBody};
121
122phoxal_api_tree! {
123    version v1 {
124        drive {
125            /// Why actuation authority is in its current state.
126            enum StopReason {
127                NoTarget,
128                TargetStale,
129                TargetFromFuture,
130                TargetNotFinite,
131                ActuatorCommandNotFinite,
132                Inactive,
133                EmergencyStop,
134                Fault,
135            }
136
137            /// Whether the drive is actively commanding the actuators.
138            enum ActuatorAuthority {
139                Active,
140                Stopped,
141            }
142
143            /// A requested or limited planar velocity.
144            struct Target {
145                linear_x_mps: f32,
146                angular_z_radps: f32,
147                curvature_limit_radpm: Option<f32>,
148            }
149
150            /// The drive participant's published control state.
151            struct State {
152                target: Target,
153                limited_target: Target,
154                actuator_authority: ActuatorAuthority,
155                stop_reason: Option<StopReason>,
156                target_age_ns: Option<u64>,
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                stamp_ns: Option<u64>,
183            }
184
185            /// Transforms that do not change over time.
186            struct StaticTransforms {
187                transforms: Vec<FrameTransform>,
188            }
189
190            /// The current transform tree.
191            struct Tree {
192                transforms: Vec<FrameTransform>,
193            }
194
195            /// Ask for the transform between two frames, optionally at a time.
196            struct LookupRequest {
197                target_frame_id: String,
198                source_frame_id: String,
199                at_ns: Option<u64>,
200            }
201
202            /// The resolved transform, or `None` if it is not available.
203            struct LookupResponse {
204                transform: Option<FrameTransform>,
205            }
206
207            topic tree: state Tree;
208            topic static_transforms: state StaticTransforms;
209            topic lookup: query LookupRequest => LookupResponse;
210        }
211
212        power {
213            /// A platform power command.
214            #[derive(Copy, Eq)]
215            enum Command {
216                Reboot,
217                Shutdown,
218            }
219
220            /// Where the power participant is in handling a command.
221            #[derive(Copy, Eq)]
222            enum Status {
223                Idle,
224                Rebooting,
225                ShuttingDown,
226                Failed,
227            }
228
229            /// Why a power command was rejected outright.
230            #[derive(Copy, Eq)]
231            #[serde(rename_all = "snake_case")]
232            enum RejectedReason {
233                HostIntegrationUnavailable,
234                CommandRejected,
235            }
236
237            /// Why an accepted power command later failed.
238            #[derive(Copy, Eq)]
239            #[serde(rename_all = "snake_case")]
240            enum FailedReason {
241                HostCommandFailed,
242            }
243
244            /// The power participant's published state.
245            struct State {
246                status: Status,
247                detail: Option<String>,
248            }
249
250            topic command: command Command;
251            topic state: state State;
252        }
253
254        motion {
255            struct Target {
256                linear_x_mps: f32,
257                angular_z_radps: f32,
258                curvature_limit_radpm: Option<f32>,
259            }
260
261            #[derive(Copy, Eq)]
262            #[serde(rename_all = "snake_case")]
263            enum Source {
264                Manual,
265                Navigation,
266                EmergencyStop,
267            }
268
269            #[derive(Copy, Eq)]
270            #[serde(rename_all = "snake_case")]
271            enum ZeroReason {
272                NoCandidate,
273                ManualCandidateStale,
274                NavigationCandidateStale,
275                ManualCandidateFromFuture,
276                NavigationCandidateFromFuture,
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            #[derive(Eq)]
297            struct EmergencyStopRequest {
298                engaged: bool,
299            }
300
301            struct State {
302                manual_candidate_age_ns: Option<u64>,
303                autonomous_candidate_age_ns: Option<u64>,
304                safety_constraints_age_ns: Option<u64>,
305                selected_source: Option<Source>,
306                final_target: Target,
307                zero_reason: Option<ZeroReason>,
308                safety_runtime: SafetyRuntime,
309                software_estop_engaged: bool,
310                component_estop_blocked: bool,
311                active_safety_constraints: Vec<super::safety::Constraint>,
312            }
313
314            topic manual: command ManualCommand;
315            topic estop: command EmergencyStopRequest;
316            topic state: state State;
317        }
318
319        safety {
320            /// Why safety is stopping or limiting body motion.
321            #[derive(Copy, Eq)]
322            #[serde(rename_all = "snake_case")]
323            enum ConstraintReason {
324                WorldUnavailable,
325                MapUnavailable,
326                DrivableSpaceUnavailable,
327                LocalizationUnavailable,
328                LocalizationUncertain,
329                ObstacleProximity,
330                RangeSensorFault,
331                DriveFault,
332                BatteryLow,
333                BatteryCritical,
334                SpeedZone,
335                OperatorPolicy,
336            }
337
338            /// Typed origin of one constraint, suitable for operator diagnosis.
339            #[derive(Copy, Eq)]
340            #[serde(rename_all = "snake_case")]
341            enum ConstraintSourceKind {
342                WorldModel,
343                Map,
344                Localization,
345                Range,
346                Drive,
347                Battery,
348                Operator,
349            }
350
351            struct ConstraintSource {
352                kind: ConstraintSourceKind,
353                participant_id: String,
354                component_id: Option<String>,
355                capability_id: Option<String>,
356            }
357
358            struct Constraint {
359                reason: ConstraintReason,
360                source: ConstraintSource,
361                stop: bool,
362                max_linear_speed_mps: Option<f32>,
363                max_angular_speed_radps: Option<f32>,
364                observed_value: Option<f32>,
365                valid_from_ns: u64,
366                expires_at_ns: u64,
367            }
368
369            /// The sole safety-to-motion control product. Motion accepts it only
370            /// in the same epoch and before `expires_at_ns`.
371            struct MotionConstraints {
372                sequence: u64,
373                stop: bool,
374                max_linear_speed_mps: Option<f32>,
375                max_angular_speed_radps: Option<f32>,
376                constraints: Vec<Constraint>,
377                expires_at_ns: u64,
378            }
379
380            /// Operator-facing state mirrors the exact product consumed by motion.
381            struct State {
382                clear: bool,
383                motion: MotionConstraints,
384            }
385
386            topic constraints: state MotionConstraints;
387            topic state: state State;
388        }
389
390        navigation {
391            #[derive(Eq)]
392            struct RequestId {
393                value: String,
394            }
395
396            struct Pose {
397                x_m: f64,
398                y_m: f64,
399                yaw_rad: Option<f64>,
400            }
401
402            struct Path {
403                poses: Vec<Pose>,
404                map_revision: Option<u64>,
405            }
406
407            enum RequestKind {
408                GotoPose(Pose),
409                FollowPath(Path),
410                Cancel(RequestId),
411            }
412
413            struct Request {
414                request_id: RequestId,
415                kind: RequestKind,
416            }
417
418            enum State {
419                Idle,
420                Accepted(RequestId),
421                Running(RequestId),
422            }
423
424            #[derive(Copy, Eq)]
425            #[serde(rename_all = "snake_case")]
426            enum FailureReason {
427                LocalizationUnavailable,
428                MapUnavailable,
429                MapChanged,
430                NoPath,
431                Blocked,
432                Internal,
433            }
434
435            #[derive(Copy, Eq)]
436            #[serde(rename_all = "snake_case")]
437            enum RefusalReason {
438                Busy,
439                InvalidRequest,
440                Unsupported,
441            }
442
443            enum Outcome {
444                Succeeded,
445                Failed(FailureReason),
446                Refused(RefusalReason),
447                Cancelled,
448                TimedOut,
449            }
450
451            struct Progress {
452                request_id: RequestId,
453                distance_remaining_m: f64,
454                path_index: u32,
455            }
456
457            struct Result {
458                request_id: RequestId,
459                outcome: Outcome,
460            }
461
462            struct Candidate {
463                request_id: RequestId,
464                linear_x_mps: f32,
465                angular_z_radps: f32,
466            }
467
468            struct FrontierRequest {
469                map_revision: Option<u64>,
470            }
471
472            struct Frontier {
473                x_m: f64,
474                y_m: f64,
475                score: f32,
476                size: u32,
477            }
478
479            struct FrontierResponse {
480                frontier: Option<Frontier>,
481                map_revision: Option<u64>,
482            }
483
484            topic request: command Request;
485            topic state: state State;
486            topic progress: state Progress;
487            topic result: state Result;
488            topic candidate: state Candidate;
489            topic next_frontier: query FrontierRequest => FrontierResponse;
490        }
491
492        behavior {
493            #[derive(Eq)]
494            struct RequestId {
495                value: String,
496            }
497
498            #[derive(Copy, Eq)]
499            #[serde(rename_all = "snake_case")]
500            enum ConflictPolicy {
501                Reject,
502                Queue,
503                Interrupt,
504            }
505
506            enum Value {
507                Bool(bool),
508                Integer(i64),
509                Number(f64),
510                String(String),
511                Pose(super::navigation::Pose),
512            }
513
514            struct Request {
515                request_id: RequestId,
516                behavior_id: String,
517                args: ::std::collections::BTreeMap<String, Value>,
518                priority: u8,
519                conflict_policy: ConflictPolicy,
520            }
521
522            enum Command {
523                Pause,
524                Resume,
525                Cancel,
526            }
527
528            #[derive(Copy, Eq)]
529            #[serde(rename_all = "snake_case")]
530            enum ExecutionStatus {
531                Idle,
532                Running,
533                Paused,
534                Succeeded,
535                Failed,
536                Cancelled,
537                Abandoned,
538            }
539
540            #[derive(Copy, Eq)]
541            #[serde(rename_all = "snake_case")]
542            enum NodeStatus {
543                Idle,
544                Running,
545                Succeeded,
546                Failed,
547                Skipped,
548                Waiting,
549                Cancelling,
550            }
551
552            #[derive(Copy, Eq)]
553            #[serde(rename_all = "snake_case")]
554            enum FailureReason {
555                MissingCapability,
556                ActionRefused,
557                ActionFailed,
558                ActionTimedOut,
559                ActionCancelled,
560                ConditionFailed,
561                SafetyStopped,
562                EmergencyStopped,
563                ResourceConflict,
564                InvalidArgument,
565                InvalidBlackboardValue,
566                SubtreeFailed,
567                ExecutionAbandoned,
568                InternalError,
569            }
570
571            struct Failure {
572                reason: FailureReason,
573                detail: Option<String>,
574                node_path: Option<String>,
575                action_id: Option<String>,
576            }
577
578            struct DefinitionRef {
579                id: String,
580                version: String,
581                content_hash: String,
582            }
583
584            struct State {
585                execution_id: Option<String>,
586                root_behavior_id: Option<String>,
587                active_request_id: Option<RequestId>,
588                active_behavior_id: Option<String>,
589                status: ExecutionStatus,
590                active_node_path: Option<String>,
591                failure: Option<Failure>,
592            }
593
594            struct Snapshot {
595                execution_id: Option<String>,
596                root: Option<DefinitionRef>,
597                definition_stack: Vec<DefinitionRef>,
598                active_request_id: Option<RequestId>,
599                active_behavior_id: Option<String>,
600                status: ExecutionStatus,
601                node_statuses: ::std::collections::BTreeMap<String, NodeStatus>,
602                active_node_path: Option<String>,
603                blackboard: ::std::collections::BTreeMap<String, Value>,
604                args: ::std::collections::BTreeMap<String, Value>,
605                started_at_ns: Option<u64>,
606                updated_at_ns: u64,
607                failure: Option<Failure>,
608            }
609
610            enum EventKind {
611                ExecutionStarted,
612                ExecutionPaused,
613                ExecutionResumed,
614                ExecutionCompleted,
615                ExecutionCancelled,
616                ExecutionAbandoned,
617                NodeTransition(NodeStatus),
618                RequestAccepted,
619                RequestCompleted(ExecutionStatus),
620                RequestRejected(FailureReason),
621            }
622
623            struct Event {
624                sequence: u64,
625                execution_id: Option<String>,
626                request_id: Option<RequestId>,
627                behavior_id: Option<String>,
628                content_hash: Option<String>,
629                node_path: Option<String>,
630                kind: EventKind,
631                failure: Option<Failure>,
632                participant_id: String,
633                logical_time_ns: u64,
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: state 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 v1.
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                overflowed_rows: u32,
771            }
772
773            log {
774                /// Requests tool-log's complete current bounded snapshot. The
775                /// first protocol version intentionally has no pagination or
776                /// filtering surface.
777                struct SnapshotRequest {}
778
779                /// Wall-clock timestamp copied from one participant-originated
780                /// structured `v1::logs` event.
781                struct Timestamp {
782                    unix_seconds: i64,
783                    nanos: u32,
784                }
785
786                #[derive(Copy, Eq)]
787                #[serde(rename_all = "snake_case")]
788                enum Level {
789                    Error,
790                    Warn,
791                    Info,
792                    Debug,
793                    Trace,
794                }
795
796                #[serde(untagged)]
797                enum LogValue {
798                    Bool(bool),
799                    I64(i64),
800                    U64(u64),
801                    F64(f64),
802                    String(String),
803                }
804
805                /// One retained participant log. `sequence` is assigned by
806                /// tool-log at ingest and is independent of the producer's
807                /// `source_sequence`.
808                struct Record {
809                    sequence: u64,
810                    participant_id: String,
811                    source_sequence: u64,
812                    time: Timestamp,
813                    level: Level,
814                    target: String,
815                    message: String,
816                    fields: ::std::collections::BTreeMap<String, LogValue>,
817                    dropped: u32,
818                    truncated: u32,
819                }
820
821                /// The complete bounded tool-log state at `cursor`.
822                struct Snapshot {
823                    cursor: crate::v1::tool::Cursor,
824                    /// Cumulative structured log samples evicted from
825                    /// tool-log's bounded ingest subscriber in this process.
826                    /// An increase is observable, unrecoverable source loss;
827                    /// it is distinct from producer-side `Record::dropped`.
828                    ingest_dropped: u64,
829                    records: Vec<Record>,
830                }
831
832                /// One live record following the snapshot query. A consumer
833                /// must re-query when the generation changes or the sequence is
834                /// not exactly one after its installed cursor.
835                struct Follow {
836                    cursor: crate::v1::tool::Cursor,
837                    /// Current cumulative tool-log ingest loss counter.
838                    ingest_dropped: u64,
839                    record: Record,
840                }
841
842                topic snapshot: query SnapshotRequest => Snapshot;
843                topic follow: state Follow;
844            }
845
846            bus {
847                /// Requests tool-bus's complete current bounded snapshot. The
848                /// first protocol version intentionally has no pagination or
849                /// filtering surface.
850                struct SnapshotRequest {}
851
852                /// One topic-producer pair measured during a one-second window.
853                /// Empty topic and producer identify the bounded overflow row.
854                struct TopicMetric {
855                    topic: String,
856                    from_participant: String,
857                    ingress_rate_hz: f32,
858                    count: u64,
859                }
860
861                /// One retained bus-rate window. `window_ns` is a duration, not
862                /// a production timestamp.
863                struct Window {
864                    sequence: u64,
865                    topics: Vec<TopicMetric>,
866                    throughput_msg_s: f32,
867                    window_ns: u64,
868                }
869
870                /// The current partial counters plus the bounded recent
871                /// completed-window history. The partial `current` window has
872                /// the next sequence that its eventual follow item will use;
873                /// the snapshot cursor covers only `windows`.
874                struct Snapshot {
875                    cursor: crate::v1::tool::Cursor,
876                    current: Option<Window>,
877                    windows: Vec<Window>,
878                }
879
880                /// One newly completed bus-rate window.
881                struct Follow {
882                    cursor: crate::v1::tool::Cursor,
883                    window: Window,
884                }
885
886                topic snapshot: query SnapshotRequest => Snapshot;
887                topic follow: state Follow;
888            }
889
890            device {
891                /// One mounted filesystem capacity observation. The contract
892                /// intentionally excludes OS-specific device names and I/O
893                /// counters; an unavailable storage inventory is represented
894                /// by `Sample::disks == None`, never fabricated zero rows.
895                struct Disk {
896                    mount_point: String,
897                    file_system: String,
898                    used_bytes: u64,
899                    total_bytes: u64,
900                }
901
902                /// Portable whole-device observations produced by one
903                /// runner-owned tool-device. Every optional field is `None`
904                /// when the current platform cannot provide a truthful value.
905                /// These totals must never be attributed to a runtime.
906                struct Sample {
907                    /// Logical device identity within the robot root. The
908                    /// current execution environment is always `main`.
909                    device_id: String,
910                    cpu_pct: Option<f32>,
911                    ram_used_bytes: Option<u64>,
912                    ram_total_bytes: Option<u64>,
913                    swap_used_bytes: Option<u64>,
914                    swap_total_bytes: Option<u64>,
915                    load_1m: Option<f32>,
916                    load_5m: Option<f32>,
917                    load_15m: Option<f32>,
918                    uptime_s: Option<u64>,
919                    disks: Option<Vec<Disk>>,
920                    /// Host-monotonic duration between sampler refreshes.
921                    window_ns: u64,
922                }
923
924                /// Requests tool-telemetry's bounded device history.
925                struct SnapshotRequest {
926                    /// Optional exact logical device filter.
927                    device_id: Option<String>,
928                    /// Maximum newest records to return. Zero selects the
929                    /// tool's bounded default.
930                    limit: u32,
931                    /// Exclusive global ingest-sequence upper bound.
932                    before_sequence: Option<u64>,
933                }
934
935                struct Record {
936                    sequence: u64,
937                    sample: Sample,
938                    /// Text fields or rows truncated by tool-telemetry.
939                    truncated: u32,
940                }
941
942                struct Snapshot {
943                    cursor: crate::v1::tool::Cursor,
944                    records: Vec<Record>,
945                    /// Records evicted by the absolute capacity bound before
946                    /// their five-minute age horizon elapsed.
947                    capacity_evictions: u64,
948                    next_before_sequence: Option<u64>,
949                }
950
951                struct Follow {
952                    cursor: crate::v1::tool::Cursor,
953                    record: Record,
954                }
955
956                topic sample: state Sample;
957                topic snapshot: query SnapshotRequest => Snapshot;
958                topic follow: state Follow;
959            }
960
961            runtime {
962                /// Runner-originated portable performance rollup. The runner
963                /// publishes at most one per host-monotonic grid interval;
964                /// envelope provenance identifies the participant. Interval
965                /// counters are best-effort sequential atomic samples, not a
966                /// transactional stop-the-world boundary, so concurrent queue
967                /// activity may land on either neighboring rollup.
968                struct Rollup {
969                    window_ns: u64,
970                    step: Option<crate::v1::tool::RuntimeStep>,
971                    topics: Vec<crate::v1::tool::RuntimeTopic>,
972                    overflow: Option<crate::v1::tool::RuntimeTopic>,
973                }
974
975                /// Requests tool-telemetry's current bounded five-minute
976                /// runtime history.
977                struct SnapshotRequest {
978                    /// Optional exact participant filter. `None` selects the
979                    /// complete per-robot history.
980                    participant_id: Option<String>,
981                    /// Maximum newest records to return. Zero selects the
982                    /// tool's bounded default.
983                    limit: u32,
984                    /// Exclusive global ingest-sequence upper bound for
985                    /// backward pagination. `None` starts at the newest record.
986                    before_sequence: Option<u64>,
987                }
988
989                /// One retained rollup. `sequence` is assigned by
990                /// tool-telemetry at ingest, independent of producer metadata.
991                /// Duplicate normal topic keys are deterministically
992                /// re-aggregated before row bounds are applied.
993                struct Record {
994                    sequence: u64,
995                    participant_id: String,
996                    /// Text values truncated by tool-telemetry's ingest bound.
997                    /// Oversized/excess topic identities are not truncated;
998                    /// they are aggregated into the explicit overflow row.
999                    truncated: u32,
1000                    window_ns: u64,
1001                    step: Option<crate::v1::tool::RuntimeStep>,
1002                    topics: Vec<crate::v1::tool::RuntimeTopic>,
1003                    overflow: Option<crate::v1::tool::RuntimeTopic>,
1004                }
1005
1006                struct Snapshot {
1007                    cursor: crate::v1::tool::Cursor,
1008                    records: Vec<Record>,
1009                    /// Records evicted by the absolute memory cap before their
1010                    /// five-minute age horizon elapsed.
1011                    capacity_evictions: u64,
1012                    /// Pass this as the next request's `before_sequence` to
1013                    /// continue backward. `None` means the retained matching
1014                    /// history is complete.
1015                    next_before_sequence: Option<u64>,
1016                }
1017
1018                struct Follow {
1019                    cursor: crate::v1::tool::Cursor,
1020                    record: Record,
1021                }
1022
1023                topic rollup: state Rollup;
1024                topic snapshot: query SnapshotRequest => Snapshot;
1025                topic follow: state Follow;
1026            }
1027        }
1028
1029        bus {
1030            uplink {
1031                /// Reserved observable state for a future authenticated router
1032                /// uplink. Phase 1 has no publisher for this contract.
1033                #[derive(Copy, Eq)]
1034                #[serde(rename_all = "snake_case")]
1035                enum UplinkPhase {
1036                    Disabled,
1037                    Connecting,
1038                    Connected,
1039                    /// Reserved for future retry telemetry.
1040                    Retrying,
1041                }
1042
1043                /// Reserved router-owned state for a future optional site uplink.
1044                struct State {
1045                    phase: UplinkPhase,
1046                    connect: Option<String>,
1047                    /// Reserved for future retry telemetry.
1048                    retry_attempt: u32,
1049                    /// Optional human-readable diagnostic while not connected,
1050                    /// such as DNS failure or waiting for the configured remote
1051                    /// link. Invalid endpoints fail configuration before startup.
1052                    detail: Option<String>,
1053                }
1054
1055                topic state: state State;
1056            }
1057        }
1058
1059
1060
1061
1062        perception {
1063            /// A single detected object: class, confidence, and pose in a frame.
1064            struct Detection {
1065                class_id: String,
1066                confidence: f32,
1067                position_m: [f64; 3],
1068                frame_id: String,
1069                track_id: Option<u64>,
1070            }
1071
1072            /// A batch of detections from one perception cycle.
1073            struct Detections {
1074                detections: Vec<Detection>,
1075                stamp_ns: Option<u64>,
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 simulator clock: current time and whether it is advancing.
1127            struct Clock {
1128                now_ns: u64,
1129                running: bool,
1130            }
1131
1132            /// A command to the simulator's run loop.
1133            #[derive(Copy, Eq)]
1134            enum Control {
1135                Pause,
1136                Resume,
1137                Reset,
1138            }
1139
1140            /// The simulated robot's ground-truth planar pose.
1141            struct RobotPose {
1142                x_m: f64,
1143                y_m: f64,
1144                yaw_rad: f64,
1145            }
1146
1147            /// Whether the simulated robot is in contact, with optional detail.
1148            struct Contact {
1149                in_contact: bool,
1150                detail: Option<String>,
1151            }
1152
1153            topic clock: state Clock;
1154            topic control: command Control;
1155            topic robot_pose: state RobotPose;
1156            topic contact: state Contact;
1157        }
1158
1159        // Per-instance component capabilities (D17/D38: framework participant / driver
1160        // territory). `component(instance)` selects a manifest-declared component;
1161        // each child `kind(capability)` is a self-contained node whose key is
1162        // `component/{instance}/<kind>/{capability}/<leaf>`. Nodes duplicate any
1163        // types they share by design - the node path disambiguates, so the names
1164        // are path-local.
1165        component(instance) {
1166            motor(capability) {
1167                /// A per-actuator command.
1168                enum Command {
1169                    Velocity(f32),
1170                    Torque(f32),
1171                    Stop,
1172                }
1173
1174                topic command: command Command;
1175            }
1176
1177            encoder(capability) {
1178                /// Per-encoder sample on a dynamic per-instance key.
1179                struct Sample {
1180                    position_rad: f64,
1181                    velocity_radps: f32,
1182                }
1183
1184                topic sample: state Sample;
1185            }
1186
1187            accelerometer(capability) {
1188                /// Raw accelerometer sample in the sensor-local frame in m/s^2.
1189                struct Sample {
1190                    linear_acceleration: [f32; 3],
1191                }
1192
1193                topic sample: state Sample;
1194            }
1195
1196            gyroscope(capability) {
1197                /// Raw angular velocity sample in the sensor-local frame in rad/s.
1198                struct Sample {
1199                    angular_velocity: [f32; 3],
1200                }
1201
1202                topic sample: state Sample;
1203            }
1204
1205            magnetometer(capability) {
1206                /// Raw magnetic-field sample in the sensor-local frame.
1207                struct Sample {
1208                    magnetic_field: [f32; 3],
1209                }
1210
1211                topic sample: state Sample;
1212            }
1213
1214            imu(capability) {
1215                #[derive(Copy, Eq)]
1216                #[serde(rename_all = "snake_case")]
1217                enum SensorHealth {
1218                    Nominal,
1219                    Degraded,
1220                    Fault,
1221                }
1222
1223                #[derive(Copy)]
1224                struct Bias {
1225                    angular_velocity_radps: [f32; 3],
1226                    linear_acceleration_mps2: [f32; 3],
1227                }
1228
1229                struct Sample {
1230                    orientation: Option<[f32; 4]>,
1231                    angular_velocity_radps: [f32; 3],
1232                    linear_acceleration_mps2: [f32; 3],
1233                    covariance: Option<[f32; 9]>,
1234                    noise_density: Option<[f32; 3]>,
1235                    sensor_frame_id: Option<String>,
1236                    measured_at_ns: Option<u64>,
1237                    health: SensorHealth,
1238                    bias: Option<Bias>,
1239                }
1240
1241                topic sample: state Sample;
1242            }
1243
1244            range(capability) {
1245                #[derive(Copy, Eq)]
1246                #[serde(rename_all = "snake_case")]
1247                enum SensorHealth {
1248                    Nominal,
1249                    Degraded,
1250                    Fault,
1251                }
1252
1253                #[derive(Copy)]
1254                struct Limits {
1255                    min_m: f32,
1256                    max_m: f32,
1257                }
1258
1259                #[derive(Copy)]
1260                struct SampleQuality {
1261                    valid: bool,
1262                    confidence: Option<f32>,
1263                }
1264
1265                struct Sample {
1266                    distance_m: f32,
1267                    limits: Option<Limits>,
1268                    measured_at_ns: Option<u64>,
1269                    quality: Option<SampleQuality>,
1270                    health: SensorHealth,
1271                }
1272
1273                topic sample: state Sample;
1274            }
1275
1276            gnss(capability) {
1277                /// A GNSS fix: geodetic position plus a 3x3 position covariance.
1278                struct Sample {
1279                    latitude: f64,
1280                    longitude: f64,
1281                    altitude: f64,
1282                    position_covariance: [f64; 9],
1283                }
1284
1285                topic sample: state Sample;
1286            }
1287
1288            camera(capability) {
1289                #[derive(Copy, Eq)]
1290                #[serde(rename_all = "snake_case")]
1291                enum Encoding {
1292                    Jpeg,
1293                    Png,
1294                    L8,
1295                    Rgb8,
1296                    Rgba8,
1297                }
1298
1299                #[derive(Copy)]
1300                struct Intrinsics {
1301                    fx: f32,
1302                    fy: f32,
1303                    cx: f32,
1304                    cy: f32,
1305                }
1306
1307                struct Distortion {
1308                    model: String,
1309                    coefficients: Vec<f32>,
1310                }
1311
1312                #[derive(Copy)]
1313                struct ExposureTiming {
1314                    exposure_start_ns: Option<u64>,
1315                    exposure_duration_ns: Option<u64>,
1316                }
1317
1318                struct CalibrationIdentity {
1319                    id: String,
1320                    version: String,
1321                }
1322
1323                /// One camera frame: encoded pixel bytes plus optional calibration
1324                /// and timing metadata.
1325                struct Frame {
1326                    width: u32,
1327                    height: u32,
1328                    encoding: Encoding,
1329                    intrinsics: Option<Intrinsics>,
1330                    distortion: Option<Distortion>,
1331                    exposure: Option<ExposureTiming>,
1332                    measured_at_ns: Option<u64>,
1333                    calibration: Option<CalibrationIdentity>,
1334                    #[serde(with = "serde_bytes")]
1335                    data: Vec<u8>,
1336                }
1337
1338                topic frame: state Frame;
1339            }
1340
1341            depth(capability) {
1342                #[derive(Copy, Eq)]
1343                #[serde(rename_all = "snake_case")]
1344                enum Encoding {
1345                    U16Millimeters,
1346                }
1347
1348                #[derive(Copy, Eq)]
1349                #[serde(rename_all = "snake_case")]
1350                enum InvalidSamplePolicy {
1351                    ZeroIsInvalid,
1352                    NonFiniteIsInvalid,
1353                }
1354
1355                #[derive(Copy)]
1356                struct Intrinsics {
1357                    fx: f32,
1358                    fy: f32,
1359                    cx: f32,
1360                    cy: f32,
1361                }
1362
1363                struct Distortion {
1364                    model: String,
1365                    coefficients: Vec<f32>,
1366                }
1367
1368                #[derive(Copy)]
1369                struct ExposureTiming {
1370                    exposure_start_ns: Option<u64>,
1371                    exposure_duration_ns: Option<u64>,
1372                }
1373
1374                struct CalibrationIdentity {
1375                    id: String,
1376                    version: String,
1377                }
1378
1379                /// One depth frame: per-pixel millimetre samples plus optional
1380                /// calibration and timing metadata.
1381                struct Frame {
1382                    samples_mm: Vec<u16>,
1383                    encoding: Encoding,
1384                    invalid_sample_policy: InvalidSamplePolicy,
1385                    width: Option<u32>,
1386                    height: Option<u32>,
1387                    intrinsics: Option<Intrinsics>,
1388                    distortion: Option<Distortion>,
1389                    exposure: Option<ExposureTiming>,
1390                    measured_at_ns: Option<u64>,
1391                    calibration: Option<CalibrationIdentity>,
1392                }
1393
1394                topic frame: state Frame;
1395            }
1396
1397            lidar(capability) {
1398                #[derive(Copy, Eq)]
1399                #[serde(rename_all = "snake_case")]
1400                enum SensorHealth {
1401                    Nominal,
1402                    Degraded,
1403                    Fault,
1404                }
1405
1406                #[derive(Copy)]
1407                struct ScanGeometry {
1408                    angle_min_rad: f32,
1409                    angle_increment_rad: f32,
1410                }
1411
1412                #[derive(Copy)]
1413                struct RangeLimits {
1414                    min_m: f32,
1415                    max_m: f32,
1416                }
1417
1418                #[derive(Copy)]
1419                struct ScanQuality {
1420                    valid_points: u32,
1421                }
1422
1423                struct Ranges {
1424                    ranges: Vec<f32>,
1425                    geometry: Option<ScanGeometry>,
1426                    limits: Option<RangeLimits>,
1427                    measured_at_ns: Option<u64>,
1428                    quality: Option<ScanQuality>,
1429                    health: SensorHealth,
1430                }
1431
1432                struct Points {
1433                    points: Vec<[f32; 3]>,
1434                    limits: Option<RangeLimits>,
1435                    measured_at_ns: Option<u64>,
1436                    quality: Option<ScanQuality>,
1437                    health: SensorHealth,
1438                }
1439
1440                /// One lidar scan, either as polar ranges or as cartesian points.
1441                #[serde(tag = "kind", rename_all = "snake_case")]
1442                enum Scan {
1443                    Ranges(Ranges),
1444                    Points(Points),
1445                }
1446
1447                topic scan: state Scan;
1448            }
1449
1450            mmwave(capability) {
1451                /// One mmWave radar detection: position, velocity, and SNR.
1452                #[derive(Copy)]
1453                struct Detection {
1454                    position: [f32; 3],
1455                    velocity: [f32; 3],
1456                    snr: f32,
1457                }
1458
1459                /// One mmWave radar scan as a set of detections.
1460                struct Scan {
1461                    detections: Vec<Detection>,
1462                }
1463
1464                topic scan: state Scan;
1465            }
1466
1467            microphone(capability) {
1468                /// One audio frame as raw encoded bytes.
1469                struct Frame {
1470                    data: Vec<u8>,
1471                }
1472
1473                topic frame: state Frame;
1474            }
1475
1476            led(capability) {
1477                /// A per-LED on/off command.
1478                #[derive(Copy, Eq)]
1479                enum Command {
1480                    On,
1481                    Off,
1482                }
1483
1484                topic command: command Command;
1485            }
1486
1487            emergency_stop(capability) {
1488                /// Per-instance emergency-stop state.
1489                #[derive(Eq)]
1490                struct State {
1491                    engaged: bool,
1492                }
1493
1494                topic state: state State;
1495            }
1496        }
1497
1498        odometry {
1499            /// A planar pose + twist estimate in the odometry frame.
1500            struct State {
1501                x_m: f64,
1502                y_m: f64,
1503                yaw_rad: f64,
1504                linear_x_mps: f32,
1505                angular_z_radps: f32,
1506            }
1507
1508            topic state: state State;
1509        }
1510
1511        localize {
1512            /// A planar localization estimate in the map frame.
1513            struct LocalizationState {
1514                x_m: f64,
1515                y_m: f64,
1516                yaw_rad: f64,
1517                confidence: f32,
1518            }
1519
1520            topic state: state LocalizationState;
1521        }
1522
1523        map {
1524            /// A published map revision marker.
1525            struct Revision {
1526                revision: u64,
1527                resolution_m: f32,
1528            }
1529
1530            /// Request a rectangular submap window (map-frame metres).
1531            struct SubmapRequest {
1532                min_x_m: f64,
1533                min_y_m: f64,
1534                max_x_m: f64,
1535                max_y_m: f64,
1536            }
1537
1538            /// An occupancy-grid window: row-major cells, 0..=100 + 255 = unknown.
1539            struct SubmapResponse {
1540                width: u32,
1541                height: u32,
1542                resolution_m: f32,
1543                cells: Vec<u8>,
1544            }
1545
1546            topic revision: state Revision;
1547            topic submap: query SubmapRequest => SubmapResponse;
1548        }
1549
1550        asset {
1551            /// Fetch a stored asset by path.
1552            struct GetRequest {
1553                path: String,
1554            }
1555
1556            /// The asset bytes, a not-found marker, or a rejected path.
1557            enum GetResponse {
1558                Found { bytes: Vec<u8> },
1559                Missing,
1560                InvalidPath,
1561            }
1562
1563            topic get: query GetRequest => GetResponse;
1564        }
1565    }
1566
1567    // v2 is the one evolving preview surface. New and changed contracts are
1568    // edited here until the complete version is ready to freeze; preview work
1569    // does not mint another public API namespace for each implementation batch.
1570    preview version v2 {
1571        battery {
1572            /// Battery state remains preview while hardware ownership evolves.
1573            struct State {
1574                voltage_v: f32,
1575                current_a: f32,
1576                charge_ratio: f32,
1577            }
1578
1579            topic state: state State;
1580        }
1581
1582        simulation {
1583            /// One robot node that the simulator spawn authority should import.
1584            struct RobotSpawn {
1585                robot_id: String,
1586                node_string: String,
1587            }
1588
1589            /// Requests the current complete robot spawn set.
1590            ///
1591            /// `known_revision` lets a future responder distinguish initial
1592            /// bootstrap from refreshes after a runtime join. Responders may
1593            /// still return the current set when the revision is unchanged.
1594            struct SpawnRequest {
1595                known_revision: Option<u64>,
1596            }
1597
1598            /// The complete robot spawn set for one simulation world.
1599            struct SpawnSet {
1600                revision: u64,
1601                robots: Vec<RobotSpawn>,
1602            }
1603
1604            /// The authoritative advancing simulation clock. Publication means
1605            /// the world advanced; silence means it did not.
1606            struct Clock {
1607                now_ns: u64,
1608                step: u64,
1609            }
1610
1611            topic spawn: query SpawnRequest => SpawnSet;
1612            topic clock: state Clock;
1613        }
1614
1615        joypad {
1616            /// Whether an observed controller is ready for the fixed manual
1617            /// input preset, disconnected, or connected without a compatible
1618            /// control mapping.
1619            enum DeviceStatus {
1620                Ready,
1621                Disconnected,
1622                Unsupported,
1623            }
1624
1625            /// One gamepad the tool can see. `id` is a STABLE wire id the tool
1626            /// assigns (name/guid-derived) - NOT a process-local gilrs id.
1627            struct Device {
1628                id: String,
1629                name: String,
1630                status: DeviceStatus,
1631            }
1632
1633            /// The joypad tool's published device state.
1634            struct Devices {
1635                available: Vec<Device>,
1636                selected: Option<String>,
1637                enabled: bool,
1638                /// Structural reason manual input cannot be enabled in this
1639                /// session (for example robot-model or backend limitations),
1640                /// independent of transient device/request errors.
1641                unavailable_reason: Option<String>,
1642                /// One-shot acknowledgement of a failed select/enable/rescan
1643                /// request. Event-driven consumers may show it once; periodic
1644                /// state heartbeats omit it. The tool also writes the failure
1645                /// to its log stream for durable diagnostics.
1646                last_error: Option<String>,
1647            }
1648
1649            /// Client asks the tool to select a device by its stable id.
1650            struct Select {
1651                id: String,
1652            }
1653
1654            /// Client asks the tool to enable or disable manual input.
1655            struct SetEnabled {
1656                enabled: bool,
1657            }
1658
1659            /// Client asks the tool to re-enumerate devices.
1660            struct Rescan {}
1661
1662            topic devices: state Devices;
1663            topic select: command Select;
1664            topic set_enabled: command SetEnabled;
1665            topic rescan: command Rescan;
1666        }
1667    }
1668}
1669
1670#[cfg(test)]
1671mod tests;