1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use alloc::vec;
4use alloc::vec::Vec;
5
6#[cfg(feature = "std")]
7use crate::E2eEncryptionPolicy;
8#[cfg(feature = "std")]
9use crate::config::export_schema;
10use crate::router::encode_slice_le;
11use crate::{
12 DataEndpoint, DataType, MessageElement, TelemetryError, TelemetryResult,
13 config::{
14 OwnedDataTypeDefinition, OwnedEndpointDefinition, OwnedRuntimeSchemaSnapshot,
15 RuntimeSchemaSnapshot, e2e_encryption_policy_code, e2e_encryption_policy_from_code,
16 message_class_code, message_class_from_code, message_data_type_code,
17 message_data_type_from_code, reliable_code, reliable_from_code,
18 },
19 packet::Packet,
20};
21
22pub const DISCOVERY_ROUTE_TTL_MS: u64 = 30_000;
23pub const DISCOVERY_FAST_INTERVAL_MS: u64 = 250;
24pub const DISCOVERY_SLOW_INTERVAL_MS: u64 = 5_000;
25pub const DISCOVERY_SLOW_LINK_CAPACITY_BPS: u64 = 512;
26pub const DISCOVERY_SLOW_LINK_PING_INTERVAL_MS: u64 = 15_000;
27pub const DISCOVERY_SLOW_LINK_FULL_INTERVAL_MS: u64 = 120_000;
28pub const DISCOVERY_BASELINE_RETRY_MS: u64 = 5_000;
30pub const DISCOVERY_BASELINE_GRACE_MS: u64 = 1_000;
32pub const DISCOVERY_INCREMENTAL_RETRY_COUNT: u8 = 3;
37pub const TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS: u64 = 30_000;
38const DISCOVERY_TOPOLOGY_DELTA_MARKER: u32 = 0;
42
43fn bounded_wire_vec<T>(
50 payload: &[u8],
51 cursor: usize,
52 count: usize,
53 minimum_encoded_bytes: usize,
54 label: &'static str,
55) -> TelemetryResult<Vec<T>> {
56 let remaining = payload.len().saturating_sub(cursor);
57 if minimum_encoded_bytes == 0 || count > remaining / minimum_encoded_bytes {
58 return Err(TelemetryError::Unpack(label));
59 }
60 let mut values = Vec::new();
61 values
62 .try_reserve_exact(count)
63 .map_err(|_| TelemetryError::Unpack(label))?;
64 Ok(values)
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct DiscoveryCadenceState {
69 pub current_interval_ms: u64,
70 pub next_announce_ms: u64,
71}
72
73impl Default for DiscoveryCadenceState {
74 fn default() -> Self {
75 Self {
76 current_interval_ms: DISCOVERY_FAST_INTERVAL_MS,
77 next_announce_ms: 0,
78 }
79 }
80}
81
82impl DiscoveryCadenceState {
83 pub fn on_topology_change(&mut self, now_ms: u64) {
85 self.current_interval_ms = DISCOVERY_FAST_INTERVAL_MS;
86 self.next_announce_ms = now_ms;
87 }
88
89 pub fn on_announce_sent(&mut self, now_ms: u64) {
91 self.next_announce_ms = now_ms.saturating_add(self.current_interval_ms);
92 self.current_interval_ms = core::cmp::min(
93 self.current_interval_ms.saturating_mul(2),
94 DISCOVERY_SLOW_INTERVAL_MS,
95 );
96 }
97
98 pub fn due(&self, now_ms: u64) -> bool {
100 now_ms >= self.next_announce_ms
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct TopologyBoardNode {
106 pub sender_id: String,
107 pub reachable_endpoints: Vec<DataEndpoint>,
108 pub reachable_timesync_sources: Vec<String>,
109 pub connections: Vec<String>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct TopologyLink {
114 pub source: String,
115 pub target: String,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct TopologyAnnouncerRoute {
120 pub sender_id: String,
121 pub reachable_endpoints: Vec<DataEndpoint>,
122 pub reachable_network_variables: Vec<DataType>,
123 pub reachable_timesync_sources: Vec<String>,
124 pub routers: Vec<TopologyBoardNode>,
125 pub last_seen_ms: u64,
126 pub age_ms: u64,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct TopologySideRoute {
131 pub side_id: usize,
132 pub side_name: String,
133 pub reachable_endpoints: Vec<DataEndpoint>,
134 pub reachable_network_variables: Vec<DataType>,
135 pub reachable_timesync_sources: Vec<String>,
136 pub announcers: Vec<TopologyAnnouncerRoute>,
137 pub last_seen_ms: u64,
138 pub age_ms: u64,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct TopologySnapshot {
143 pub advertised_endpoints: Vec<DataEndpoint>,
144 pub advertised_timesync_sources: Vec<String>,
145 pub routers: Vec<TopologyBoardNode>,
146 pub links: Vec<TopologyLink>,
147 pub routes: Vec<TopologySideRoute>,
148 pub current_announce_interval_ms: u64,
149 pub next_announce_ms: u64,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct ClientStatsSnapshot {
154 pub sender_id: String,
155 pub connected: bool,
156 pub side_ids: Vec<usize>,
157 pub side_names: Vec<String>,
158 pub last_seen_ms: Option<u64>,
159 pub age_ms: Option<u64>,
160 pub reachable_endpoints: Vec<DataEndpoint>,
161 pub reachable_timesync_sources: Vec<String>,
162 pub packets_sent: u64,
163 pub packets_received: u64,
164 pub bytes_sent: u64,
165 pub bytes_received: u64,
166}
167
168pub const LINK_CAPABILITY_HEADER_TEMPLATES: u32 = 0x0000_0001;
169pub const LINK_CAPABILITY_CHUNKING: u32 = 0x0000_0002;
170pub const LINK_CAPABILITY_RELIABILITY: u32 = 0x0000_0004;
171pub const LINK_CAPABILITY_CRYPTO: u32 = 0x0000_0008;
172pub const LINK_CAPABILITY_END_TO_END_RELIABILITY: u32 = 0x0000_0010;
173pub const LINK_CAPABILITY_OMIT_UNCHANGED_TIMESTAMPS: u32 = 0x0000_0020;
174
175pub const LINK_PROFILE_CANONICAL: u8 = 0;
176pub const LINK_PROFILE_TEMPLATE: u8 = 1;
177pub const LINK_PROFILE_IPV6_LIKE: u8 = 2;
178pub const LINK_PROFILE_IPV4_LIKE: u8 = 3;
179
180pub const ADDRESS_MODE_DYNAMIC: u8 = 0;
181pub const ADDRESS_MODE_REQUESTED: u8 = 1;
182pub const ADDRESS_MODE_STATIC: u8 = 2;
183pub const ADDRESS_STATE_REQUEST: u8 = 0;
184pub const ADDRESS_STATE_APPROVED: u8 = 1;
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub struct LinkCapabilities {
188 pub version: u8,
189 pub flags: u32,
190 pub profile: u8,
191 pub max_frame_bytes: u32,
192 pub compact_header_target_bytes: u32,
193 pub max_side_transport_templates: u32,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct AddressAdvertisement {
198 pub hostname: String,
199 pub address: u32,
200 pub requested_address: u32,
201 pub mode: u8,
202 pub state: u8,
203 pub birth_ms: u64,
204 pub owner_hash: u64,
205 pub reachable_endpoints: Vec<DataEndpoint>,
206 pub reachable_network_variables: Vec<DataType>,
210 pub reachable_timesync_sources: Vec<String>,
211 pub link_capabilities: LinkCapabilities,
212}
213
214#[inline]
215pub const fn is_router_control_endpoint(ep: DataEndpoint) -> bool {
216 matches!(
217 ep,
218 DataEndpoint::TelemetryError | DataEndpoint::TimeSync | DataEndpoint::Discovery
219 )
220}
221
222#[inline]
224pub const fn is_discovery_endpoint(ep: DataEndpoint) -> bool {
225 matches!(ep, DataEndpoint::Discovery)
226}
227
228#[inline]
230pub const fn is_discovery_type(ty: DataType) -> bool {
231 matches!(
232 ty,
233 DataType::DiscoveryAnnounce
234 | DataType::DiscoveryTimeSyncSources
235 | DataType::DiscoveryTopology
236 | DataType::DiscoverySchema
237 | DataType::DiscoveryTopologyRequest
238 | DataType::DiscoverySchemaRequest
239 | DataType::ManagedVariableRequest
240 | DataType::ManagedVariableValue
241 | DataType::DiscoveryLeave
242 | DataType::DiscoveryLinkCapabilities
243 | DataType::DiscoveryAddress
244 )
245}
246
247#[inline]
248pub const fn is_discovery_request_type(ty: DataType) -> bool {
249 matches!(
250 ty,
251 DataType::DiscoveryTopologyRequest
252 | DataType::DiscoverySchemaRequest
253 | DataType::ManagedVariableRequest
254 | DataType::DiscoveryLeave
255 | DataType::DiscoveryAddress
256 )
257}
258
259fn sort_dedup_strings(items: &mut Vec<String>) {
260 items.sort_unstable();
261 items.dedup();
262}
263
264pub fn normalize_topology_boards(boards: &mut Vec<TopologyBoardNode>) {
266 for board in boards.iter_mut() {
267 board
268 .reachable_endpoints
269 .retain(|ep| !is_router_control_endpoint(*ep));
270 crate::small_sort::sort(&mut board.reachable_endpoints);
271 board.reachable_endpoints.dedup();
272 sort_dedup_strings(&mut board.reachable_timesync_sources);
273 board.connections.retain(|peer| peer != &board.sender_id);
274 sort_dedup_strings(&mut board.connections);
275 }
276 boards.sort_unstable_by(|a, b| a.sender_id.cmp(&b.sender_id));
277 boards.dedup_by(|a, b| a.sender_id == b.sender_id);
278}
279
280pub fn topology_links_from_boards(boards: &[TopologyBoardNode]) -> Vec<TopologyLink> {
281 let mut links = Vec::new();
282 for board in boards {
283 for peer in &board.connections {
284 if peer == &board.sender_id {
285 continue;
286 }
287 let (source, target) = if board.sender_id <= *peer {
288 (board.sender_id.clone(), peer.clone())
289 } else {
290 (peer.clone(), board.sender_id.clone())
291 };
292 links.push(TopologyLink { source, target });
293 }
294 }
295 links.sort_unstable_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target)));
296 links.dedup_by(|a, b| a.source == b.source && a.target == b.target);
297 links
298}
299
300pub fn merge_topology_boards(dst: &mut Vec<TopologyBoardNode>, src: &[TopologyBoardNode]) {
302 let mut index = 0;
307 while index < dst.len() {
308 if dst[index + 1..]
309 .iter()
310 .any(|node| node.sender_id == dst[index].sender_id)
311 {
312 dst.remove(index);
313 } else {
314 index += 1;
315 }
316 }
317 for board in src {
318 if let Some(entry) = dst
319 .iter_mut()
320 .find(|node| node.sender_id == board.sender_id)
321 {
322 for endpoint in &board.reachable_endpoints {
323 if !entry.reachable_endpoints.contains(endpoint) {
324 entry.reachable_endpoints.push(*endpoint);
325 }
326 }
327 for source in &board.reachable_timesync_sources {
328 if !entry.reachable_timesync_sources.contains(source) {
329 entry.reachable_timesync_sources.push(source.clone());
330 }
331 }
332 for connection in &board.connections {
333 if !entry.connections.contains(connection) {
334 entry.connections.push(connection.clone());
335 }
336 }
337 } else {
338 dst.push(board.clone());
339 }
340 }
341 normalize_topology_boards(dst);
342}
343
344pub fn summarize_topology_boards(boards: &[TopologyBoardNode]) -> (Vec<DataEndpoint>, Vec<String>) {
346 let mut reachable_endpoints = Vec::new();
347 let mut reachable_timesync_sources = Vec::new();
348 for board in boards {
349 for endpoint in &board.reachable_endpoints {
350 if !is_router_control_endpoint(*endpoint) && !reachable_endpoints.contains(endpoint) {
351 reachable_endpoints.push(*endpoint);
352 }
353 }
354 for source in &board.reachable_timesync_sources {
355 if !reachable_timesync_sources.contains(source) {
356 reachable_timesync_sources.push(source.clone());
357 }
358 }
359 }
360 crate::small_sort::sort(&mut reachable_endpoints);
361 reachable_endpoints.dedup();
362 reachable_endpoints.retain(|ep| !is_router_control_endpoint(*ep));
363 sort_dedup_strings(&mut reachable_timesync_sources);
364 (reachable_endpoints, reachable_timesync_sources)
365}
366
367pub fn build_discovery_announce(
369 sender: &str,
370 timestamp_ms: u64,
371 endpoints: &[DataEndpoint],
372) -> TelemetryResult<Packet> {
373 let payload_words: Vec<u32> = endpoints.iter().copied().map(|ep| ep.as_u32()).collect();
374 Packet::new(
375 DataType::DiscoveryAnnounce,
376 &[DataEndpoint::Discovery],
377 sender,
378 timestamp_ms,
379 encode_slice_le(payload_words.as_slice()),
380 )
381}
382
383pub fn decode_discovery_announce(pkt: &Packet) -> TelemetryResult<Vec<DataEndpoint>> {
385 if pkt.data_type() != DataType::DiscoveryAnnounce {
386 return Err(TelemetryError::InvalidType);
387 }
388 decode_discovery_payload(pkt.payload())
389}
390
391pub fn decode_discovery_payload(payload: &[u8]) -> TelemetryResult<Vec<DataEndpoint>> {
393 if !payload.len().is_multiple_of(4) {
394 return Err(TelemetryError::Unpack("discovery payload width"));
395 }
396
397 let mut endpoints = Vec::with_capacity(payload.len() / 4);
398 for chunk in payload.as_chunks::<4>().0 {
399 let raw = u32::from_le_bytes(*chunk);
400 let ep = DataEndpoint(raw);
401 if is_discovery_endpoint(ep) {
402 continue;
403 }
404 endpoints.push(ep);
405 }
406 crate::small_sort::sort(&mut endpoints);
407 endpoints.dedup();
408 Ok(endpoints)
409}
410
411pub fn build_discovery_timesync_sources<S: AsRef<str>>(
413 sender: &str,
414 timestamp_ms: u64,
415 sources: &[S],
416) -> TelemetryResult<Packet> {
417 let mut payload = Vec::new();
418 let mut deduped: Vec<&str> = sources.iter().map(|s| s.as_ref()).collect();
419 crate::small_sort::sort(&mut deduped);
420 deduped.dedup();
421
422 payload.extend_from_slice(&(deduped.len() as u32).to_le_bytes());
423 for source in deduped {
424 let bytes = source.as_bytes();
425 let len = u32::try_from(bytes.len())
426 .map_err(|_| TelemetryError::Pack("discovery source id too long"))?;
427 payload.extend_from_slice(&len.to_le_bytes());
428 payload.extend_from_slice(bytes);
429 }
430
431 Packet::new(
432 DataType::DiscoveryTimeSyncSources,
433 &[DataEndpoint::Discovery],
434 sender,
435 timestamp_ms,
436 payload.into(),
437 )
438}
439
440pub fn build_discovery_topology_request(
441 sender: &str,
442 timestamp_ms: u64,
443) -> TelemetryResult<Packet> {
444 Packet::new(
445 DataType::DiscoveryTopologyRequest,
446 &[DataEndpoint::Discovery],
447 sender,
448 timestamp_ms,
449 Vec::<u8>::new().into(),
450 )
451}
452
453pub fn build_discovery_schema_request(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
454 Packet::new(
455 DataType::DiscoverySchemaRequest,
456 &[DataEndpoint::Discovery],
457 sender,
458 timestamp_ms,
459 Vec::<u8>::new().into(),
460 )
461}
462
463pub fn build_discovery_leave(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
464 Packet::new(
465 DataType::DiscoveryLeave,
466 &[DataEndpoint::Discovery],
467 sender,
468 timestamp_ms,
469 Vec::<u8>::new().into(),
470 )
471}
472
473pub fn build_discovery_link_capabilities(
474 sender: &str,
475 timestamp_ms: u64,
476 capabilities: LinkCapabilities,
477) -> TelemetryResult<Packet> {
478 let mut payload = Vec::with_capacity(18);
479 payload.push(capabilities.version);
480 payload.extend_from_slice(&capabilities.flags.to_le_bytes());
481 payload.push(capabilities.profile);
482 payload.extend_from_slice(&capabilities.max_frame_bytes.to_le_bytes());
483 payload.extend_from_slice(&capabilities.compact_header_target_bytes.to_le_bytes());
484 payload.extend_from_slice(&capabilities.max_side_transport_templates.to_le_bytes());
485 Packet::new(
486 DataType::DiscoveryLinkCapabilities,
487 &[DataEndpoint::Discovery],
488 sender,
489 timestamp_ms,
490 payload.into(),
491 )
492}
493
494pub fn decode_discovery_link_capabilities(pkt: &Packet) -> TelemetryResult<LinkCapabilities> {
495 if pkt.data_type() != DataType::DiscoveryLinkCapabilities {
496 return Err(TelemetryError::InvalidType);
497 }
498 let payload = pkt.payload();
499 if payload.len() != 18 {
500 return Err(TelemetryError::Unpack("discovery link capabilities width"));
501 }
502 Ok(LinkCapabilities {
503 version: payload[0],
504 flags: u32::from_le_bytes(payload[1..5].try_into().expect("4-byte flags")),
505 profile: payload[5],
506 max_frame_bytes: u32::from_le_bytes(payload[6..10].try_into().expect("4-byte max frame")),
507 compact_header_target_bytes: u32::from_le_bytes(
508 payload[10..14].try_into().expect("4-byte target"),
509 ),
510 max_side_transport_templates: u32::from_le_bytes(
511 payload[14..18].try_into().expect("4-byte templates"),
512 ),
513 })
514}
515
516pub fn build_discovery_address(
517 sender: &str,
518 timestamp_ms: u64,
519 ad: &AddressAdvertisement,
520) -> TelemetryResult<Packet> {
521 let mut payload = Vec::new();
522 payload.push(2);
523 payload.push(ad.mode);
524 payload.push(ad.state);
525 payload.extend_from_slice(&ad.address.to_le_bytes());
526 payload.extend_from_slice(&ad.requested_address.to_le_bytes());
527 payload.extend_from_slice(&ad.birth_ms.to_le_bytes());
528 payload.extend_from_slice(&ad.owner_hash.to_le_bytes());
529 encode_string(&mut payload, &ad.hostname)?;
530 let mut endpoints = ad.reachable_endpoints.clone();
531 endpoints.retain(|ep| !is_discovery_endpoint(*ep));
532 crate::small_sort::sort(&mut endpoints);
533 endpoints.dedup();
534 let endpoint_count = u32::try_from(endpoints.len())
535 .map_err(|_| TelemetryError::Pack("discovery address endpoint count"))?;
536 payload.extend_from_slice(&endpoint_count.to_le_bytes());
537 for ep in endpoints {
538 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
539 }
540 let mut network_variables = ad.reachable_network_variables.clone();
541 crate::small_sort::sort(&mut network_variables);
542 network_variables.dedup();
543 let network_variable_count = u32::try_from(network_variables.len())
544 .map_err(|_| TelemetryError::Pack("discovery address network variable count"))?;
545 payload.extend_from_slice(&network_variable_count.to_le_bytes());
546 for ty in network_variables {
547 payload.extend_from_slice(&ty.as_u32().to_le_bytes());
548 }
549 let mut sources = ad.reachable_timesync_sources.clone();
550 sort_dedup_strings(&mut sources);
551 let source_count = u32::try_from(sources.len())
552 .map_err(|_| TelemetryError::Pack("discovery address source count"))?;
553 payload.extend_from_slice(&source_count.to_le_bytes());
554 for source in sources {
555 encode_string(&mut payload, &source)?;
556 }
557 payload.push(ad.link_capabilities.version);
558 payload.extend_from_slice(&ad.link_capabilities.flags.to_le_bytes());
559 payload.push(ad.link_capabilities.profile);
560 payload.extend_from_slice(&ad.link_capabilities.max_frame_bytes.to_le_bytes());
561 payload.extend_from_slice(
562 &ad.link_capabilities
563 .compact_header_target_bytes
564 .to_le_bytes(),
565 );
566 payload.extend_from_slice(
567 &ad.link_capabilities
568 .max_side_transport_templates
569 .to_le_bytes(),
570 );
571 Packet::new(
572 DataType::DiscoveryAddress,
573 &[DataEndpoint::Discovery],
574 sender,
575 timestamp_ms,
576 payload.into(),
577 )
578}
579
580pub fn decode_discovery_address(pkt: &Packet) -> TelemetryResult<AddressAdvertisement> {
581 if pkt.data_type() != DataType::DiscoveryAddress {
582 return Err(TelemetryError::InvalidType);
583 }
584 let payload = pkt.payload();
585 let mut cursor = 0usize;
586 let version = read_u8(payload, &mut cursor, "discovery address version")?;
587 if version != 1 && version != 2 {
588 return Err(TelemetryError::Unpack("discovery address version"));
589 }
590 let mode = read_u8(payload, &mut cursor, "discovery address mode")?;
591 let state = read_u8(payload, &mut cursor, "discovery address state")?;
592 let address = read_u32(payload, &mut cursor, "discovery address current")?;
593 let requested_address = read_u32(payload, &mut cursor, "discovery address requested")?;
594 if payload.len().saturating_sub(cursor) < 8 {
595 return Err(TelemetryError::Unpack("discovery address birth"));
596 }
597 let birth_ms = u64::from_le_bytes(
598 payload[cursor..cursor + 8]
599 .try_into()
600 .expect("8-byte birth"),
601 );
602 cursor += 8;
603 if payload.len().saturating_sub(cursor) < 8 {
604 return Err(TelemetryError::Unpack("discovery address owner"));
605 }
606 let owner_hash = u64::from_le_bytes(
607 payload[cursor..cursor + 8]
608 .try_into()
609 .expect("8-byte owner"),
610 );
611 cursor += 8;
612 let hostname = decode_string(payload, &mut cursor, "discovery address hostname")?;
613 let endpoint_count =
614 read_u32(payload, &mut cursor, "discovery address endpoint count")? as usize;
615 let mut reachable_endpoints = bounded_wire_vec(
616 payload,
617 cursor,
618 endpoint_count,
619 4,
620 "discovery address endpoint count",
621 )?;
622 for _ in 0..endpoint_count {
623 let raw = read_u32(payload, &mut cursor, "discovery address endpoint")?;
624 let ep = DataEndpoint(raw);
625 if !is_discovery_endpoint(ep) {
626 reachable_endpoints.push(ep);
627 }
628 }
629 crate::small_sort::sort(&mut reachable_endpoints);
630 reachable_endpoints.dedup();
631 let mut reachable_network_variables = Vec::new();
632 if version >= 2 {
633 let count = read_u32(
634 payload,
635 &mut cursor,
636 "discovery address network variable count",
637 )? as usize;
638 reachable_network_variables = bounded_wire_vec(
639 payload,
640 cursor,
641 count,
642 4,
643 "discovery address network variable count",
644 )?;
645 for _ in 0..count {
646 let raw = read_u32(payload, &mut cursor, "discovery address network variable")?;
647 let ty = DataType(raw);
652 if !is_discovery_type(ty) {
653 reachable_network_variables.push(ty);
654 }
655 }
656 crate::small_sort::sort(&mut reachable_network_variables);
657 reachable_network_variables.dedup();
658 }
659 let source_count = read_u32(payload, &mut cursor, "discovery address source count")? as usize;
660 let mut reachable_timesync_sources = bounded_wire_vec(
661 payload,
662 cursor,
663 source_count,
664 4,
665 "discovery address source count",
666 )?;
667 for _ in 0..source_count {
668 let source = decode_string(payload, &mut cursor, "discovery address source")?;
669 if !source.is_empty() {
670 reachable_timesync_sources.push(source);
671 }
672 }
673 sort_dedup_strings(&mut reachable_timesync_sources);
674 let version = read_u8(payload, &mut cursor, "discovery address link version")?;
675 let flags = read_u32(payload, &mut cursor, "discovery address link flags")?;
676 let profile = read_u8(payload, &mut cursor, "discovery address link profile")?;
677 let max_frame_bytes = read_u32(payload, &mut cursor, "discovery address link max frame")?;
678 let compact_header_target_bytes = read_u32(
679 payload,
680 &mut cursor,
681 "discovery address link compact target",
682 )?;
683 let max_side_transport_templates =
684 read_u32(payload, &mut cursor, "discovery address link templates")?;
685 if cursor != payload.len() {
686 return Err(TelemetryError::Unpack("discovery address trailing bytes"));
687 }
688 if hostname.is_empty() || address == 0 {
689 return Err(TelemetryError::Unpack("bad discovery address"));
690 }
691 Ok(AddressAdvertisement {
692 hostname,
693 address,
694 requested_address,
695 mode,
696 state,
697 birth_ms,
698 owner_hash,
699 reachable_endpoints,
700 reachable_network_variables,
701 reachable_timesync_sources,
702 link_capabilities: LinkCapabilities {
703 version,
704 flags,
705 profile,
706 max_frame_bytes,
707 compact_header_target_bytes,
708 max_side_transport_templates,
709 },
710 })
711}
712
713pub fn build_managed_variable_request(
714 sender: &str,
715 timestamp_ms: u64,
716 ty: DataType,
717) -> TelemetryResult<Packet> {
718 Packet::new(
719 DataType::ManagedVariableRequest,
720 &[DataEndpoint::Discovery],
721 sender,
722 timestamp_ms,
723 encode_slice_le(&[ty.as_u32()]),
724 )
725}
726
727pub fn decode_managed_variable_request(pkt: &Packet) -> TelemetryResult<DataType> {
728 if pkt.data_type() != DataType::ManagedVariableRequest {
729 return Err(TelemetryError::InvalidType);
730 }
731 let payload = pkt.payload();
732 if payload.len() != 4 {
733 return Err(TelemetryError::Unpack("managed variable request width"));
734 }
735 let raw = u32::from_le_bytes(payload.try_into().expect("4-byte payload"));
736 Ok(DataType(raw))
739}
740
741pub fn decode_discovery_timesync_sources(pkt: &Packet) -> TelemetryResult<Vec<String>> {
743 if pkt.data_type() != DataType::DiscoveryTimeSyncSources {
744 return Err(TelemetryError::InvalidType);
745 }
746 decode_discovery_timesync_sources_payload(pkt.payload())
747}
748
749pub fn decode_discovery_timesync_sources_payload(payload: &[u8]) -> TelemetryResult<Vec<String>> {
751 if payload.len() < 4 {
752 return Err(TelemetryError::Unpack("discovery timesync source count"));
753 }
754
755 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
756 let mut cursor = 4usize;
757 let mut out = bounded_wire_vec(payload, cursor, count, 4, "discovery timesync source count")?;
758
759 for _ in 0..count {
760 if payload.len().saturating_sub(cursor) < 4 {
761 return Err(TelemetryError::Unpack("discovery timesync source len"));
762 }
763 let len = u32::from_le_bytes(payload[cursor..cursor + 4].try_into().expect("4-byte len"))
764 as usize;
765 cursor += 4;
766 if payload.len().saturating_sub(cursor) < len {
767 return Err(TelemetryError::Unpack("discovery timesync source bytes"));
768 }
769 let raw = &payload[cursor..cursor + len];
770 cursor += len;
771 let source = core::str::from_utf8(raw)
772 .map_err(|_| TelemetryError::Unpack("discovery timesync source utf8"))?;
773 if !source.is_empty() {
774 out.push(source.to_string());
775 }
776 }
777
778 if cursor != payload.len() {
779 return Err(TelemetryError::Unpack("discovery timesync trailing bytes"));
780 }
781
782 out.sort_unstable();
783 out.dedup();
784 Ok(out)
785}
786
787type TopologyScratchPeer<'a> = (
788 &'a str,
789 &'a [DataEndpoint],
790 &'a [String],
791 &'a [TopologyBoardNode],
792);
793
794pub(crate) fn topology_snapshot_scratch<'a>(
797 sender: &str,
798 local_endpoints: usize,
799 variables: usize,
800 graph_copies: usize,
801 include_wire: bool,
802 peers: impl Iterator<Item = TopologyScratchPeer<'a>> + Clone,
803) -> (usize, usize) {
804 topology_snapshot_scratch_impl(
805 sender,
806 local_endpoints,
807 variables,
808 graph_copies,
809 include_wire,
810 &|index| peers.clone().nth(index),
811 )
812}
813
814#[inline(never)]
815fn topology_snapshot_scratch_impl<'a>(
816 sender: &str,
817 local_endpoints: usize,
818 variables: usize,
819 graph_copies: usize,
820 include_wire: bool,
821 peer_at: &dyn Fn(usize) -> Option<TopologyScratchPeer<'a>>,
822) -> (usize, usize) {
823 let peers = core::iter::from_fn({
826 let mut index = 0usize;
827 move || {
828 let peer = peer_at(index);
829 index = index.saturating_add(1);
830 peer
831 }
832 });
833 #[derive(Clone, Copy)]
834 struct View<'a> {
835 name: &'a str,
836 endpoints: &'a [DataEndpoint],
837 sources: &'a [String],
838 connections: &'a [String],
839 backlink: bool,
840 }
841 let node_size = core::mem::size_of::<TopologyBoardNode>();
842 let string_size = core::mem::size_of::<String>();
843 let mut nodes = 1usize;
844 let mut graph = node_size
845 .saturating_add(sender.len().saturating_mul(2))
846 .saturating_add(local_endpoints.saturating_add(1).saturating_mul(4))
847 .saturating_add(string_size);
848 let mut wire = 24usize
849 .saturating_add(sender.len().saturating_mul(2))
850 .saturating_add(local_endpoints.saturating_add(1).saturating_mul(4));
851 for (index, (name, _, _, _)) in peers.clone().enumerate() {
852 if name != sender && !peers.clone().take(index).any(|(old, _, _, _)| old == name) {
853 graph = graph.saturating_add(string_size).saturating_add(name.len());
854 wire = wire.saturating_add(4).saturating_add(name.len());
855 }
856 }
857 let views = peers.flat_map(|(name, endpoints, sources, boards)| {
860 core::iter::once(View {
861 name,
862 endpoints,
863 sources,
864 connections: &[],
865 backlink: true,
866 })
867 .filter(move |_| boards.is_empty())
868 .chain(boards.iter().map(move |board| View {
869 name: &board.sender_id,
870 endpoints: &board.reachable_endpoints,
871 sources: &board.reachable_timesync_sources,
872 connections: &board.connections,
873 backlink: board.sender_id == name,
874 }))
875 });
876 for (index, node) in views.clone().enumerate() {
877 let prior = || {
878 views
879 .clone()
880 .take(index)
881 .filter(|old| old.name == node.name)
882 };
883 if node.name != sender && prior().next().is_none() {
884 nodes = nodes.saturating_add(1);
885 graph = graph
886 .saturating_add(node_size)
887 .saturating_add(node.name.len());
888 wire = wire.saturating_add(20).saturating_add(node.name.len());
889 }
890 for endpoint in node.endpoints {
891 if !prior().any(|old| old.endpoints.contains(endpoint)) {
892 graph = graph.saturating_add(4);
893 wire = wire.saturating_add(4);
894 }
895 }
896 for source in node.sources {
897 if !prior().any(|old| old.sources.contains(source)) {
898 graph = graph
899 .saturating_add(string_size)
900 .saturating_add(source.len());
901 wire = wire.saturating_add(4).saturating_add(source.len());
902 }
903 }
904 for connection in node.connections {
905 if connection != node.name
906 && !prior().any(|old| {
907 old.connections.contains(connection) || (old.backlink && connection == sender)
908 })
909 {
910 graph = graph
911 .saturating_add(string_size)
912 .saturating_add(connection.len());
913 wire = wire.saturating_add(4).saturating_add(connection.len());
914 }
915 }
916 if node.backlink
917 && node.name != sender
918 && !node.connections.iter().any(|value| value == sender)
919 && !prior()
920 .any(|old| old.backlink || old.connections.iter().any(|value| value == sender))
921 {
922 graph = graph
923 .saturating_add(string_size)
924 .saturating_add(sender.len());
925 wire = wire.saturating_add(4).saturating_add(sender.len());
926 }
927 }
928 let grow = |bytes: usize| bytes.checked_next_power_of_two().unwrap_or(usize::MAX);
929 let largest = 1024usize
930 .max(grow(nodes).saturating_mul(node_size))
931 .max(if include_wire { grow(wire) } else { 0 });
932 let additional = 2048usize
936 .saturating_add(graph.saturating_mul(graph_copies))
937 .saturating_add(variables.saturating_mul(32));
938 (additional, largest)
939}
940
941#[inline(never)]
945fn topology_payload(
946 boards: &[TopologyBoardNode],
947 removed: Option<&[String]>,
948) -> TelemetryResult<Vec<u8>> {
949 let canonical = boards
950 .windows(2)
951 .all(|pair| pair[0].sender_id < pair[1].sender_id)
952 && boards.iter().all(|board| {
953 board
954 .reachable_endpoints
955 .iter()
956 .all(|ep| !is_router_control_endpoint(*ep))
957 && board
958 .reachable_endpoints
959 .windows(2)
960 .all(|pair| pair[0] < pair[1])
961 && board
962 .reachable_timesync_sources
963 .windows(2)
964 .all(|pair| pair[0] < pair[1])
965 && board
966 .connections
967 .iter()
968 .all(|peer| peer != &board.sender_id)
969 && board.connections.windows(2).all(|pair| pair[0] < pair[1])
970 });
971 let mut owned = Vec::new();
972 let normalized = if canonical {
973 boards
974 } else {
975 owned.extend_from_slice(boards);
976 normalize_topology_boards(&mut owned);
977 &owned
978 };
979 let board_count = normalized.len();
980 let mut length = 4usize;
981 let mut add = |bytes: usize| {
982 length = length.saturating_add(bytes);
983 };
984 for board in normalized {
985 add(16);
986 add(board.sender_id.len());
987 add(board.reachable_endpoints.len().saturating_mul(4));
988 for value in board
989 .reachable_timesync_sources
990 .iter()
991 .chain(&board.connections)
992 {
993 add(4);
994 add(value.len());
995 }
996 }
997 if let Some(removed) = removed {
998 add(8);
999 for value in removed {
1000 add(4);
1001 add(value.len());
1002 }
1003 }
1004 let mut payload = Vec::new();
1005 if canonical {
1006 payload
1008 .try_reserve_exact(length)
1009 .map_err(|_| TelemetryError::Io("memory pressure"))?;
1010 } else {
1011 owned.reverse();
1014 }
1015 if removed.is_some() {
1016 payload.extend_from_slice(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes());
1017 }
1018 payload.extend_from_slice(&(board_count as u32).to_le_bytes());
1019 for original in boards.iter().take(board_count) {
1020 let owned_board = owned.pop();
1021 let board = owned_board.as_ref().unwrap_or(original);
1022 encode_string(&mut payload, &board.sender_id)?;
1023
1024 payload.extend_from_slice(&(board.reachable_endpoints.len() as u32).to_le_bytes());
1025 for ep in &board.reachable_endpoints {
1026 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1027 }
1028
1029 payload.extend_from_slice(&(board.reachable_timesync_sources.len() as u32).to_le_bytes());
1030 for source in &board.reachable_timesync_sources {
1031 encode_string(&mut payload, source)?;
1032 }
1033
1034 payload.extend_from_slice(&(board.connections.len() as u32).to_le_bytes());
1035 for peer in &board.connections {
1036 encode_string(&mut payload, peer)?;
1037 }
1038 }
1039
1040 if let Some(removed) = removed {
1041 payload.extend_from_slice(&(removed.len() as u32).to_le_bytes());
1042 for sender_id in removed {
1043 encode_string(&mut payload, sender_id)?;
1044 }
1045 }
1046 debug_assert_eq!(payload.len(), length);
1047 Ok(payload)
1048}
1049
1050pub fn build_discovery_topology(
1052 sender: &str,
1053 timestamp_ms: u64,
1054 boards: &[TopologyBoardNode],
1055) -> TelemetryResult<Packet> {
1056 let payload = topology_payload(boards, None)?;
1057
1058 Packet::new(
1059 DataType::DiscoveryTopology,
1060 &[DataEndpoint::Discovery],
1061 sender,
1062 timestamp_ms,
1063 payload.into(),
1064 )
1065}
1066
1067#[derive(Debug, Clone, PartialEq, Eq)]
1070pub struct DiscoveryTopologyUpdate {
1071 pub incremental: bool,
1072 pub boards: Vec<TopologyBoardNode>,
1073 pub removed: Vec<String>,
1074}
1075
1076pub fn apply_topology_update(
1079 retained: &mut Vec<TopologyBoardNode>,
1080 update: DiscoveryTopologyUpdate,
1081) -> bool {
1082 if !update.incremental {
1083 if *retained == update.boards {
1084 return false;
1085 }
1086 *retained = update.boards;
1087 return true;
1088 }
1089 let old_len = retained.len();
1090 retained.retain(|board| {
1091 !update.removed.iter().any(|id| id == &board.sender_id)
1092 || update
1093 .boards
1094 .iter()
1095 .any(|replacement| replacement.sender_id == board.sender_id)
1096 });
1097 let mut changed = old_len != retained.len();
1098 for board in update.boards {
1099 if let Some(old) = retained
1100 .iter_mut()
1101 .find(|old| old.sender_id == board.sender_id)
1102 {
1103 if *old != board {
1104 *old = board;
1105 changed = true;
1106 }
1107 } else {
1108 retained.push(board);
1109 changed = true;
1110 }
1111 }
1112 if changed {
1113 normalize_topology_boards(retained);
1114 }
1115 changed
1116}
1117
1118pub fn topology_delta(
1120 previous: &[TopologyBoardNode],
1121 current: &[TopologyBoardNode],
1122) -> (Vec<TopologyBoardNode>, Vec<String>) {
1123 let boards = current
1124 .iter()
1125 .filter(|node| previous.iter().find(|old| old.sender_id == node.sender_id) != Some(*node))
1126 .cloned()
1127 .collect();
1128 let removed = previous
1129 .iter()
1130 .filter(|old| !current.iter().any(|node| node.sender_id == old.sender_id))
1131 .map(|old| old.sender_id.clone())
1132 .collect();
1133 (boards, removed)
1134}
1135
1136pub fn build_discovery_topology_delta(
1138 sender: &str,
1139 timestamp_ms: u64,
1140 boards: &[TopologyBoardNode],
1141 removed: &[String],
1142) -> TelemetryResult<Packet> {
1143 let payload = topology_payload(boards, Some(removed))?;
1144 Packet::new(
1145 DataType::DiscoveryTopology,
1146 &[DataEndpoint::Discovery],
1147 sender,
1148 timestamp_ms,
1149 payload.into(),
1150 )
1151}
1152
1153fn decode_string(
1154 payload: &[u8],
1155 cursor: &mut usize,
1156 label: &'static str,
1157) -> TelemetryResult<String> {
1158 if payload.len().saturating_sub(*cursor) < 4 {
1159 return Err(TelemetryError::Unpack(label));
1160 }
1161 let len = u32::from_le_bytes(
1162 payload[*cursor..*cursor + 4]
1163 .try_into()
1164 .expect("4-byte len"),
1165 ) as usize;
1166 *cursor += 4;
1167 if payload.len().saturating_sub(*cursor) < len {
1168 return Err(TelemetryError::Unpack(label));
1169 }
1170 let raw = &payload[*cursor..*cursor + len];
1171 *cursor += len;
1172 core::str::from_utf8(raw)
1173 .map(|s| s.to_string())
1174 .map_err(|_| TelemetryError::Unpack(label))
1175}
1176
1177pub fn decode_discovery_topology(pkt: &Packet) -> TelemetryResult<Vec<TopologyBoardNode>> {
1179 if pkt.data_type() != DataType::DiscoveryTopology {
1180 return Err(TelemetryError::InvalidType);
1181 }
1182 Ok(decode_discovery_topology_update(pkt)?.boards)
1183}
1184
1185pub fn decode_discovery_topology_update(pkt: &Packet) -> TelemetryResult<DiscoveryTopologyUpdate> {
1187 if pkt.data_type() != DataType::DiscoveryTopology {
1188 return Err(TelemetryError::InvalidType);
1189 }
1190 let payload = pkt.payload();
1191 if payload.len() < 4 {
1192 return Err(TelemetryError::Unpack("discovery topology board count"));
1193 }
1194 let first = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count"));
1195 if first != DISCOVERY_TOPOLOGY_DELTA_MARKER {
1196 return Ok(DiscoveryTopologyUpdate {
1197 incremental: false,
1198 boards: decode_discovery_topology_payload(payload)?,
1199 removed: Vec::new(),
1200 });
1201 }
1202 if payload.len() < 8 {
1203 return Err(TelemetryError::Unpack("discovery topology delta count"));
1204 }
1205 let count = u32::from_le_bytes(payload[4..8].try_into().expect("4-byte count")) as usize;
1206 let mut cursor = 8usize;
1207 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1208 if payload.len().saturating_sub(cursor) < 4 {
1209 return Err(TelemetryError::Unpack("discovery topology removal count"));
1210 }
1211 let removed_count = u32::from_le_bytes(
1212 payload[cursor..cursor + 4]
1213 .try_into()
1214 .expect("4-byte count"),
1215 ) as usize;
1216 cursor += 4;
1217 let mut removed = bounded_wire_vec(
1218 payload,
1219 cursor,
1220 removed_count,
1221 4,
1222 "discovery topology removal count",
1223 )?;
1224 for _ in 0..removed_count {
1225 removed.push(decode_string(
1226 payload,
1227 &mut cursor,
1228 "discovery topology removed sender id",
1229 )?);
1230 }
1231 if cursor != payload.len() {
1232 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1233 }
1234 normalize_topology_boards(&mut boards);
1235 removed.sort_unstable();
1236 removed.dedup();
1237 Ok(DiscoveryTopologyUpdate {
1238 incremental: true,
1239 boards,
1240 removed,
1241 })
1242}
1243
1244pub fn decode_discovery_topology_payload(
1246 payload: &[u8],
1247) -> TelemetryResult<Vec<TopologyBoardNode>> {
1248 if payload.len() < 4 {
1249 return Err(TelemetryError::Unpack("discovery topology board count"));
1250 }
1251
1252 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
1253 let mut cursor = 4usize;
1254 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1255
1256 if cursor != payload.len() {
1257 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1258 }
1259
1260 normalize_topology_boards(&mut boards);
1261 Ok(boards)
1262}
1263
1264fn decode_topology_boards(
1265 payload: &[u8],
1266 cursor: &mut usize,
1267 count: usize,
1268) -> TelemetryResult<Vec<TopologyBoardNode>> {
1269 let mut boards = bounded_wire_vec(
1271 payload,
1272 *cursor,
1273 count,
1274 16,
1275 "discovery topology board count",
1276 )?;
1277
1278 for _ in 0..count {
1279 let sender_id = decode_string(payload, cursor, "discovery topology sender id")?;
1280
1281 if payload.len().saturating_sub(*cursor) < 4 {
1282 return Err(TelemetryError::Unpack("discovery topology endpoint count"));
1283 }
1284 let endpoint_count = u32::from_le_bytes(
1285 payload[*cursor..*cursor + 4]
1286 .try_into()
1287 .expect("4-byte count"),
1288 ) as usize;
1289 *cursor += 4;
1290 let mut reachable_endpoints = bounded_wire_vec(
1291 payload,
1292 *cursor,
1293 endpoint_count,
1294 4,
1295 "discovery topology endpoint count",
1296 )?;
1297 for _ in 0..endpoint_count {
1298 if payload.len().saturating_sub(*cursor) < 4 {
1299 return Err(TelemetryError::Unpack("discovery topology endpoint"));
1300 }
1301 let raw =
1302 u32::from_le_bytes(payload[*cursor..*cursor + 4].try_into().expect("4-byte ep"));
1303 *cursor += 4;
1304 let ep = DataEndpoint(raw);
1305 if !is_discovery_endpoint(ep) {
1306 reachable_endpoints.push(ep);
1307 }
1308 }
1309
1310 if payload.len().saturating_sub(*cursor) < 4 {
1311 return Err(TelemetryError::Unpack(
1312 "discovery topology timesync source count",
1313 ));
1314 }
1315 let source_count = u32::from_le_bytes(
1316 payload[*cursor..*cursor + 4]
1317 .try_into()
1318 .expect("4-byte count"),
1319 ) as usize;
1320 *cursor += 4;
1321 let mut reachable_timesync_sources = bounded_wire_vec(
1322 payload,
1323 *cursor,
1324 source_count,
1325 4,
1326 "discovery topology timesync source count",
1327 )?;
1328 for _ in 0..source_count {
1329 let source = decode_string(payload, cursor, "discovery topology timesync source")?;
1330 if !source.is_empty() {
1331 reachable_timesync_sources.push(source);
1332 }
1333 }
1334
1335 if payload.len().saturating_sub(*cursor) < 4 {
1336 return Err(TelemetryError::Unpack(
1337 "discovery topology connection count",
1338 ));
1339 }
1340 let connection_count = u32::from_le_bytes(
1341 payload[*cursor..*cursor + 4]
1342 .try_into()
1343 .expect("4-byte count"),
1344 ) as usize;
1345 *cursor += 4;
1346 let mut connections = bounded_wire_vec(
1347 payload,
1348 *cursor,
1349 connection_count,
1350 4,
1351 "discovery topology connection count",
1352 )?;
1353 for _ in 0..connection_count {
1354 let peer = decode_string(payload, cursor, "discovery topology connection")?;
1355 if !peer.is_empty() {
1356 connections.push(peer);
1357 }
1358 }
1359
1360 boards.push(TopologyBoardNode {
1361 sender_id,
1362 reachable_endpoints,
1363 reachable_timesync_sources,
1364 connections,
1365 });
1366 }
1367
1368 Ok(boards)
1369}
1370
1371fn encode_string(payload: &mut Vec<u8>, value: &str) -> TelemetryResult<()> {
1372 let len = u32::try_from(value.len())
1373 .map_err(|_| TelemetryError::Pack("discovery schema string too long"))?;
1374 payload.extend_from_slice(&len.to_le_bytes());
1375 payload.extend_from_slice(value.as_bytes());
1376 Ok(())
1377}
1378
1379fn read_u8(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u8> {
1380 if payload.len().saturating_sub(*cursor) < 1 {
1381 return Err(TelemetryError::Unpack(label));
1382 }
1383 let out = payload[*cursor];
1384 *cursor += 1;
1385 Ok(out)
1386}
1387
1388fn read_u32(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u32> {
1389 if payload.len().saturating_sub(*cursor) < 4 {
1390 return Err(TelemetryError::Unpack(label));
1391 }
1392 let out = u32::from_le_bytes(
1393 payload[*cursor..*cursor + 4]
1394 .try_into()
1395 .expect("4-byte u32"),
1396 );
1397 *cursor += 4;
1398 Ok(out)
1399}
1400
1401#[cfg(any(not(feature = "std"), test))]
1402fn skip_string(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<()> {
1403 let len = read_u32(payload, cursor, label)? as usize;
1404 if payload.len().saturating_sub(*cursor) < len {
1405 return Err(TelemetryError::Unpack(label));
1406 }
1407 *cursor += len;
1408 Ok(())
1409}
1410
1411#[cfg(any(not(feature = "std"), test))]
1412fn borrowed_schema_string<'a>(
1413 payload: &'a [u8],
1414 cursor: &mut usize,
1415 label: &'static str,
1416) -> TelemetryResult<&'a str> {
1417 let len = read_u32(payload, cursor, label)? as usize;
1418 let end = cursor
1419 .checked_add(len)
1420 .ok_or(TelemetryError::Unpack(label))?;
1421 let bytes = payload
1422 .get(*cursor..end)
1423 .ok_or(TelemetryError::Unpack(label))?;
1424 *cursor = end;
1425 core::str::from_utf8(bytes).map_err(|_| TelemetryError::Unpack(label))
1426}
1427
1428#[cfg(any(not(feature = "std"), test))]
1433pub(crate) fn discovery_schema_payload_is_fully_known(payload: &[u8]) -> TelemetryResult<bool> {
1434 let mut cursor = 0usize;
1435 if read_u32(payload, &mut cursor, "discovery schema version")? != 3 {
1436 return Ok(false);
1437 }
1438
1439 let endpoint_count =
1440 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1441 if endpoint_count > payload.len().saturating_sub(cursor) / 13 {
1442 return Err(TelemetryError::Unpack("discovery schema endpoint count"));
1443 }
1444 let mut all_known = true;
1445 for _ in 0..endpoint_count {
1446 let id = DataEndpoint(read_u32(
1447 payload,
1448 &mut cursor,
1449 "discovery schema endpoint id",
1450 )?);
1451 let known = crate::config::endpoint_exists(id);
1452 let meta = crate::config::get_endpoint_meta(id);
1453 let link_local_only =
1454 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1455 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema endpoint name")?;
1456 all_known &= known
1457 && AsRef::<str>::as_ref(&meta.name) == name
1458 && (!link_local_only || meta.link_local_only);
1459 skip_string(
1460 payload,
1461 &mut cursor,
1462 "discovery schema endpoint description",
1463 )?;
1464 }
1465
1466 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1467 if type_count > payload.len().saturating_sub(cursor) / 19 {
1468 return Err(TelemetryError::Unpack("discovery schema type count"));
1469 }
1470 for _ in 0..type_count {
1471 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1472 let meta = crate::config::try_get_message_meta(id);
1473 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema type name")?;
1474 skip_string(payload, &mut cursor, "discovery schema type description")?;
1475 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1476 if element_kind > 1 {
1477 return Err(TelemetryError::Unpack("discovery schema element kind"));
1478 }
1479 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1480 let data_type = message_data_type_from_code(read_u8(
1481 payload,
1482 &mut cursor,
1483 "discovery schema data type",
1484 )?)
1485 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1486 let class =
1487 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1488 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1489 let reliable =
1490 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1491 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1492 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1493 let encryption = e2e_encryption_policy_from_code(read_u8(
1494 payload,
1495 &mut cursor,
1496 "discovery schema e2e cryptography",
1497 )?)
1498 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1499 let endpoint_count =
1500 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1501 if endpoint_count > payload.len().saturating_sub(cursor) / 4 {
1502 return Err(TelemetryError::Unpack(
1503 "discovery schema type endpoint count",
1504 ));
1505 }
1506 let element = if element_kind == 0 {
1507 MessageElement::Static(count, data_type, class)
1508 } else {
1509 MessageElement::Dynamic(data_type, class)
1510 };
1511 all_known &= meta.as_ref().is_some_and(|meta| {
1512 AsRef::<str>::as_ref(&meta.name) == name
1513 && meta.element == element
1514 && meta.reliable == reliable
1515 && meta.priority >= priority
1516 && meta.e2e_encryption == encryption
1517 });
1518 for _ in 0..endpoint_count {
1519 let ep = DataEndpoint(read_u32(
1520 payload,
1521 &mut cursor,
1522 "discovery schema type endpoint",
1523 )?);
1524 all_known &= meta
1525 .as_ref()
1526 .is_some_and(|meta| meta.endpoints.contains(&ep));
1527 }
1528 }
1529 if cursor != payload.len() {
1530 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1531 }
1532 Ok(all_known)
1533}
1534
1535pub fn build_discovery_schema(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
1537 #[cfg(feature = "std")]
1538 return build_discovery_schema_from_owned_snapshot(sender, timestamp_ms, export_schema());
1539 #[cfg(not(feature = "std"))]
1540 return Packet::new(
1541 DataType::DiscoverySchema,
1542 &[DataEndpoint::Discovery],
1543 sender,
1544 timestamp_ms,
1545 crate::config::encode_embedded_schema_payload()?,
1546 );
1547}
1548
1549pub fn elect_discovery_master(local_sender: &str, boards: &[TopologyBoardNode]) -> String {
1557 let mut nodes: BTreeMap<String, Vec<String>> = BTreeMap::new();
1558 for board in boards {
1559 nodes.entry(board.sender_id.clone()).or_default();
1560 for peer in board.connections.iter() {
1561 nodes
1562 .entry(peer.clone())
1563 .or_default()
1564 .push(board.sender_id.clone());
1565 nodes
1566 .entry(board.sender_id.clone())
1567 .or_default()
1568 .push(peer.clone());
1569 }
1570 }
1571 nodes.entry(local_sender.to_string()).or_default();
1572 for peers in nodes.values_mut() {
1573 peers.sort_unstable();
1574 peers.dedup();
1575 }
1576
1577 let mut best_sender = local_sender.to_string();
1578 let mut best_unreachable = usize::MAX;
1579 let mut best_max_hops = usize::MAX;
1580 let mut best_total_hops = usize::MAX;
1581
1582 for sender in nodes.keys() {
1583 let mut frontier = vec![sender.clone()];
1584 let mut seen: BTreeMap<String, usize> = BTreeMap::new();
1585 seen.insert(sender.clone(), 0);
1586 let mut idx = 0;
1587 while idx < frontier.len() {
1588 let cur = frontier[idx].clone();
1589 idx += 1;
1590 let cur_dist = seen[&cur];
1591 if let Some(peers) = nodes.get(&cur) {
1592 for peer in peers {
1593 if !seen.contains_key(peer) {
1594 seen.insert(peer.clone(), cur_dist + 1);
1595 frontier.push(peer.clone());
1596 }
1597 }
1598 }
1599 }
1600
1601 let unreachable = nodes.len().saturating_sub(seen.len());
1602 let max_hops = seen.values().copied().max().unwrap_or(0);
1603 let total_hops = seen.values().copied().sum();
1604 let better = unreachable < best_unreachable
1605 || (unreachable == best_unreachable && max_hops < best_max_hops)
1606 || (unreachable == best_unreachable
1607 && max_hops == best_max_hops
1608 && total_hops < best_total_hops)
1609 || (unreachable == best_unreachable
1610 && max_hops == best_max_hops
1611 && total_hops == best_total_hops
1612 && sender < &best_sender);
1613 if better {
1614 best_sender = sender.clone();
1615 best_unreachable = unreachable;
1616 best_max_hops = max_hops;
1617 best_total_hops = total_hops;
1618 }
1619 }
1620
1621 best_sender
1622}
1623
1624pub fn build_discovery_schema_from_snapshot(
1626 sender: &str,
1627 timestamp_ms: u64,
1628 schema: RuntimeSchemaSnapshot,
1629) -> TelemetryResult<Packet> {
1630 let mut payload = Vec::new();
1631 payload.extend_from_slice(&3u32.to_le_bytes());
1632
1633 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1634 for ep in schema.endpoints {
1635 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1636 payload.push(ep.link_local_only as u8);
1637 encode_string(&mut payload, ep.name)?;
1638 #[cfg(feature = "std")]
1639 encode_string(&mut payload, ep.description)?;
1640 #[cfg(not(feature = "std"))]
1641 encode_string(&mut payload, "")?;
1642 }
1643
1644 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1645 for ty in schema.types {
1646 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1647 encode_string(&mut payload, ty.name)?;
1648 #[cfg(feature = "std")]
1649 encode_string(&mut payload, ty.description)?;
1650 #[cfg(not(feature = "std"))]
1651 encode_string(&mut payload, "")?;
1652 match ty.element {
1653 MessageElement::Static(count, data_type, class) => {
1654 payload.push(0);
1655 payload.extend_from_slice(&(count as u32).to_le_bytes());
1656 payload.push(message_data_type_code(data_type));
1657 payload.push(message_class_code(class));
1658 }
1659 MessageElement::Dynamic(data_type, class) => {
1660 payload.push(1);
1661 payload.extend_from_slice(&0u32.to_le_bytes());
1662 payload.push(message_data_type_code(data_type));
1663 payload.push(message_class_code(class));
1664 }
1665 }
1666 payload.push(reliable_code(ty.reliable));
1667 payload.push(ty.priority);
1668 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1669 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1670 for ep in ty.endpoints {
1671 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1672 }
1673 }
1674
1675 Packet::new(
1676 DataType::DiscoverySchema,
1677 &[DataEndpoint::Discovery],
1678 sender,
1679 timestamp_ms,
1680 payload.into(),
1681 )
1682}
1683
1684pub fn build_discovery_schema_from_owned_snapshot(
1686 sender: &str,
1687 timestamp_ms: u64,
1688 schema: OwnedRuntimeSchemaSnapshot,
1689) -> TelemetryResult<Packet> {
1690 let mut payload = Vec::new();
1691 payload.extend_from_slice(&3u32.to_le_bytes());
1692
1693 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1694 for ep in schema.endpoints {
1695 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1696 payload.push(ep.link_local_only as u8);
1697 encode_string(&mut payload, &ep.name)?;
1698 encode_string(&mut payload, "")?;
1702 }
1703
1704 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1705 for ty in schema.types {
1706 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1707 encode_string(&mut payload, &ty.name)?;
1708 encode_string(&mut payload, "")?;
1709 match ty.element {
1710 MessageElement::Static(count, data_type, class) => {
1711 payload.push(0);
1712 payload.extend_from_slice(&(count as u32).to_le_bytes());
1713 payload.push(message_data_type_code(data_type));
1714 payload.push(message_class_code(class));
1715 }
1716 MessageElement::Dynamic(data_type, class) => {
1717 payload.push(1);
1718 payload.extend_from_slice(&0u32.to_le_bytes());
1719 payload.push(message_data_type_code(data_type));
1720 payload.push(message_class_code(class));
1721 }
1722 }
1723 payload.push(reliable_code(ty.reliable));
1724 payload.push(ty.priority);
1725 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1726 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1727 for ep in ty.endpoints {
1728 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1729 }
1730 }
1731
1732 Packet::new(
1733 DataType::DiscoverySchema,
1734 &[DataEndpoint::Discovery],
1735 sender,
1736 timestamp_ms,
1737 payload.into(),
1738 )
1739}
1740
1741pub fn decode_discovery_schema(pkt: &Packet) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1743 if pkt.data_type() != DataType::DiscoverySchema {
1744 return Err(TelemetryError::InvalidType);
1745 }
1746 decode_discovery_schema_payload(pkt.payload())
1747}
1748
1749pub fn decode_discovery_schema_payload(
1751 payload: &[u8],
1752) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1753 let mut cursor = 0usize;
1754 let version = read_u32(payload, &mut cursor, "discovery schema version")?;
1755 #[cfg(feature = "std")]
1756 if version != 1 && version != 2 && version != 3 {
1757 return Err(TelemetryError::Unpack("discovery schema version"));
1758 }
1759 #[cfg(not(feature = "std"))]
1760 if version != 3 {
1761 return Err(TelemetryError::Unpack("discovery schema version"));
1762 }
1763
1764 let endpoint_count =
1765 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1766 let mut endpoints = bounded_wire_vec(
1767 payload,
1768 cursor,
1769 endpoint_count,
1770 13,
1771 "discovery schema endpoint count",
1772 )?;
1773 for _ in 0..endpoint_count {
1774 let id = DataEndpoint(read_u32(
1775 payload,
1776 &mut cursor,
1777 "discovery schema endpoint id",
1778 )?);
1779 let link_local_only =
1780 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1781 let name = decode_string(payload, &mut cursor, "discovery schema endpoint name")?;
1782 #[cfg(feature = "std")]
1783 let description = if version >= 2 {
1784 decode_string(
1785 payload,
1786 &mut cursor,
1787 "discovery schema endpoint description",
1788 )?
1789 } else {
1790 String::new()
1791 };
1792 #[cfg(not(feature = "std"))]
1793 let description = {
1794 skip_string(
1795 payload,
1796 &mut cursor,
1797 "discovery schema endpoint description",
1798 )?;
1799 String::new()
1800 };
1801 endpoints.push(OwnedEndpointDefinition {
1802 id,
1803 name,
1804 description,
1805 link_local_only,
1806 });
1807 }
1808
1809 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1810 let mut types = bounded_wire_vec(
1811 payload,
1812 cursor,
1813 type_count,
1814 19,
1815 "discovery schema type count",
1816 )?;
1817 for _ in 0..type_count {
1818 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1819 let name = decode_string(payload, &mut cursor, "discovery schema type name")?;
1820 #[cfg(feature = "std")]
1821 let description = if version >= 2 {
1822 decode_string(payload, &mut cursor, "discovery schema type description")?
1823 } else {
1824 String::new()
1825 };
1826 #[cfg(not(feature = "std"))]
1827 let description = {
1828 skip_string(payload, &mut cursor, "discovery schema type description")?;
1829 String::new()
1830 };
1831 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1832 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1833 let data_type = message_data_type_from_code(read_u8(
1834 payload,
1835 &mut cursor,
1836 "discovery schema data type",
1837 )?)
1838 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1839 let class =
1840 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1841 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1842 let element = match element_kind {
1843 0 => MessageElement::Static(count, data_type, class),
1844 1 => MessageElement::Dynamic(data_type, class),
1845 _ => return Err(TelemetryError::Unpack("discovery schema element kind")),
1846 };
1847 let reliable =
1848 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1849 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1850 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1851 #[cfg(feature = "std")]
1852 let e2e_encryption = if version >= 3 {
1853 e2e_encryption_policy_from_code(read_u8(
1854 payload,
1855 &mut cursor,
1856 "discovery schema e2e cryptography",
1857 )?)
1858 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?
1859 } else {
1860 E2eEncryptionPolicy::PreferOff
1861 };
1862 #[cfg(not(feature = "std"))]
1863 let e2e_encryption = e2e_encryption_policy_from_code(read_u8(
1864 payload,
1865 &mut cursor,
1866 "discovery schema e2e cryptography",
1867 )?)
1868 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1869 let endpoint_count =
1870 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1871 let mut type_endpoints = bounded_wire_vec(
1872 payload,
1873 cursor,
1874 endpoint_count,
1875 4,
1876 "discovery schema type endpoint count",
1877 )?;
1878 for _ in 0..endpoint_count {
1879 type_endpoints.push(DataEndpoint(read_u32(
1880 payload,
1881 &mut cursor,
1882 "discovery schema type endpoint",
1883 )?));
1884 }
1885 types.push(OwnedDataTypeDefinition {
1886 id,
1887 name,
1888 description,
1889 element,
1890 endpoints: type_endpoints,
1891 reliable,
1892 priority,
1893 e2e_encryption,
1894 });
1895 }
1896
1897 if cursor != payload.len() {
1898 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1899 }
1900 Ok(OwnedRuntimeSchemaSnapshot { endpoints, types })
1901}
1902
1903pub(crate) fn same_schema_snapshot(existing: &Packet, incoming: &Packet) -> bool {
1907 existing.data_type() == DataType::DiscoverySchema
1908 && incoming.data_type() == DataType::DiscoverySchema
1909 && existing.sender() == incoming.sender()
1910 && existing.endpoints() == incoming.endpoints()
1911 && existing.wire_target_senders() == incoming.wire_target_senders()
1912 && existing.payload() == incoming.payload()
1913}
1914
1915#[cfg(test)]
1916mod bounded_decode_tests {
1917 use super::{
1918 ADDRESS_MODE_STATIC, ADDRESS_STATE_APPROVED, AddressAdvertisement, LinkCapabilities,
1919 build_discovery_address, build_managed_variable_request, decode_discovery_address,
1920 decode_discovery_schema_payload, decode_discovery_timesync_sources_payload,
1921 decode_discovery_topology_payload, decode_managed_variable_request,
1922 discovery_schema_payload_is_fully_known,
1923 };
1924 use crate::{DataEndpoint, DataType};
1925
1926 #[test]
1927 fn equivalent_schema_definitions_are_detected_without_owned_decode() {
1928 fn one_endpoint(id: DataEndpoint) -> Vec<u8> {
1929 let mut payload = Vec::new();
1930 payload.extend_from_slice(&3u32.to_le_bytes());
1931 payload.extend_from_slice(&1u32.to_le_bytes());
1932 payload.extend_from_slice(&id.0.to_le_bytes());
1933 payload.push(0);
1934 let meta = crate::config::get_endpoint_meta(id);
1935 payload.extend_from_slice(&(meta.name.len() as u32).to_le_bytes());
1936 payload.extend_from_slice(meta.name.as_bytes());
1937 payload.extend_from_slice(&0u32.to_le_bytes());
1938 payload.extend_from_slice(&0u32.to_le_bytes());
1939 payload
1940 }
1941
1942 assert!(
1943 discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint::Discovery,))
1944 .unwrap()
1945 );
1946 assert!(
1947 !discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint(u32::MAX - 7),))
1948 .unwrap()
1949 );
1950 }
1951
1952 #[test]
1953 fn reused_ids_with_changed_schema_are_not_treated_as_known() {
1954 let schema = crate::config::export_schema();
1955 let original =
1956 super::build_discovery_schema_from_owned_snapshot("test", 1, schema.clone()).unwrap();
1957 assert!(discovery_schema_payload_is_fully_known(original.payload()).unwrap());
1958 for change in 0..4 {
1959 let mut updated = schema.clone();
1960 match change {
1961 0 => updated.endpoints[0].name.push_str("_CHANGED"),
1962 1 => updated.endpoints[0].link_local_only = !updated.endpoints[0].link_local_only,
1963 2 => updated.types[0].name.push_str("_CHANGED"),
1964 _ => updated.types[0].endpoints.push(DataEndpoint(u32::MAX - 7)),
1965 }
1966 let packet =
1967 super::build_discovery_schema_from_owned_snapshot("test", 2, updated).unwrap();
1968 assert!(
1969 !discovery_schema_payload_is_fully_known(packet.payload()).unwrap(),
1970 "change {change} was silently ignored"
1971 );
1972 }
1973 }
1974
1975 #[test]
1976 fn unknown_managed_variable_ids_survive_discovery_before_schema_merge() {
1977 let unknown = DataType(4090);
1978 let unknown_endpoint = DataEndpoint(4090);
1979 let advertisement = AddressAdvertisement {
1980 hostname: "flight".into(),
1981 address: 42,
1982 requested_address: 42,
1983 mode: ADDRESS_MODE_STATIC,
1984 state: ADDRESS_STATE_APPROVED,
1985 birth_ms: 10,
1986 owner_hash: 20,
1987 reachable_endpoints: vec![unknown_endpoint],
1988 reachable_network_variables: vec![unknown],
1989 reachable_timesync_sources: Vec::new(),
1990 link_capabilities: LinkCapabilities {
1991 version: 1,
1992 flags: 0,
1993 profile: 0,
1994 max_frame_bytes: 128,
1995 compact_header_target_bytes: 40,
1996 max_side_transport_templates: 4,
1997 },
1998 };
1999 let packet = build_discovery_address("FC", 1, &advertisement).unwrap();
2000 let decoded = decode_discovery_address(&packet).unwrap();
2001 assert_eq!(decoded.reachable_endpoints, vec![unknown_endpoint]);
2002 assert_eq!(decoded.reachable_network_variables, vec![unknown]);
2003
2004 let request = build_managed_variable_request("GS", 2, unknown).unwrap();
2005 assert_eq!(decode_managed_variable_request(&request).unwrap(), unknown);
2006 }
2007
2008 #[test]
2009 fn corrupt_timesync_count_is_rejected_without_reserving() {
2010 assert!(decode_discovery_timesync_sources_payload(&u32::MAX.to_le_bytes()).is_err());
2011 }
2012
2013 #[test]
2014 fn corrupt_topology_count_is_rejected_without_reserving() {
2015 assert!(decode_discovery_topology_payload(&u32::MAX.to_le_bytes()).is_err());
2016 }
2017
2018 #[test]
2019 fn corrupt_schema_counts_are_rejected_without_reserving() {
2020 let mut endpoints = 3u32.to_le_bytes().to_vec();
2021 endpoints.extend_from_slice(&u32::MAX.to_le_bytes());
2022 assert!(decode_discovery_schema_payload(&endpoints).is_err());
2023
2024 let mut types = 3u32.to_le_bytes().to_vec();
2025 types.extend_from_slice(&0u32.to_le_bytes());
2026 types.extend_from_slice(&u32::MAX.to_le_bytes());
2027 assert!(decode_discovery_schema_payload(&types).is_err());
2028 }
2029}