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