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