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;