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