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