Skip to main content

phoxal_api/
lib.rs

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