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
941pub fn build_discovery_topology(
943 sender: &str,
944 timestamp_ms: u64,
945 boards: &[TopologyBoardNode],
946) -> TelemetryResult<Packet> {
947 let mut payload = Vec::new();
948 let mut normalized = boards.to_vec();
949 normalize_topology_boards(&mut normalized);
950
951 payload.extend_from_slice(&(normalized.len() as u32).to_le_bytes());
952 for board in normalized {
953 let sender_bytes = board.sender_id.as_bytes();
954 let sender_len = u32::try_from(sender_bytes.len())
955 .map_err(|_| TelemetryError::Pack("discovery topology sender id too long"))?;
956 payload.extend_from_slice(&sender_len.to_le_bytes());
957 payload.extend_from_slice(sender_bytes);
958
959 payload.extend_from_slice(&(board.reachable_endpoints.len() as u32).to_le_bytes());
960 for ep in board.reachable_endpoints {
961 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
962 }
963
964 payload.extend_from_slice(&(board.reachable_timesync_sources.len() as u32).to_le_bytes());
965 for source in board.reachable_timesync_sources {
966 let bytes = source.as_bytes();
967 let len = u32::try_from(bytes.len())
968 .map_err(|_| TelemetryError::Pack("discovery topology source id too long"))?;
969 payload.extend_from_slice(&len.to_le_bytes());
970 payload.extend_from_slice(bytes);
971 }
972
973 payload.extend_from_slice(&(board.connections.len() as u32).to_le_bytes());
974 for peer in board.connections {
975 let bytes = peer.as_bytes();
976 let len = u32::try_from(bytes.len())
977 .map_err(|_| TelemetryError::Pack("discovery topology connection id too long"))?;
978 payload.extend_from_slice(&len.to_le_bytes());
979 payload.extend_from_slice(bytes);
980 }
981 }
982
983 Packet::new(
984 DataType::DiscoveryTopology,
985 &[DataEndpoint::Discovery],
986 sender,
987 timestamp_ms,
988 payload.into(),
989 )
990}
991
992#[derive(Debug, Clone, PartialEq, Eq)]
995pub struct DiscoveryTopologyUpdate {
996 pub incremental: bool,
997 pub boards: Vec<TopologyBoardNode>,
998 pub removed: Vec<String>,
999}
1000
1001pub fn apply_topology_update(
1004 retained: &mut Vec<TopologyBoardNode>,
1005 update: DiscoveryTopologyUpdate,
1006) -> bool {
1007 if !update.incremental {
1008 if *retained == update.boards {
1009 return false;
1010 }
1011 *retained = update.boards;
1012 return true;
1013 }
1014 let old_len = retained.len();
1015 retained.retain(|board| {
1016 !update.removed.iter().any(|id| id == &board.sender_id)
1017 || update
1018 .boards
1019 .iter()
1020 .any(|replacement| replacement.sender_id == board.sender_id)
1021 });
1022 let mut changed = old_len != retained.len();
1023 for board in update.boards {
1024 if let Some(old) = retained
1025 .iter_mut()
1026 .find(|old| old.sender_id == board.sender_id)
1027 {
1028 if *old != board {
1029 *old = board;
1030 changed = true;
1031 }
1032 } else {
1033 retained.push(board);
1034 changed = true;
1035 }
1036 }
1037 if changed {
1038 normalize_topology_boards(retained);
1039 }
1040 changed
1041}
1042
1043pub fn topology_delta(
1045 previous: &[TopologyBoardNode],
1046 current: &[TopologyBoardNode],
1047) -> (Vec<TopologyBoardNode>, Vec<String>) {
1048 let boards = current
1049 .iter()
1050 .filter(|node| previous.iter().find(|old| old.sender_id == node.sender_id) != Some(*node))
1051 .cloned()
1052 .collect();
1053 let removed = previous
1054 .iter()
1055 .filter(|old| !current.iter().any(|node| node.sender_id == old.sender_id))
1056 .map(|old| old.sender_id.clone())
1057 .collect();
1058 (boards, removed)
1059}
1060
1061pub fn build_discovery_topology_delta(
1063 sender: &str,
1064 timestamp_ms: u64,
1065 boards: &[TopologyBoardNode],
1066 removed: &[String],
1067) -> TelemetryResult<Packet> {
1068 let full = build_discovery_topology(sender, timestamp_ms, boards)?;
1069 let mut payload = Vec::with_capacity(full.payload().len() + 8 + removed.len() * 8);
1070 payload.extend_from_slice(&DISCOVERY_TOPOLOGY_DELTA_MARKER.to_le_bytes());
1071 payload.extend_from_slice(full.payload());
1072 payload.extend_from_slice(&(removed.len() as u32).to_le_bytes());
1073 for sender_id in removed {
1074 encode_string(&mut payload, sender_id)?;
1075 }
1076 Packet::new(
1077 DataType::DiscoveryTopology,
1078 &[DataEndpoint::Discovery],
1079 sender,
1080 timestamp_ms,
1081 payload.into(),
1082 )
1083}
1084
1085fn decode_string(
1086 payload: &[u8],
1087 cursor: &mut usize,
1088 label: &'static str,
1089) -> TelemetryResult<String> {
1090 if payload.len().saturating_sub(*cursor) < 4 {
1091 return Err(TelemetryError::Unpack(label));
1092 }
1093 let len = u32::from_le_bytes(
1094 payload[*cursor..*cursor + 4]
1095 .try_into()
1096 .expect("4-byte len"),
1097 ) as usize;
1098 *cursor += 4;
1099 if payload.len().saturating_sub(*cursor) < len {
1100 return Err(TelemetryError::Unpack(label));
1101 }
1102 let raw = &payload[*cursor..*cursor + len];
1103 *cursor += len;
1104 core::str::from_utf8(raw)
1105 .map(|s| s.to_string())
1106 .map_err(|_| TelemetryError::Unpack(label))
1107}
1108
1109pub fn decode_discovery_topology(pkt: &Packet) -> TelemetryResult<Vec<TopologyBoardNode>> {
1111 if pkt.data_type() != DataType::DiscoveryTopology {
1112 return Err(TelemetryError::InvalidType);
1113 }
1114 Ok(decode_discovery_topology_update(pkt)?.boards)
1115}
1116
1117pub fn decode_discovery_topology_update(pkt: &Packet) -> TelemetryResult<DiscoveryTopologyUpdate> {
1119 if pkt.data_type() != DataType::DiscoveryTopology {
1120 return Err(TelemetryError::InvalidType);
1121 }
1122 let payload = pkt.payload();
1123 if payload.len() < 4 {
1124 return Err(TelemetryError::Unpack("discovery topology board count"));
1125 }
1126 let first = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count"));
1127 if first != DISCOVERY_TOPOLOGY_DELTA_MARKER {
1128 return Ok(DiscoveryTopologyUpdate {
1129 incremental: false,
1130 boards: decode_discovery_topology_payload(payload)?,
1131 removed: Vec::new(),
1132 });
1133 }
1134 if payload.len() < 8 {
1135 return Err(TelemetryError::Unpack("discovery topology delta count"));
1136 }
1137 let count = u32::from_le_bytes(payload[4..8].try_into().expect("4-byte count")) as usize;
1138 let mut cursor = 8usize;
1139 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1140 if payload.len().saturating_sub(cursor) < 4 {
1141 return Err(TelemetryError::Unpack("discovery topology removal count"));
1142 }
1143 let removed_count = u32::from_le_bytes(
1144 payload[cursor..cursor + 4]
1145 .try_into()
1146 .expect("4-byte count"),
1147 ) as usize;
1148 cursor += 4;
1149 let mut removed = bounded_wire_vec(
1150 payload,
1151 cursor,
1152 removed_count,
1153 4,
1154 "discovery topology removal count",
1155 )?;
1156 for _ in 0..removed_count {
1157 removed.push(decode_string(
1158 payload,
1159 &mut cursor,
1160 "discovery topology removed sender id",
1161 )?);
1162 }
1163 if cursor != payload.len() {
1164 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1165 }
1166 normalize_topology_boards(&mut boards);
1167 removed.sort_unstable();
1168 removed.dedup();
1169 Ok(DiscoveryTopologyUpdate {
1170 incremental: true,
1171 boards,
1172 removed,
1173 })
1174}
1175
1176pub fn decode_discovery_topology_payload(
1178 payload: &[u8],
1179) -> TelemetryResult<Vec<TopologyBoardNode>> {
1180 if payload.len() < 4 {
1181 return Err(TelemetryError::Unpack("discovery topology board count"));
1182 }
1183
1184 let count = u32::from_le_bytes(payload[..4].try_into().expect("4-byte count")) as usize;
1185 let mut cursor = 4usize;
1186 let mut boards = decode_topology_boards(payload, &mut cursor, count)?;
1187
1188 if cursor != payload.len() {
1189 return Err(TelemetryError::Unpack("discovery topology trailing bytes"));
1190 }
1191
1192 normalize_topology_boards(&mut boards);
1193 Ok(boards)
1194}
1195
1196fn decode_topology_boards(
1197 payload: &[u8],
1198 cursor: &mut usize,
1199 count: usize,
1200) -> TelemetryResult<Vec<TopologyBoardNode>> {
1201 let mut boards = bounded_wire_vec(
1203 payload,
1204 *cursor,
1205 count,
1206 16,
1207 "discovery topology board count",
1208 )?;
1209
1210 for _ in 0..count {
1211 let sender_id = decode_string(payload, cursor, "discovery topology sender id")?;
1212
1213 if payload.len().saturating_sub(*cursor) < 4 {
1214 return Err(TelemetryError::Unpack("discovery topology endpoint count"));
1215 }
1216 let endpoint_count = u32::from_le_bytes(
1217 payload[*cursor..*cursor + 4]
1218 .try_into()
1219 .expect("4-byte count"),
1220 ) as usize;
1221 *cursor += 4;
1222 let mut reachable_endpoints = bounded_wire_vec(
1223 payload,
1224 *cursor,
1225 endpoint_count,
1226 4,
1227 "discovery topology endpoint count",
1228 )?;
1229 for _ in 0..endpoint_count {
1230 if payload.len().saturating_sub(*cursor) < 4 {
1231 return Err(TelemetryError::Unpack("discovery topology endpoint"));
1232 }
1233 let raw =
1234 u32::from_le_bytes(payload[*cursor..*cursor + 4].try_into().expect("4-byte ep"));
1235 *cursor += 4;
1236 let ep = DataEndpoint(raw);
1237 if !is_discovery_endpoint(ep) {
1238 reachable_endpoints.push(ep);
1239 }
1240 }
1241
1242 if payload.len().saturating_sub(*cursor) < 4 {
1243 return Err(TelemetryError::Unpack(
1244 "discovery topology timesync source count",
1245 ));
1246 }
1247 let source_count = u32::from_le_bytes(
1248 payload[*cursor..*cursor + 4]
1249 .try_into()
1250 .expect("4-byte count"),
1251 ) as usize;
1252 *cursor += 4;
1253 let mut reachable_timesync_sources = bounded_wire_vec(
1254 payload,
1255 *cursor,
1256 source_count,
1257 4,
1258 "discovery topology timesync source count",
1259 )?;
1260 for _ in 0..source_count {
1261 let source = decode_string(payload, cursor, "discovery topology timesync source")?;
1262 if !source.is_empty() {
1263 reachable_timesync_sources.push(source);
1264 }
1265 }
1266
1267 if payload.len().saturating_sub(*cursor) < 4 {
1268 return Err(TelemetryError::Unpack(
1269 "discovery topology connection count",
1270 ));
1271 }
1272 let connection_count = u32::from_le_bytes(
1273 payload[*cursor..*cursor + 4]
1274 .try_into()
1275 .expect("4-byte count"),
1276 ) as usize;
1277 *cursor += 4;
1278 let mut connections = bounded_wire_vec(
1279 payload,
1280 *cursor,
1281 connection_count,
1282 4,
1283 "discovery topology connection count",
1284 )?;
1285 for _ in 0..connection_count {
1286 let peer = decode_string(payload, cursor, "discovery topology connection")?;
1287 if !peer.is_empty() {
1288 connections.push(peer);
1289 }
1290 }
1291
1292 boards.push(TopologyBoardNode {
1293 sender_id,
1294 reachable_endpoints,
1295 reachable_timesync_sources,
1296 connections,
1297 });
1298 }
1299
1300 Ok(boards)
1301}
1302
1303fn encode_string(payload: &mut Vec<u8>, value: &str) -> TelemetryResult<()> {
1304 let len = u32::try_from(value.len())
1305 .map_err(|_| TelemetryError::Pack("discovery schema string too long"))?;
1306 payload.extend_from_slice(&len.to_le_bytes());
1307 payload.extend_from_slice(value.as_bytes());
1308 Ok(())
1309}
1310
1311fn read_u8(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u8> {
1312 if payload.len().saturating_sub(*cursor) < 1 {
1313 return Err(TelemetryError::Unpack(label));
1314 }
1315 let out = payload[*cursor];
1316 *cursor += 1;
1317 Ok(out)
1318}
1319
1320fn read_u32(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<u32> {
1321 if payload.len().saturating_sub(*cursor) < 4 {
1322 return Err(TelemetryError::Unpack(label));
1323 }
1324 let out = u32::from_le_bytes(
1325 payload[*cursor..*cursor + 4]
1326 .try_into()
1327 .expect("4-byte u32"),
1328 );
1329 *cursor += 4;
1330 Ok(out)
1331}
1332
1333#[cfg(any(not(feature = "std"), test))]
1334fn skip_string(payload: &[u8], cursor: &mut usize, label: &'static str) -> TelemetryResult<()> {
1335 let len = read_u32(payload, cursor, label)? as usize;
1336 if payload.len().saturating_sub(*cursor) < len {
1337 return Err(TelemetryError::Unpack(label));
1338 }
1339 *cursor += len;
1340 Ok(())
1341}
1342
1343#[cfg(any(not(feature = "std"), test))]
1344fn borrowed_schema_string<'a>(
1345 payload: &'a [u8],
1346 cursor: &mut usize,
1347 label: &'static str,
1348) -> TelemetryResult<&'a str> {
1349 let len = read_u32(payload, cursor, label)? as usize;
1350 let end = cursor
1351 .checked_add(len)
1352 .ok_or(TelemetryError::Unpack(label))?;
1353 let bytes = payload
1354 .get(*cursor..end)
1355 .ok_or(TelemetryError::Unpack(label))?;
1356 *cursor = end;
1357 core::str::from_utf8(bytes).map_err(|_| TelemetryError::Unpack(label))
1358}
1359
1360#[cfg(any(not(feature = "std"), test))]
1365pub(crate) fn discovery_schema_payload_is_fully_known(payload: &[u8]) -> TelemetryResult<bool> {
1366 let mut cursor = 0usize;
1367 if read_u32(payload, &mut cursor, "discovery schema version")? != 3 {
1368 return Ok(false);
1369 }
1370
1371 let endpoint_count =
1372 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1373 if endpoint_count > payload.len().saturating_sub(cursor) / 13 {
1374 return Err(TelemetryError::Unpack("discovery schema endpoint count"));
1375 }
1376 let mut all_known = true;
1377 for _ in 0..endpoint_count {
1378 let id = DataEndpoint(read_u32(
1379 payload,
1380 &mut cursor,
1381 "discovery schema endpoint id",
1382 )?);
1383 let known = crate::config::endpoint_exists(id);
1384 let meta = crate::config::get_endpoint_meta(id);
1385 let link_local_only =
1386 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1387 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema endpoint name")?;
1388 all_known &= known
1389 && AsRef::<str>::as_ref(&meta.name) == name
1390 && (!link_local_only || meta.link_local_only);
1391 skip_string(
1392 payload,
1393 &mut cursor,
1394 "discovery schema endpoint description",
1395 )?;
1396 }
1397
1398 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1399 if type_count > payload.len().saturating_sub(cursor) / 19 {
1400 return Err(TelemetryError::Unpack("discovery schema type count"));
1401 }
1402 for _ in 0..type_count {
1403 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1404 let meta = crate::config::try_get_message_meta(id);
1405 let name = borrowed_schema_string(payload, &mut cursor, "discovery schema type name")?;
1406 skip_string(payload, &mut cursor, "discovery schema type description")?;
1407 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1408 if element_kind > 1 {
1409 return Err(TelemetryError::Unpack("discovery schema element kind"));
1410 }
1411 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1412 let data_type = message_data_type_from_code(read_u8(
1413 payload,
1414 &mut cursor,
1415 "discovery schema data type",
1416 )?)
1417 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1418 let class =
1419 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1420 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1421 let reliable =
1422 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1423 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1424 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1425 let encryption = e2e_encryption_policy_from_code(read_u8(
1426 payload,
1427 &mut cursor,
1428 "discovery schema e2e cryptography",
1429 )?)
1430 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1431 let endpoint_count =
1432 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1433 if endpoint_count > payload.len().saturating_sub(cursor) / 4 {
1434 return Err(TelemetryError::Unpack(
1435 "discovery schema type endpoint count",
1436 ));
1437 }
1438 let element = if element_kind == 0 {
1439 MessageElement::Static(count, data_type, class)
1440 } else {
1441 MessageElement::Dynamic(data_type, class)
1442 };
1443 all_known &= meta.as_ref().is_some_and(|meta| {
1444 AsRef::<str>::as_ref(&meta.name) == name
1445 && meta.element == element
1446 && meta.reliable == reliable
1447 && meta.priority >= priority
1448 && meta.e2e_encryption == encryption
1449 });
1450 for _ in 0..endpoint_count {
1451 let ep = DataEndpoint(read_u32(
1452 payload,
1453 &mut cursor,
1454 "discovery schema type endpoint",
1455 )?);
1456 all_known &= meta
1457 .as_ref()
1458 .is_some_and(|meta| meta.endpoints.contains(&ep));
1459 }
1460 }
1461 if cursor != payload.len() {
1462 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1463 }
1464 Ok(all_known)
1465}
1466
1467pub fn build_discovery_schema(sender: &str, timestamp_ms: u64) -> TelemetryResult<Packet> {
1469 #[cfg(feature = "std")]
1470 return build_discovery_schema_from_owned_snapshot(sender, timestamp_ms, export_schema());
1471 #[cfg(not(feature = "std"))]
1472 return Packet::new(
1473 DataType::DiscoverySchema,
1474 &[DataEndpoint::Discovery],
1475 sender,
1476 timestamp_ms,
1477 crate::config::encode_embedded_schema_payload()?,
1478 );
1479}
1480
1481pub fn elect_discovery_master(local_sender: &str, boards: &[TopologyBoardNode]) -> String {
1489 let mut nodes: BTreeMap<String, Vec<String>> = BTreeMap::new();
1490 for board in boards {
1491 nodes.entry(board.sender_id.clone()).or_default();
1492 for peer in board.connections.iter() {
1493 nodes
1494 .entry(peer.clone())
1495 .or_default()
1496 .push(board.sender_id.clone());
1497 nodes
1498 .entry(board.sender_id.clone())
1499 .or_default()
1500 .push(peer.clone());
1501 }
1502 }
1503 nodes.entry(local_sender.to_string()).or_default();
1504 for peers in nodes.values_mut() {
1505 peers.sort_unstable();
1506 peers.dedup();
1507 }
1508
1509 let mut best_sender = local_sender.to_string();
1510 let mut best_unreachable = usize::MAX;
1511 let mut best_max_hops = usize::MAX;
1512 let mut best_total_hops = usize::MAX;
1513
1514 for sender in nodes.keys() {
1515 let mut frontier = vec![sender.clone()];
1516 let mut seen: BTreeMap<String, usize> = BTreeMap::new();
1517 seen.insert(sender.clone(), 0);
1518 let mut idx = 0;
1519 while idx < frontier.len() {
1520 let cur = frontier[idx].clone();
1521 idx += 1;
1522 let cur_dist = seen[&cur];
1523 if let Some(peers) = nodes.get(&cur) {
1524 for peer in peers {
1525 if !seen.contains_key(peer) {
1526 seen.insert(peer.clone(), cur_dist + 1);
1527 frontier.push(peer.clone());
1528 }
1529 }
1530 }
1531 }
1532
1533 let unreachable = nodes.len().saturating_sub(seen.len());
1534 let max_hops = seen.values().copied().max().unwrap_or(0);
1535 let total_hops = seen.values().copied().sum();
1536 let better = unreachable < best_unreachable
1537 || (unreachable == best_unreachable && max_hops < best_max_hops)
1538 || (unreachable == best_unreachable
1539 && max_hops == best_max_hops
1540 && total_hops < best_total_hops)
1541 || (unreachable == best_unreachable
1542 && max_hops == best_max_hops
1543 && total_hops == best_total_hops
1544 && sender < &best_sender);
1545 if better {
1546 best_sender = sender.clone();
1547 best_unreachable = unreachable;
1548 best_max_hops = max_hops;
1549 best_total_hops = total_hops;
1550 }
1551 }
1552
1553 best_sender
1554}
1555
1556pub fn build_discovery_schema_from_snapshot(
1558 sender: &str,
1559 timestamp_ms: u64,
1560 schema: RuntimeSchemaSnapshot,
1561) -> TelemetryResult<Packet> {
1562 let mut payload = Vec::new();
1563 payload.extend_from_slice(&3u32.to_le_bytes());
1564
1565 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1566 for ep in schema.endpoints {
1567 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1568 payload.push(ep.link_local_only as u8);
1569 encode_string(&mut payload, ep.name)?;
1570 #[cfg(feature = "std")]
1571 encode_string(&mut payload, ep.description)?;
1572 #[cfg(not(feature = "std"))]
1573 encode_string(&mut payload, "")?;
1574 }
1575
1576 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1577 for ty in schema.types {
1578 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1579 encode_string(&mut payload, ty.name)?;
1580 #[cfg(feature = "std")]
1581 encode_string(&mut payload, ty.description)?;
1582 #[cfg(not(feature = "std"))]
1583 encode_string(&mut payload, "")?;
1584 match ty.element {
1585 MessageElement::Static(count, data_type, class) => {
1586 payload.push(0);
1587 payload.extend_from_slice(&(count as u32).to_le_bytes());
1588 payload.push(message_data_type_code(data_type));
1589 payload.push(message_class_code(class));
1590 }
1591 MessageElement::Dynamic(data_type, class) => {
1592 payload.push(1);
1593 payload.extend_from_slice(&0u32.to_le_bytes());
1594 payload.push(message_data_type_code(data_type));
1595 payload.push(message_class_code(class));
1596 }
1597 }
1598 payload.push(reliable_code(ty.reliable));
1599 payload.push(ty.priority);
1600 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1601 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1602 for ep in ty.endpoints {
1603 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1604 }
1605 }
1606
1607 Packet::new(
1608 DataType::DiscoverySchema,
1609 &[DataEndpoint::Discovery],
1610 sender,
1611 timestamp_ms,
1612 payload.into(),
1613 )
1614}
1615
1616pub fn build_discovery_schema_from_owned_snapshot(
1618 sender: &str,
1619 timestamp_ms: u64,
1620 schema: OwnedRuntimeSchemaSnapshot,
1621) -> TelemetryResult<Packet> {
1622 let mut payload = Vec::new();
1623 payload.extend_from_slice(&3u32.to_le_bytes());
1624
1625 payload.extend_from_slice(&(schema.endpoints.len() as u32).to_le_bytes());
1626 for ep in schema.endpoints {
1627 payload.extend_from_slice(&ep.id.as_u32().to_le_bytes());
1628 payload.push(ep.link_local_only as u8);
1629 encode_string(&mut payload, &ep.name)?;
1630 encode_string(&mut payload, "")?;
1634 }
1635
1636 payload.extend_from_slice(&(schema.types.len() as u32).to_le_bytes());
1637 for ty in schema.types {
1638 payload.extend_from_slice(&ty.id.as_u32().to_le_bytes());
1639 encode_string(&mut payload, &ty.name)?;
1640 encode_string(&mut payload, "")?;
1641 match ty.element {
1642 MessageElement::Static(count, data_type, class) => {
1643 payload.push(0);
1644 payload.extend_from_slice(&(count as u32).to_le_bytes());
1645 payload.push(message_data_type_code(data_type));
1646 payload.push(message_class_code(class));
1647 }
1648 MessageElement::Dynamic(data_type, class) => {
1649 payload.push(1);
1650 payload.extend_from_slice(&0u32.to_le_bytes());
1651 payload.push(message_data_type_code(data_type));
1652 payload.push(message_class_code(class));
1653 }
1654 }
1655 payload.push(reliable_code(ty.reliable));
1656 payload.push(ty.priority);
1657 payload.push(e2e_encryption_policy_code(ty.e2e_encryption));
1658 payload.extend_from_slice(&(ty.endpoints.len() as u32).to_le_bytes());
1659 for ep in ty.endpoints {
1660 payload.extend_from_slice(&ep.as_u32().to_le_bytes());
1661 }
1662 }
1663
1664 Packet::new(
1665 DataType::DiscoverySchema,
1666 &[DataEndpoint::Discovery],
1667 sender,
1668 timestamp_ms,
1669 payload.into(),
1670 )
1671}
1672
1673pub fn decode_discovery_schema(pkt: &Packet) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1675 if pkt.data_type() != DataType::DiscoverySchema {
1676 return Err(TelemetryError::InvalidType);
1677 }
1678 decode_discovery_schema_payload(pkt.payload())
1679}
1680
1681pub fn decode_discovery_schema_payload(
1683 payload: &[u8],
1684) -> TelemetryResult<OwnedRuntimeSchemaSnapshot> {
1685 let mut cursor = 0usize;
1686 let version = read_u32(payload, &mut cursor, "discovery schema version")?;
1687 #[cfg(feature = "std")]
1688 if version != 1 && version != 2 && version != 3 {
1689 return Err(TelemetryError::Unpack("discovery schema version"));
1690 }
1691 #[cfg(not(feature = "std"))]
1692 if version != 3 {
1693 return Err(TelemetryError::Unpack("discovery schema version"));
1694 }
1695
1696 let endpoint_count =
1697 read_u32(payload, &mut cursor, "discovery schema endpoint count")? as usize;
1698 let mut endpoints = bounded_wire_vec(
1699 payload,
1700 cursor,
1701 endpoint_count,
1702 13,
1703 "discovery schema endpoint count",
1704 )?;
1705 for _ in 0..endpoint_count {
1706 let id = DataEndpoint(read_u32(
1707 payload,
1708 &mut cursor,
1709 "discovery schema endpoint id",
1710 )?);
1711 let link_local_only =
1712 read_u8(payload, &mut cursor, "discovery schema endpoint flags")? != 0;
1713 let name = decode_string(payload, &mut cursor, "discovery schema endpoint name")?;
1714 #[cfg(feature = "std")]
1715 let description = if version >= 2 {
1716 decode_string(
1717 payload,
1718 &mut cursor,
1719 "discovery schema endpoint description",
1720 )?
1721 } else {
1722 String::new()
1723 };
1724 #[cfg(not(feature = "std"))]
1725 let description = {
1726 skip_string(
1727 payload,
1728 &mut cursor,
1729 "discovery schema endpoint description",
1730 )?;
1731 String::new()
1732 };
1733 endpoints.push(OwnedEndpointDefinition {
1734 id,
1735 name,
1736 description,
1737 link_local_only,
1738 });
1739 }
1740
1741 let type_count = read_u32(payload, &mut cursor, "discovery schema type count")? as usize;
1742 let mut types = bounded_wire_vec(
1743 payload,
1744 cursor,
1745 type_count,
1746 19,
1747 "discovery schema type count",
1748 )?;
1749 for _ in 0..type_count {
1750 let id = DataType(read_u32(payload, &mut cursor, "discovery schema type id")?);
1751 let name = decode_string(payload, &mut cursor, "discovery schema type name")?;
1752 #[cfg(feature = "std")]
1753 let description = if version >= 2 {
1754 decode_string(payload, &mut cursor, "discovery schema type description")?
1755 } else {
1756 String::new()
1757 };
1758 #[cfg(not(feature = "std"))]
1759 let description = {
1760 skip_string(payload, &mut cursor, "discovery schema type description")?;
1761 String::new()
1762 };
1763 let element_kind = read_u8(payload, &mut cursor, "discovery schema element kind")?;
1764 let count = read_u32(payload, &mut cursor, "discovery schema element count")? as usize;
1765 let data_type = message_data_type_from_code(read_u8(
1766 payload,
1767 &mut cursor,
1768 "discovery schema data type",
1769 )?)
1770 .ok_or(TelemetryError::Unpack("discovery schema data type"))?;
1771 let class =
1772 message_class_from_code(read_u8(payload, &mut cursor, "discovery schema class")?)
1773 .ok_or(TelemetryError::Unpack("discovery schema class"))?;
1774 let element = match element_kind {
1775 0 => MessageElement::Static(count, data_type, class),
1776 1 => MessageElement::Dynamic(data_type, class),
1777 _ => return Err(TelemetryError::Unpack("discovery schema element kind")),
1778 };
1779 let reliable =
1780 reliable_from_code(read_u8(payload, &mut cursor, "discovery schema reliable")?)
1781 .ok_or(TelemetryError::Unpack("discovery schema reliable"))?;
1782 let priority = read_u8(payload, &mut cursor, "discovery schema priority")?;
1783 #[cfg(feature = "std")]
1784 let e2e_encryption = if version >= 3 {
1785 e2e_encryption_policy_from_code(read_u8(
1786 payload,
1787 &mut cursor,
1788 "discovery schema e2e cryptography",
1789 )?)
1790 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?
1791 } else {
1792 E2eEncryptionPolicy::PreferOff
1793 };
1794 #[cfg(not(feature = "std"))]
1795 let e2e_encryption = e2e_encryption_policy_from_code(read_u8(
1796 payload,
1797 &mut cursor,
1798 "discovery schema e2e cryptography",
1799 )?)
1800 .ok_or(TelemetryError::Unpack("discovery schema e2e cryptography"))?;
1801 let endpoint_count =
1802 read_u32(payload, &mut cursor, "discovery schema type endpoint count")? as usize;
1803 let mut type_endpoints = bounded_wire_vec(
1804 payload,
1805 cursor,
1806 endpoint_count,
1807 4,
1808 "discovery schema type endpoint count",
1809 )?;
1810 for _ in 0..endpoint_count {
1811 type_endpoints.push(DataEndpoint(read_u32(
1812 payload,
1813 &mut cursor,
1814 "discovery schema type endpoint",
1815 )?));
1816 }
1817 types.push(OwnedDataTypeDefinition {
1818 id,
1819 name,
1820 description,
1821 element,
1822 endpoints: type_endpoints,
1823 reliable,
1824 priority,
1825 e2e_encryption,
1826 });
1827 }
1828
1829 if cursor != payload.len() {
1830 return Err(TelemetryError::Unpack("discovery schema trailing bytes"));
1831 }
1832 Ok(OwnedRuntimeSchemaSnapshot { endpoints, types })
1833}
1834
1835pub(crate) fn same_schema_snapshot(existing: &Packet, incoming: &Packet) -> bool {
1839 existing.data_type() == DataType::DiscoverySchema
1840 && incoming.data_type() == DataType::DiscoverySchema
1841 && existing.sender() == incoming.sender()
1842 && existing.endpoints() == incoming.endpoints()
1843 && existing.wire_target_senders() == incoming.wire_target_senders()
1844 && existing.payload() == incoming.payload()
1845}
1846
1847#[cfg(test)]
1848mod bounded_decode_tests {
1849 use super::{
1850 ADDRESS_MODE_STATIC, ADDRESS_STATE_APPROVED, AddressAdvertisement, LinkCapabilities,
1851 build_discovery_address, build_managed_variable_request, decode_discovery_address,
1852 decode_discovery_schema_payload, decode_discovery_timesync_sources_payload,
1853 decode_discovery_topology_payload, decode_managed_variable_request,
1854 discovery_schema_payload_is_fully_known,
1855 };
1856 use crate::{DataEndpoint, DataType};
1857
1858 #[test]
1859 fn equivalent_schema_definitions_are_detected_without_owned_decode() {
1860 fn one_endpoint(id: DataEndpoint) -> Vec<u8> {
1861 let mut payload = Vec::new();
1862 payload.extend_from_slice(&3u32.to_le_bytes());
1863 payload.extend_from_slice(&1u32.to_le_bytes());
1864 payload.extend_from_slice(&id.0.to_le_bytes());
1865 payload.push(0);
1866 let meta = crate::config::get_endpoint_meta(id);
1867 payload.extend_from_slice(&(meta.name.len() as u32).to_le_bytes());
1868 payload.extend_from_slice(meta.name.as_bytes());
1869 payload.extend_from_slice(&0u32.to_le_bytes());
1870 payload.extend_from_slice(&0u32.to_le_bytes());
1871 payload
1872 }
1873
1874 assert!(
1875 discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint::Discovery,))
1876 .unwrap()
1877 );
1878 assert!(
1879 !discovery_schema_payload_is_fully_known(&one_endpoint(DataEndpoint(u32::MAX - 7),))
1880 .unwrap()
1881 );
1882 }
1883
1884 #[test]
1885 fn reused_ids_with_changed_schema_are_not_treated_as_known() {
1886 let schema = crate::config::export_schema();
1887 let original =
1888 super::build_discovery_schema_from_owned_snapshot("test", 1, schema.clone()).unwrap();
1889 assert!(discovery_schema_payload_is_fully_known(original.payload()).unwrap());
1890 for change in 0..4 {
1891 let mut updated = schema.clone();
1892 match change {
1893 0 => updated.endpoints[0].name.push_str("_CHANGED"),
1894 1 => updated.endpoints[0].link_local_only = !updated.endpoints[0].link_local_only,
1895 2 => updated.types[0].name.push_str("_CHANGED"),
1896 _ => updated.types[0].endpoints.push(DataEndpoint(u32::MAX - 7)),
1897 }
1898 let packet =
1899 super::build_discovery_schema_from_owned_snapshot("test", 2, updated).unwrap();
1900 assert!(
1901 !discovery_schema_payload_is_fully_known(packet.payload()).unwrap(),
1902 "change {change} was silently ignored"
1903 );
1904 }
1905 }
1906
1907 #[test]
1908 fn unknown_managed_variable_ids_survive_discovery_before_schema_merge() {
1909 let unknown = DataType(4090);
1910 let unknown_endpoint = DataEndpoint(4090);
1911 let advertisement = AddressAdvertisement {
1912 hostname: "flight".into(),
1913 address: 42,
1914 requested_address: 42,
1915 mode: ADDRESS_MODE_STATIC,
1916 state: ADDRESS_STATE_APPROVED,
1917 birth_ms: 10,
1918 owner_hash: 20,
1919 reachable_endpoints: vec![unknown_endpoint],
1920 reachable_network_variables: vec![unknown],
1921 reachable_timesync_sources: Vec::new(),
1922 link_capabilities: LinkCapabilities {
1923 version: 1,
1924 flags: 0,
1925 profile: 0,
1926 max_frame_bytes: 128,
1927 compact_header_target_bytes: 40,
1928 max_side_transport_templates: 4,
1929 },
1930 };
1931 let packet = build_discovery_address("FC", 1, &advertisement).unwrap();
1932 let decoded = decode_discovery_address(&packet).unwrap();
1933 assert_eq!(decoded.reachable_endpoints, vec![unknown_endpoint]);
1934 assert_eq!(decoded.reachable_network_variables, vec![unknown]);
1935
1936 let request = build_managed_variable_request("GS", 2, unknown).unwrap();
1937 assert_eq!(decode_managed_variable_request(&request).unwrap(), unknown);
1938 }
1939
1940 #[test]
1941 fn corrupt_timesync_count_is_rejected_without_reserving() {
1942 assert!(decode_discovery_timesync_sources_payload(&u32::MAX.to_le_bytes()).is_err());
1943 }
1944
1945 #[test]
1946 fn corrupt_topology_count_is_rejected_without_reserving() {
1947 assert!(decode_discovery_topology_payload(&u32::MAX.to_le_bytes()).is_err());
1948 }
1949
1950 #[test]
1951 fn corrupt_schema_counts_are_rejected_without_reserving() {
1952 let mut endpoints = 3u32.to_le_bytes().to_vec();
1953 endpoints.extend_from_slice(&u32::MAX.to_le_bytes());
1954 assert!(decode_discovery_schema_payload(&endpoints).is_err());
1955
1956 let mut types = 3u32.to_le_bytes().to_vec();
1957 types.extend_from_slice(&0u32.to_le_bytes());
1958 types.extend_from_slice(&u32::MAX.to_le_bytes());
1959 assert!(decode_discovery_schema_payload(&types).is_err());
1960 }
1961}