1use std::fmt;
2
3use prns_core::interfaces::{
4 AnnounceBandwidthCap, EgressCapability, InterfaceMode, RecursivePathRequestPolicy,
5};
6
7use crate::reference::{
8 announce_rate_target_is_explicit_off,
9 keys::{common as common_key, interface as interface_key},
10};
11use crate::{
12 ConfiguredInterfaceLifecycle, DiscoveryAdvertisementPlan, DiscoveryEncryption,
13 DiscoveryIfacPublication, InterfaceAccessPlan, InterfaceDiscoveryPlan, InterfaceKind,
14 PlannedInterface, PlannedMedium,
15};
16
17use super::interface::ALL_SETTING_KEYS;
18use super::{InterfaceSetting, InterfaceSettingKey, InterfaceSettingValue};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub enum InterfaceSettingCategory {
22 Connectivity,
23 Access,
24 Behavior,
25 Discovery,
26 Announcements,
27 Radio,
28 TrafficControl,
29 Advanced,
30}
31
32impl fmt::Display for InterfaceSettingCategory {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 formatter.write_str(match self {
35 Self::Connectivity => "Connectivity",
36 Self::Access => "Network access",
37 Self::Behavior => "Interface behavior",
38 Self::Discovery => "Discovery publication",
39 Self::Announcements => "Announcement limits",
40 Self::Radio => "Radio",
41 Self::TrafficControl => "Advanced traffic control",
42 Self::Advanced => "Advanced",
43 })
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum InterfaceSettingTier {
49 Standard,
50 Advanced,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum InterfaceSettingCondition {
55 IfacEnabled,
56 Discoverable,
57 DiscoverableKiss,
58 AnnounceRateLimit,
59 IngressControl,
60 EgressControl,
61 KissFraming,
62}
63
64impl fmt::Display for InterfaceSettingCondition {
65 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66 formatter.write_str(match self {
67 Self::IfacEnabled => "network name or pass phrase must be configured",
68 Self::Discoverable => "Discoverable must be Yes",
69 Self::DiscoverableKiss => "Discoverable and KISS framing must both be Yes",
70 Self::AnnounceRateLimit => {
71 "an interface or transport announcement-rate target must be active"
72 }
73 Self::IngressControl => "Ingress control must be Yes",
74 Self::EgressControl => "Egress control must be Yes",
75 Self::KissFraming => "KISS framing must be Yes",
76 })
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum InterfaceSettingInputKind {
82 Boolean,
83 Unsigned,
84 Signed,
85 Decimal,
86 Text,
87 List,
88 Port,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct InterfaceSettingSpec {
93 key: InterfaceSettingKey,
94}
95
96impl InterfaceSettingSpec {
97 pub const fn key(self) -> InterfaceSettingKey {
98 self.key
99 }
100
101 pub fn label(self) -> String {
102 match self.key.as_str() {
103 interface_key::OUTGOING => "Outgoing traffic allowed".to_string(),
104 interface_key::IFAC_SIZE => "IFAC size".to_string(),
105 interface_key::ID_CALLSIGN => "ID callsign".to_string(),
106 interface_key::ID_INTERVAL => "ID interval".to_string(),
107 interface_key::SSID => "SSID".to_string(),
108 interface_key::TXPOWER => "Transmit power".to_string(),
109 interface_key::TXTAIL => "TX tail".to_string(),
110 common_key::EC_PR_FREQ => "Egress path-request frequency".to_string(),
111 key => key
112 .split('_')
113 .enumerate()
114 .map(|(index, word)| {
115 let acronym = match word {
116 "ax25" => Some("AX.25"),
117 "ec" => Some("EC"),
118 "ic" => Some("IC"),
119 "id" => Some("ID"),
120 "ifac" => Some("IFAC"),
121 "ip" => Some("IP"),
122 "mtu" => Some("MTU"),
123 "pr" => Some("PR"),
124 "prs" => Some("PRs"),
125 "ssid" => Some("SSID"),
126 "tcp" => Some("TCP"),
127 "tx" => Some("TX"),
128 "udp" => Some("UDP"),
129 _ => None,
130 };
131 if let Some(acronym) = acronym {
132 return acronym.to_string();
133 }
134 if index == 0 {
135 let mut characters = word.chars();
136 match characters.next() {
137 Some(first) => {
138 first.to_uppercase().collect::<String>() + characters.as_str()
139 }
140 None => String::new(),
141 }
142 } else {
143 word.to_string()
144 }
145 })
146 .collect::<Vec<_>>()
147 .join(" "),
148 }
149 }
150
151 pub fn category(self) -> InterfaceSettingCategory {
152 match self.key.as_str() {
153 interface_key::TARGET_HOST
154 | interface_key::TARGET_PORT
155 | interface_key::TARGET
156 | interface_key::LISTEN_IP
157 | interface_key::LISTEN_PORT
158 | interface_key::FORWARD_IP
159 | interface_key::FORWARD_PORT
160 | interface_key::DEVICE
161 | interface_key::PORT
162 | interface_key::REMOTE
163 | interface_key::LISTEN_ON
164 | interface_key::PEERS
165 | interface_key::CONNECTABLE
166 | interface_key::CONNECT_TIMEOUT
167 | interface_key::MAX_RECONNECT_TRIES
168 | interface_key::PREFER_IPV6
169 | interface_key::GROUP_ID
170 | interface_key::DISCOVERY_SCOPE
171 | interface_key::DISCOVERY_PORT
172 | interface_key::DATA_PORT
173 | interface_key::DEVICES
174 | interface_key::IGNORED_DEVICES
175 | interface_key::MULTICAST_ADDRESS_TYPE => InterfaceSettingCategory::Connectivity,
176 interface_key::NETWORK_NAME | interface_key::PASS_PHRASE | interface_key::IFAC_SIZE => {
177 InterfaceSettingCategory::Access
178 }
179 interface_key::DISCOVERABLE
180 | interface_key::ANNOUNCE_INTERVAL
181 | interface_key::DISCOVERY_STAMP_VALUE
182 | interface_key::DISCOVERY_NAME
183 | interface_key::DISCOVERY_ENCRYPT
184 | interface_key::REACHABLE_ON
185 | interface_key::PUBLISH_IFAC
186 | interface_key::LATITUDE
187 | interface_key::LONGITUDE
188 | interface_key::HEIGHT
189 | interface_key::DISCOVERY_FREQUENCY
190 | interface_key::DISCOVERY_BANDWIDTH
191 | interface_key::DISCOVERY_MODULATION => InterfaceSettingCategory::Discovery,
192 interface_key::INTERFACE_MODE
193 | interface_key::OUTGOING
194 | interface_key::BITRATE
195 | interface_key::GRAVITY
196 | interface_key::BOOTSTRAP_ONLY
197 | interface_key::RECURSIVE_PRS
198 | interface_key::ANNOUNCES_FROM_INTERNAL
199 | interface_key::ANNOUNCES_TO_INTERNAL => InterfaceSettingCategory::Behavior,
200 interface_key::ANNOUNCE_CAP
201 | interface_key::ANNOUNCE_RATE_TARGET
202 | interface_key::ANNOUNCE_RATE_GRACE
203 | interface_key::ANNOUNCE_RATE_PENALTY => InterfaceSettingCategory::Announcements,
204 common_key::INGRESS_CONTROL
205 | common_key::EGRESS_CONTROL
206 | common_key::IC_MAX_HELD_ANNOUNCES
207 | common_key::IC_BURST_HOLD
208 | common_key::IC_BURST_FREQ_NEW
209 | common_key::IC_BURST_FREQ
210 | common_key::IC_PR_BURST_FREQ_NEW
211 | common_key::IC_PR_BURST_FREQ
212 | common_key::EC_PR_FREQ
213 | common_key::IC_NEW_TIME
214 | common_key::IC_BURST_PENALTY
215 | common_key::IC_HELD_RELEASE_INTERVAL => InterfaceSettingCategory::TrafficControl,
216 interface_key::SPEED
217 | interface_key::DATABITS
218 | interface_key::PARITY
219 | interface_key::STOPBITS
220 | interface_key::FLOW_CONTROL
221 | interface_key::PREAMBLE
222 | interface_key::TXTAIL
223 | interface_key::PERSISTENCE
224 | interface_key::SLOTTIME
225 | interface_key::ID_CALLSIGN
226 | interface_key::ID_INTERVAL
227 | interface_key::CALLSIGN
228 | interface_key::SSID
229 | interface_key::FREQUENCY
230 | interface_key::BANDWIDTH
231 | interface_key::SPREADINGFACTOR
232 | interface_key::CODINGRATE
233 | interface_key::TXPOWER
234 | interface_key::AIRTIME_LIMIT_SHORT
235 | interface_key::AIRTIME_LIMIT_LONG => InterfaceSettingCategory::Radio,
236 interface_key::FIXED_MTU | interface_key::IGNORE_CONFIG_WARNINGS => {
237 InterfaceSettingCategory::Advanced
238 }
239 _ => InterfaceSettingCategory::Advanced,
240 }
241 }
242
243 pub fn tier(self) -> InterfaceSettingTier {
244 match self.category() {
245 InterfaceSettingCategory::Connectivity | InterfaceSettingCategory::Access => {
246 InterfaceSettingTier::Standard
247 }
248 InterfaceSettingCategory::Behavior
249 if matches!(
250 self.key.as_str(),
251 interface_key::INTERFACE_MODE | interface_key::OUTGOING
252 ) =>
253 {
254 InterfaceSettingTier::Standard
255 }
256 InterfaceSettingCategory::Discovery
257 if self.key.as_str() == interface_key::DISCOVERABLE =>
258 {
259 InterfaceSettingTier::Standard
260 }
261 InterfaceSettingCategory::Radio
262 if !matches!(
263 self.key.as_str(),
264 interface_key::AIRTIME_LIMIT_SHORT
265 | interface_key::AIRTIME_LIMIT_LONG
266 | interface_key::ID_CALLSIGN
267 | interface_key::ID_INTERVAL
268 ) =>
269 {
270 InterfaceSettingTier::Standard
271 }
272 _ => InterfaceSettingTier::Advanced,
273 }
274 }
275
276 pub fn condition(self, kind: InterfaceKind) -> Option<InterfaceSettingCondition> {
277 match self.key.as_str() {
278 interface_key::IFAC_SIZE => Some(InterfaceSettingCondition::IfacEnabled),
279 interface_key::DISCOVERABLE if kind == InterfaceKind::TcpClient => {
280 Some(InterfaceSettingCondition::KissFraming)
281 }
282 interface_key::ANNOUNCE_INTERVAL
283 | interface_key::DISCOVERY_STAMP_VALUE
284 | interface_key::DISCOVERY_NAME
285 | interface_key::DISCOVERY_ENCRYPT
286 | interface_key::REACHABLE_ON
287 | interface_key::PUBLISH_IFAC
288 | interface_key::LATITUDE
289 | interface_key::LONGITUDE
290 | interface_key::HEIGHT
291 | interface_key::DISCOVERY_FREQUENCY
292 | interface_key::DISCOVERY_BANDWIDTH
293 | interface_key::DISCOVERY_MODULATION
294 if kind == InterfaceKind::TcpClient =>
295 {
296 Some(InterfaceSettingCondition::DiscoverableKiss)
297 }
298 interface_key::ANNOUNCE_INTERVAL
299 | interface_key::DISCOVERY_STAMP_VALUE
300 | interface_key::DISCOVERY_NAME
301 | interface_key::DISCOVERY_ENCRYPT
302 | interface_key::REACHABLE_ON
303 | interface_key::PUBLISH_IFAC
304 | interface_key::LATITUDE
305 | interface_key::LONGITUDE
306 | interface_key::HEIGHT
307 | interface_key::DISCOVERY_FREQUENCY
308 | interface_key::DISCOVERY_BANDWIDTH
309 | interface_key::DISCOVERY_MODULATION => Some(InterfaceSettingCondition::Discoverable),
310 interface_key::ANNOUNCE_RATE_GRACE | interface_key::ANNOUNCE_RATE_PENALTY => {
311 Some(InterfaceSettingCondition::AnnounceRateLimit)
312 }
313 common_key::IC_MAX_HELD_ANNOUNCES
314 | common_key::IC_BURST_HOLD
315 | common_key::IC_BURST_FREQ_NEW
316 | common_key::IC_BURST_FREQ
317 | common_key::IC_PR_BURST_FREQ_NEW
318 | common_key::IC_PR_BURST_FREQ
319 | common_key::IC_NEW_TIME
320 | common_key::IC_BURST_PENALTY
321 | common_key::IC_HELD_RELEASE_INTERVAL => {
322 Some(InterfaceSettingCondition::IngressControl)
323 }
324 common_key::EC_PR_FREQ => Some(InterfaceSettingCondition::EgressControl),
325 _ => None,
326 }
327 }
328
329 pub fn is_supported(self, kind: InterfaceKind) -> bool {
330 let discovery_capable = matches!(
331 kind,
332 InterfaceKind::TcpClient
333 | InterfaceKind::TcpServer
334 | InterfaceKind::Kiss
335 | InterfaceKind::Rnode
336 | InterfaceKind::RnodeMulti
337 | InterfaceKind::Backbone
338 );
339 match self.key.as_str() {
340 interface_key::IGNORE_CONFIG_WARNINGS => false,
341 interface_key::DISCOVERABLE
342 | interface_key::ANNOUNCE_INTERVAL
343 | interface_key::DISCOVERY_STAMP_VALUE
344 | interface_key::DISCOVERY_NAME
345 | interface_key::DISCOVERY_ENCRYPT
346 | interface_key::PUBLISH_IFAC
347 | interface_key::LATITUDE
348 | interface_key::LONGITUDE
349 | interface_key::HEIGHT => discovery_capable,
350 interface_key::REACHABLE_ON => {
351 matches!(kind, InterfaceKind::TcpServer | InterfaceKind::Backbone)
352 }
353 interface_key::DISCOVERY_FREQUENCY
354 | interface_key::DISCOVERY_BANDWIDTH
355 | interface_key::DISCOVERY_MODULATION => {
356 matches!(kind, InterfaceKind::TcpClient | InterfaceKind::Kiss)
357 }
358 _ => true,
359 }
360 }
361
362 pub fn unsupported_reason(self, kind: InterfaceKind) -> Option<&'static str> {
363 if self.is_supported(kind) {
364 return None;
365 }
366 Some(match self.key.as_str() {
367 interface_key::IGNORE_CONFIG_WARNINGS => {
368 "Prns does not suppress configuration warnings per interface"
369 }
370 interface_key::REACHABLE_ON => {
371 "only listening TCP and Backbone interfaces publish a reachable address"
372 }
373 interface_key::DISCOVERY_FREQUENCY
374 | interface_key::DISCOVERY_BANDWIDTH
375 | interface_key::DISCOVERY_MODULATION => {
376 "only KISS discovery advertisements use separately configured radio metadata"
377 }
378 _ => "this interface type cannot publish interface-discovery advertisements",
379 })
380 }
381
382 pub fn description(self) -> &'static str {
383 match self.key.as_str() {
384 interface_key::INTERFACE_MODE => {
385 "Controls how routing and path discovery treat this interface."
386 }
387 interface_key::OUTGOING => {
388 "Allows or prevents Prns from transmitting traffic through this interface."
389 }
390 interface_key::BITRATE => {
391 "Overrides the interface bitrate used for pacing, MTU selection, and route costs."
392 }
393 interface_key::GRAVITY => {
394 "Prefers this interface when equally fresh valid announce evidence arrives through multiple paths."
395 }
396 interface_key::ANNOUNCE_CAP => {
397 "Limits announcement traffic to a percentage of this interface's bitrate."
398 }
399 interface_key::ANNOUNCE_RATE_TARGET => {
400 "Sets the minimum target interval between announcements from one destination."
401 }
402 interface_key::ANNOUNCE_RATE_GRACE => {
403 "Sets how many announcement-rate violations are tolerated before penalizing a destination."
404 }
405 interface_key::ANNOUNCE_RATE_PENALTY => {
406 "Sets the penalty interval applied after the announcement-rate grace is exhausted."
407 }
408 interface_key::NETWORK_NAME => {
409 "Adds this interface to a named IFAC network and restricts traffic to matching peers."
410 }
411 interface_key::PASS_PHRASE => {
412 "Adds secret IFAC key material used to authenticate traffic on this interface."
413 }
414 interface_key::IFAC_SIZE => {
415 "Sets the number of authentication bits carried by each IFAC-protected packet."
416 }
417 interface_key::DISCOVERABLE => {
418 "Publishes this interface through Prns interface discovery."
419 }
420 interface_key::ANNOUNCE_INTERVAL => {
421 "Sets how often this interface publishes its discovery advertisement."
422 }
423 interface_key::DISCOVERY_STAMP_VALUE => {
424 "Sets the proof-of-work cost required for this interface's discovery advertisement."
425 }
426 interface_key::DISCOVERY_NAME => {
427 "Publishes a human-readable name with this interface's discovery advertisement."
428 }
429 interface_key::DISCOVERY_ENCRYPT => {
430 "Encrypts discovery advertisements to the configured network identity."
431 }
432 interface_key::REACHABLE_ON => {
433 "Publishes the address peers should use to reach this listening interface."
434 }
435 interface_key::PUBLISH_IFAC => {
436 "Includes this interface's IFAC identity in its discovery advertisement."
437 }
438 interface_key::LATITUDE => {
439 "Publishes the interface's latitude in discovery metadata."
440 }
441 interface_key::LONGITUDE => {
442 "Publishes the interface's longitude in discovery metadata."
443 }
444 interface_key::HEIGHT => {
445 "Publishes the interface's height in discovery metadata."
446 }
447 interface_key::DISCOVERY_FREQUENCY => {
448 "Publishes a radio frequency for a KISS discovery advertisement."
449 }
450 interface_key::DISCOVERY_BANDWIDTH => {
451 "Publishes a radio bandwidth for a KISS discovery advertisement."
452 }
453 interface_key::DISCOVERY_MODULATION => {
454 "Publishes a modulation name for a KISS discovery advertisement."
455 }
456 interface_key::BOOTSTRAP_ONLY => {
457 "Marks this interface as temporary bootstrap connectivity that can retire after discovery succeeds."
458 }
459 interface_key::RECURSIVE_PRS => {
460 "Allows recursive path-request forwarding according to this interface's routing policy."
461 }
462 interface_key::ANNOUNCES_FROM_INTERNAL => {
463 "Allows announcements arriving from internal-mode interfaces to leave through this interface."
464 }
465 interface_key::ANNOUNCES_TO_INTERNAL => {
466 "Allows announcements arriving on this interface to enter internal-mode interfaces."
467 }
468 interface_key::IGNORE_CONFIG_WARNINGS => {
469 "Requests stock RNS warning suppression; Prns intentionally does not apply this setting."
470 }
471 interface_key::GROUP_ID => {
472 "Selects the AutoInterface discovery group whose nearby members can find each other."
473 }
474 interface_key::DISCOVERY_SCOPE => {
475 "Sets how far AutoInterface multicast discovery packets may travel."
476 }
477 interface_key::DISCOVERY_PORT => {
478 "Sets the UDP port used for AutoInterface peer discovery."
479 }
480 interface_key::DATA_PORT => {
481 "Sets the UDP port used for AutoInterface packet traffic."
482 }
483 interface_key::DEVICES => {
484 "Restricts AutoInterface to the listed network device names."
485 }
486 interface_key::IGNORED_DEVICES => {
487 "Prevents AutoInterface from using the listed network device names."
488 }
489 interface_key::MULTICAST_ADDRESS_TYPE => {
490 "Chooses temporary or permanently assigned IPv6 multicast addressing for AutoInterface discovery."
491 }
492 interface_key::TARGET_HOST => "Sets the host name or address this client connects to.",
493 interface_key::TARGET_PORT => "Sets the remote port this client connects to.",
494 interface_key::TARGET => "Sets the complete remote WebSocket URL.",
495 interface_key::KISS_FRAMING => {
496 "Wraps packets in KISS framing while they cross this TCP connection."
497 }
498 interface_key::I2P_TUNNELED => {
499 "Treats this TCP connection as already carried through an I2P tunnel."
500 }
501 interface_key::CONNECT_TIMEOUT => {
502 "Limits how long each outbound connection attempt may take."
503 }
504 interface_key::MAX_RECONNECT_TRIES => {
505 "Limits reconnect attempts after an established connection is lost."
506 }
507 interface_key::FIXED_MTU => {
508 "Overrides automatic MTU selection with a fixed packet size."
509 }
510 interface_key::LISTEN_IP => "Sets the local IP address this server listens on.",
511 interface_key::LISTEN_PORT => "Sets the local port this server listens on.",
512 interface_key::DEVICE => {
513 "Selects a local network or serial device, depending on the interface type."
514 }
515 interface_key::PORT => {
516 "Sets a listener port or serial path accepted by this interface type."
517 }
518 interface_key::PREFER_IPV6 => {
519 "Prefers IPv6 addresses when both address families are available."
520 }
521 interface_key::FORWARD_IP => "Sets the UDP address that outgoing packets are sent to.",
522 interface_key::FORWARD_PORT => "Sets the UDP port that outgoing packets are sent to.",
523 interface_key::SPEED => "Sets the serial line speed in bits per second.",
524 interface_key::DATABITS => "Sets the number of data bits in each serial character.",
525 interface_key::PARITY => "Sets serial parity checking.",
526 interface_key::STOPBITS => "Sets the number of serial stop bits.",
527 interface_key::FLOW_CONTROL => {
528 "Enables hardware or ready-command flow control supported by this interface."
529 }
530 interface_key::PREAMBLE => "Sets the KISS modem preamble duration.",
531 interface_key::TXTAIL => "Sets the KISS modem transmit-tail duration.",
532 interface_key::PERSISTENCE => "Sets the KISS channel-access persistence value.",
533 interface_key::SLOTTIME => "Sets the KISS channel-access slot duration.",
534 interface_key::ID_CALLSIGN => "Sets the station-identification callsign.",
535 interface_key::ID_INTERVAL => {
536 "Sets the interval between station-identification transmissions."
537 }
538 interface_key::CALLSIGN => "Sets the AX.25 callsign used by this interface.",
539 interface_key::SSID => "Sets the AX.25 secondary station identifier.",
540 interface_key::FREQUENCY => "Sets the RNode radio carrier frequency in hertz.",
541 interface_key::BANDWIDTH => "Sets the RNode radio bandwidth in hertz.",
542 interface_key::SPREADINGFACTOR => "Sets the RNode LoRa spreading factor.",
543 interface_key::CODINGRATE => "Sets the RNode LoRa coding rate.",
544 interface_key::TXPOWER => "Sets the RNode transmit power in dBm.",
545 interface_key::AIRTIME_LIMIT_SHORT => {
546 "Sets the short-term radio airtime limit as a percentage."
547 }
548 interface_key::AIRTIME_LIMIT_LONG => {
549 "Sets the long-term radio airtime limit as a percentage."
550 }
551 interface_key::COMMAND => "Sets the executable command used by this pipe interface.",
552 interface_key::RESPAWN_DELAY => {
553 "Sets how long the pipe interface waits before restarting its command."
554 }
555 interface_key::REMOTE => "Sets the remote Backbone address.",
556 interface_key::LISTEN_ON => "Sets the local Backbone listener address.",
557 interface_key::PEERS => "Lists the I2P destinations this interface connects to.",
558 interface_key::CONNECTABLE => {
559 "Allows other I2P peers to establish inbound connections to this interface."
560 }
561 common_key::INGRESS_CONTROL => {
562 "Enables burst control for announcements and path requests entering this interface."
563 }
564 common_key::EGRESS_CONTROL => {
565 "Enables rate control for path requests leaving this interface."
566 }
567 common_key::IC_MAX_HELD_ANNOUNCES => {
568 "Sets the maximum announcements ingress control may hold for later release."
569 }
570 common_key::IC_BURST_HOLD => {
571 "Sets how long ingress control holds traffic after detecting a burst."
572 }
573 common_key::IC_BURST_FREQ_NEW => {
574 "Sets the announcement burst threshold while an interface is considered new."
575 }
576 common_key::IC_BURST_FREQ => {
577 "Sets the normal announcement burst threshold for ingress control."
578 }
579 common_key::IC_PR_BURST_FREQ_NEW => {
580 "Sets the path-request burst threshold while an interface is considered new."
581 }
582 common_key::IC_PR_BURST_FREQ => {
583 "Sets the normal path-request burst threshold for ingress control."
584 }
585 common_key::EC_PR_FREQ => {
586 "Sets the maximum path-request frequency allowed by egress control."
587 }
588 common_key::IC_NEW_TIME => {
589 "Sets how long ingress control treats this interface as newly started."
590 }
591 common_key::IC_BURST_PENALTY => {
592 "Sets the additional hold time applied after repeated ingress bursts."
593 }
594 common_key::IC_HELD_RELEASE_INTERVAL => {
595 "Sets the interval between announcements released from the ingress-control queue."
596 }
597 _ => "Configures an advanced value accepted by this interface type.",
598 }
599 }
600
601 pub fn default_hint(self, kind: InterfaceKind) -> Option<&'static str> {
602 match self.key.as_str() {
603 interface_key::INTERFACE_MODE
604 if matches!(
605 kind,
606 InterfaceKind::PrnsUsbAuto
607 | InterfaceKind::PrnsWebSocketClient
608 | InterfaceKind::PrnsWebSocketServer
609 ) =>
610 {
611 Some("pointtopoint")
612 }
613 interface_key::INTERFACE_MODE => Some("full"),
614 interface_key::OUTGOING => Some("Yes"),
615 interface_key::GRAVITY => Some("0"),
616 interface_key::ANNOUNCE_CAP => Some("2%"),
617 interface_key::NETWORK_NAME | interface_key::PASS_PHRASE => Some("not set"),
618 interface_key::IFAC_SIZE
619 if matches!(
620 kind,
621 InterfaceKind::Serial
622 | InterfaceKind::Kiss
623 | InterfaceKind::Ax25Kiss
624 | InterfaceKind::Rnode
625 | InterfaceKind::RnodeMulti
626 | InterfaceKind::Pipe
627 | InterfaceKind::PrnsBluetoothAuto
628 ) =>
629 {
630 Some("64 bits when IFAC is enabled")
631 }
632 interface_key::IFAC_SIZE => Some("128 bits when IFAC is enabled"),
633 interface_key::DISCOVERABLE => Some("No"),
634 interface_key::ANNOUNCE_INTERVAL => Some("360 minutes"),
635 interface_key::DISCOVERY_STAMP_VALUE => Some("14"),
636 interface_key::DISCOVERY_ENCRYPT | interface_key::PUBLISH_IFAC => Some("No"),
637 interface_key::BOOTSTRAP_ONLY => Some("No"),
638 interface_key::RECURSIVE_PRS => Some("No"),
639 interface_key::ANNOUNCES_FROM_INTERNAL => Some("Yes"),
640 interface_key::ANNOUNCES_TO_INTERNAL => Some("No"),
641 interface_key::GROUP_ID if kind == InterfaceKind::Auto => Some("reticulum"),
642 interface_key::DISCOVERY_SCOPE if kind == InterfaceKind::Auto => Some("link"),
643 interface_key::DISCOVERY_PORT if kind == InterfaceKind::Auto => Some("29716"),
644 interface_key::DATA_PORT if kind == InterfaceKind::Auto => Some("42671"),
645 interface_key::DEVICES if kind == InterfaceKind::Auto => Some("all usable devices"),
646 interface_key::IGNORED_DEVICES if kind == InterfaceKind::Auto => Some("none"),
647 interface_key::MULTICAST_ADDRESS_TYPE if kind == InterfaceKind::Auto => {
648 Some("temporary")
649 }
650 common_key::INGRESS_CONTROL => Some("Yes"),
651 common_key::EGRESS_CONTROL => Some("No"),
652 common_key::IC_MAX_HELD_ANNOUNCES => Some("256"),
653 common_key::IC_BURST_HOLD => Some("15 seconds"),
654 common_key::IC_BURST_FREQ_NEW => Some("3 Hz"),
655 common_key::IC_BURST_FREQ => Some("10 Hz"),
656 common_key::IC_PR_BURST_FREQ_NEW => Some("3 Hz"),
657 common_key::IC_PR_BURST_FREQ => Some("8 Hz"),
658 common_key::EC_PR_FREQ => Some("5 Hz"),
659 common_key::IC_NEW_TIME => Some("7200 seconds"),
660 common_key::IC_BURST_PENALTY => Some("15 seconds"),
661 common_key::IC_HELD_RELEASE_INTERVAL => Some("5 seconds"),
662 _ => None,
663 }
664 }
665
666 pub fn required_hint(self, kind: InterfaceKind) -> Option<&'static str> {
667 match (kind, self.key.as_str()) {
668 (InterfaceKind::TcpClient, interface_key::TARGET_HOST)
669 | (InterfaceKind::BackboneClient, interface_key::TARGET_HOST) => {
670 Some("a remote host is required")
671 }
672 (InterfaceKind::TcpClient, interface_key::TARGET_PORT)
673 | (InterfaceKind::BackboneClient, interface_key::TARGET_PORT) => {
674 Some("a remote port is required")
675 }
676 (
677 InterfaceKind::TcpServer
678 | InterfaceKind::Backbone
679 | InterfaceKind::PrnsWebSocketServer,
680 interface_key::LISTEN_PORT,
681 ) => Some("a listener port is required"),
682 (
683 InterfaceKind::Serial
684 | InterfaceKind::Kiss
685 | InterfaceKind::Ax25Kiss
686 | InterfaceKind::Rnode
687 | InterfaceKind::RnodeMulti
688 | InterfaceKind::Weave,
689 interface_key::PORT,
690 ) => Some("a device or transport target is required"),
691 (InterfaceKind::Ax25Kiss, interface_key::CALLSIGN) => Some("a callsign is required"),
692 (InterfaceKind::Ax25Kiss, interface_key::SSID) => Some("an SSID is required"),
693 (
694 InterfaceKind::Rnode,
695 interface_key::FREQUENCY
696 | interface_key::BANDWIDTH
697 | interface_key::SPREADINGFACTOR
698 | interface_key::CODINGRATE
699 | interface_key::TXPOWER,
700 ) => Some("a radio value is required"),
701 (InterfaceKind::Pipe, interface_key::COMMAND) => Some("a command is required"),
702 (InterfaceKind::PrnsWebSocketClient, interface_key::TARGET) => {
703 Some("a ws:// or wss:// target is required")
704 }
705 _ => None,
706 }
707 }
708
709 pub fn inherits_when_unset(self) -> bool {
710 matches!(
711 self.key.as_str(),
712 interface_key::ANNOUNCE_RATE_TARGET
713 | interface_key::ANNOUNCE_RATE_GRACE
714 | interface_key::ANNOUNCE_RATE_PENALTY
715 | interface_key::GRAVITY
716 | interface_key::RECURSIVE_PRS
717 | interface_key::ANNOUNCES_FROM_INTERNAL
718 | interface_key::ANNOUNCES_TO_INTERNAL
719 | common_key::INGRESS_CONTROL
720 | common_key::EGRESS_CONTROL
721 | common_key::IC_MAX_HELD_ANNOUNCES
722 | common_key::IC_BURST_HOLD
723 | common_key::IC_BURST_FREQ_NEW
724 | common_key::IC_BURST_FREQ
725 | common_key::IC_PR_BURST_FREQ_NEW
726 | common_key::IC_PR_BURST_FREQ
727 | common_key::EC_PR_FREQ
728 | common_key::IC_NEW_TIME
729 | common_key::IC_BURST_PENALTY
730 | common_key::IC_HELD_RELEASE_INTERVAL
731 )
732 }
733
734 pub fn effective_value(self, planned: &PlannedInterface) -> Option<String> {
735 let policy = &planned.policy;
736 let common = &policy.common;
737 match self.key.as_str() {
738 interface_key::INTERFACE_MODE => Some(interface_mode_name(policy.mode).to_string()),
739 interface_key::OUTGOING => Some(yes_no(!matches!(
740 policy.capabilities.egress,
741 EgressCapability::Disabled
742 ))),
743 interface_key::BITRATE => Some(policy.bitrate.get().to_string()),
744 interface_key::ANNOUNCE_CAP => Some(match policy.announce_bandwidth_cap {
745 AnnounceBandwidthCap::Unlimited => "unlimited".to_string(),
746 AnnounceBandwidthCap::Limited { cap_per_mille } => {
747 format!("{}%", concise_decimal(f64::from(cap_per_mille) / 10.0))
748 }
749 }),
750 interface_key::ANNOUNCE_RATE_TARGET => policy
751 .announce_rate_limit
752 .map(|limit| concise_decimal(limit.target_ms as f64 / 1_000.0)),
753 interface_key::ANNOUNCE_RATE_GRACE => policy
754 .announce_rate_limit
755 .map(|limit| limit.grace.to_string()),
756 interface_key::ANNOUNCE_RATE_PENALTY => policy
757 .announce_rate_limit
758 .map(|limit| concise_decimal(limit.penalty_ms as f64 / 1_000.0)),
759 interface_key::NETWORK_NAME => match &planned.access {
760 InterfaceAccessPlan::Ifac { network_name, .. } => network_name.clone(),
761 InterfaceAccessPlan::Open => None,
762 },
763 interface_key::PASS_PHRASE => match &planned.access {
764 InterfaceAccessPlan::Ifac { passphrase, .. } => passphrase.clone(),
765 InterfaceAccessPlan::Open => None,
766 },
767 interface_key::IFAC_SIZE => match planned.access {
768 InterfaceAccessPlan::Ifac { size, .. } => Some((size.bytes() * 8).to_string()),
769 InterfaceAccessPlan::Open => None,
770 },
771 interface_key::DISCOVERABLE => Some(yes_no(!matches!(
772 planned.discovery,
773 InterfaceDiscoveryPlan::Disabled
774 ))),
775 interface_key::ANNOUNCE_INTERVAL => discovery_announcement(planned)
776 .map(|announcement| (announcement.interval.0 / 60_000).to_string()),
777 interface_key::DISCOVERY_STAMP_VALUE => discovery_announcement(planned)
778 .map(|announcement| announcement.stamp_cost.get().to_string()),
779 interface_key::DISCOVERY_NAME => {
780 discovery_announcement(planned).and_then(|announcement| announcement.name.clone())
781 }
782 interface_key::DISCOVERY_ENCRYPT => {
783 discovery_announcement(planned).map(|announcement| {
784 yes_no(matches!(
785 announcement.encryption,
786 DiscoveryEncryption::NetworkIdentity
787 ))
788 })
789 }
790 interface_key::PUBLISH_IFAC => discovery_announcement(planned).map(|announcement| {
791 yes_no(matches!(
792 announcement.ifac,
793 DiscoveryIfacPublication::Include
794 ))
795 }),
796 interface_key::REACHABLE_ON => {
797 discovery_advertisement(planned).and_then(|value| match value {
798 DiscoveryAdvertisementPlan::Backbone { reachable_on, .. }
799 | DiscoveryAdvertisementPlan::TcpServer { reachable_on, .. } => {
800 Some(reachable_on.clone())
801 }
802 _ => None,
803 })
804 }
805 interface_key::REACHABLE_PORT => {
806 discovery_advertisement(planned).and_then(|value| match value {
807 DiscoveryAdvertisementPlan::Backbone { port, .. }
808 | DiscoveryAdvertisementPlan::TcpServer { port, .. } => Some(port.to_string()),
809 _ => None,
810 })
811 }
812 interface_key::LATITUDE => discovery_announcement(planned)
813 .and_then(|announcement| announcement.location.latitude)
814 .map(concise_decimal),
815 interface_key::LONGITUDE => discovery_announcement(planned)
816 .and_then(|announcement| announcement.location.longitude)
817 .map(concise_decimal),
818 interface_key::HEIGHT => discovery_announcement(planned)
819 .and_then(|announcement| announcement.location.height)
820 .map(concise_decimal),
821 interface_key::DISCOVERY_FREQUENCY => {
822 discovery_advertisement(planned).and_then(|value| match value {
823 DiscoveryAdvertisementPlan::Kiss { frequency_hz, .. } => {
824 Some(frequency_hz.to_string())
825 }
826 _ => None,
827 })
828 }
829 interface_key::DISCOVERY_BANDWIDTH => {
830 discovery_advertisement(planned).and_then(|value| match value {
831 DiscoveryAdvertisementPlan::Kiss { bandwidth_hz, .. } => {
832 Some(bandwidth_hz.to_string())
833 }
834 _ => None,
835 })
836 }
837 interface_key::DISCOVERY_MODULATION => {
838 discovery_advertisement(planned).and_then(|value| match value {
839 DiscoveryAdvertisementPlan::Kiss { modulation, .. } => Some(modulation.clone()),
840 _ => None,
841 })
842 }
843 interface_key::GRAVITY => Some(planned.policy.gravity.get().to_string()),
844 interface_key::BOOTSTRAP_ONLY => Some(yes_no(matches!(
845 planned.lifecycle,
846 ConfiguredInterfaceLifecycle::BootstrapOnly
847 ))),
848 interface_key::RECURSIVE_PRS => match common.forwarding.recursive_path_requests {
849 RecursivePathRequestPolicy::InheritNode => None,
850 RecursivePathRequestPolicy::Enabled => Some(yes_no(true)),
851 RecursivePathRequestPolicy::Disabled => Some(yes_no(false)),
852 },
853 interface_key::ANNOUNCES_FROM_INTERNAL => {
854 Some(yes_no(common.forwarding.announces_from_internal))
855 }
856 interface_key::ANNOUNCES_TO_INTERNAL => {
857 Some(yes_no(common.forwarding.announces_to_internal))
858 }
859 common_key::INGRESS_CONTROL => Some(yes_no(common.ingress_control.enabled)),
860 common_key::EGRESS_CONTROL => Some(yes_no(common.path_request_egress.enabled)),
861 common_key::IC_MAX_HELD_ANNOUNCES => {
862 Some(common.ingress_control.max_held_announces.to_string())
863 }
864 common_key::IC_BURST_HOLD => Some(concise_decimal(
865 common.ingress_control.burst_hold_millis as f64 / 1_000.0,
866 )),
867 common_key::IC_BURST_FREQ_NEW => Some(concise_decimal(
868 common.ingress_control.announce_burst_frequency_new.get() as f64 / 1_000.0,
869 )),
870 common_key::IC_BURST_FREQ => Some(concise_decimal(
871 common.ingress_control.announce_burst_frequency.get() as f64 / 1_000.0,
872 )),
873 common_key::IC_PR_BURST_FREQ_NEW => Some(concise_decimal(
874 common
875 .ingress_control
876 .path_request_burst_frequency_new
877 .get() as f64
878 / 1_000.0,
879 )),
880 common_key::IC_PR_BURST_FREQ => Some(concise_decimal(
881 common.ingress_control.path_request_burst_frequency.get() as f64 / 1_000.0,
882 )),
883 common_key::EC_PR_FREQ => Some(concise_decimal(
884 common.path_request_egress.frequency.get() as f64 / 1_000.0,
885 )),
886 common_key::IC_NEW_TIME => Some(concise_decimal(
887 common.ingress_control.new_interface_millis as f64 / 1_000.0,
888 )),
889 common_key::IC_BURST_PENALTY => Some(concise_decimal(
890 common.ingress_control.burst_penalty_millis as f64 / 1_000.0,
891 )),
892 common_key::IC_HELD_RELEASE_INTERVAL => Some(concise_decimal(
893 common.ingress_control.held_release_interval_millis as f64 / 1_000.0,
894 )),
895 interface_key::SPEED => serial_line(planned).map(|line| line.baud().to_string()),
896 interface_key::DATABITS => serial_line(planned).map(|line| match line.data_bits() {
897 crate::SerialDataBits::Five => "5".to_string(),
898 crate::SerialDataBits::Six => "6".to_string(),
899 crate::SerialDataBits::Seven => "7".to_string(),
900 crate::SerialDataBits::Eight => "8".to_string(),
901 }),
902 interface_key::PARITY => serial_line(planned).map(|line| match line.parity() {
903 crate::SerialParity::None => "none".to_string(),
904 crate::SerialParity::Even => "even".to_string(),
905 crate::SerialParity::Odd => "odd".to_string(),
906 }),
907 interface_key::STOPBITS => serial_line(planned).map(|line| match line.stop_bits() {
908 crate::SerialStopBits::One => "1".to_string(),
909 crate::SerialStopBits::Two => "2".to_string(),
910 }),
911 interface_key::FLOW_CONTROL => match &planned.medium {
912 PlannedMedium::Kiss { flow_control, .. }
913 | PlannedMedium::Ax25Kiss { flow_control, .. }
914 | PlannedMedium::Rnode { flow_control, .. } => Some(yes_no(matches!(
915 flow_control,
916 crate::ReadyCommandFlowControl::Enabled
917 ))),
918 _ => None,
919 },
920 interface_key::PREAMBLE => match &planned.medium {
921 PlannedMedium::Kiss { preamble_ms, .. }
922 | PlannedMedium::Ax25Kiss { preamble_ms, .. } => Some(preamble_ms.to_string()),
923 _ => None,
924 },
925 interface_key::TXTAIL => match &planned.medium {
926 PlannedMedium::Kiss { txtail_ms, .. }
927 | PlannedMedium::Ax25Kiss { txtail_ms, .. } => Some(txtail_ms.to_string()),
928 _ => None,
929 },
930 interface_key::PERSISTENCE => match &planned.medium {
931 PlannedMedium::Kiss { persistence, .. }
932 | PlannedMedium::Ax25Kiss { persistence, .. } => Some(persistence.to_string()),
933 _ => None,
934 },
935 interface_key::SLOTTIME => match &planned.medium {
936 PlannedMedium::Kiss { slottime_ms, .. }
937 | PlannedMedium::Ax25Kiss { slottime_ms, .. } => Some(slottime_ms.to_string()),
938 _ => None,
939 },
940 interface_key::ID_CALLSIGN => match &planned.medium {
941 PlannedMedium::Kiss {
942 station_id: Some(station),
943 ..
944 }
945 | PlannedMedium::Rnode {
946 station_id: Some(station),
947 ..
948 } => Some(station.callsign().to_string()),
949 _ => None,
950 },
951 interface_key::ID_INTERVAL => match &planned.medium {
952 PlannedMedium::Kiss {
953 station_id: Some(station),
954 ..
955 }
956 | PlannedMedium::Rnode {
957 station_id: Some(station),
958 ..
959 } => Some(station.interval_seconds().to_string()),
960 _ => None,
961 },
962 interface_key::CALLSIGN => match &planned.medium {
963 PlannedMedium::Ax25Kiss { callsign, .. } => Some(callsign.clone()),
964 _ => None,
965 },
966 interface_key::SSID => match &planned.medium {
967 PlannedMedium::Ax25Kiss { ssid, .. } => Some(ssid.to_string()),
968 _ => None,
969 },
970 interface_key::FREQUENCY => match &planned.medium {
971 PlannedMedium::Rnode { frequency_hz, .. } => Some(frequency_hz.to_string()),
972 _ => None,
973 },
974 interface_key::BANDWIDTH => match &planned.medium {
975 PlannedMedium::Rnode { bandwidth_hz, .. } => Some(bandwidth_hz.to_string()),
976 _ => None,
977 },
978 interface_key::SPREADINGFACTOR => match &planned.medium {
979 PlannedMedium::Rnode {
980 spreading_factor, ..
981 } => Some(spreading_factor.to_string()),
982 _ => None,
983 },
984 interface_key::CODINGRATE => match &planned.medium {
985 PlannedMedium::Rnode { coding_rate, .. } => Some(coding_rate.to_string()),
986 _ => None,
987 },
988 interface_key::TXPOWER => match &planned.medium {
989 PlannedMedium::Rnode { tx_power_dbm, .. } => Some(tx_power_dbm.to_string()),
990 _ => None,
991 },
992 interface_key::AIRTIME_LIMIT_SHORT => match &planned.medium {
993 PlannedMedium::Rnode {
994 airtime_limit_short,
995 ..
996 } => {
997 airtime_limit_short.map(|limit| concise_decimal(f64::from(limit.get()) / 100.0))
998 }
999 _ => None,
1000 },
1001 interface_key::AIRTIME_LIMIT_LONG => match &planned.medium {
1002 PlannedMedium::Rnode {
1003 airtime_limit_long, ..
1004 } => {
1005 airtime_limit_long.map(|limit| concise_decimal(f64::from(limit.get()) / 100.0))
1006 }
1007 _ => None,
1008 },
1009 interface_key::COMMAND => match &planned.medium {
1010 PlannedMedium::Pipe { command, .. } => Some(command.source().to_string()),
1011 _ => None,
1012 },
1013 interface_key::RESPAWN_DELAY => match &planned.medium {
1014 PlannedMedium::Pipe { respawn_delay, .. } => {
1015 Some(concise_decimal(respawn_delay.get().as_secs_f64()))
1016 }
1017 _ => None,
1018 },
1019 interface_key::PEERS => match &planned.medium {
1020 PlannedMedium::I2p { peers, .. } => Some(if peers.is_empty() {
1021 "none".to_string()
1022 } else {
1023 peers
1024 .iter()
1025 .map(|peer| peer.as_str())
1026 .collect::<Vec<_>>()
1027 .join(", ")
1028 }),
1029 _ => None,
1030 },
1031 interface_key::CONNECTABLE => match &planned.medium {
1032 PlannedMedium::I2p { reachability, .. } => {
1033 Some(yes_no(reachability.is_connectable()))
1034 }
1035 _ => None,
1036 },
1037 interface_key::TARGET => match &planned.medium {
1038 PlannedMedium::PrnsWebSocketClient { target } => Some(target.as_str().to_string()),
1039 _ => None,
1040 },
1041 interface_key::GROUP_ID => {
1042 auto_plan(planned).map(|auto| auto.group_id().as_str().to_string())
1043 }
1044 interface_key::DISCOVERY_SCOPE => auto_plan(planned)
1045 .map(|auto| format!("{:?}", auto.discovery_scope()).to_ascii_lowercase()),
1046 interface_key::DISCOVERY_PORT => {
1047 auto_plan(planned).map(|auto| auto.discovery_port().get().to_string())
1048 }
1049 interface_key::DATA_PORT => {
1050 auto_plan(planned).map(|auto| auto.data_port().get().to_string())
1051 }
1052 interface_key::DEVICES => auto_plan(planned).map(|auto| {
1053 if auto.devices().allowed().is_empty() {
1054 "all usable devices".to_string()
1055 } else {
1056 auto.devices().allowed().join(", ")
1057 }
1058 }),
1059 interface_key::IGNORED_DEVICES => auto_plan(planned).map(|auto| {
1060 if auto.devices().ignored().is_empty() {
1061 "none".to_string()
1062 } else {
1063 auto.devices().ignored().join(", ")
1064 }
1065 }),
1066 interface_key::MULTICAST_ADDRESS_TYPE => auto_plan(planned)
1067 .map(|auto| format!("{:?}", auto.multicast_address_type()).to_ascii_lowercase()),
1068 _ => None,
1069 }
1070 }
1071
1072 pub fn input_kind(self, kind: InterfaceKind) -> InterfaceSettingInputKind {
1073 match self.key.as_str() {
1074 interface_key::ANNOUNCE_RATE_TARGET => InterfaceSettingInputKind::Text,
1075 interface_key::OUTGOING
1076 | interface_key::DISCOVERABLE
1077 | interface_key::DISCOVERY_ENCRYPT
1078 | interface_key::PUBLISH_IFAC
1079 | interface_key::BOOTSTRAP_ONLY
1080 | interface_key::RECURSIVE_PRS
1081 | interface_key::ANNOUNCES_FROM_INTERNAL
1082 | interface_key::ANNOUNCES_TO_INTERNAL
1083 | interface_key::IGNORE_CONFIG_WARNINGS
1084 | interface_key::KISS_FRAMING
1085 | interface_key::I2P_TUNNELED
1086 | interface_key::PREFER_IPV6
1087 | interface_key::FLOW_CONTROL
1088 | interface_key::CONNECTABLE
1089 | common_key::INGRESS_CONTROL
1090 | common_key::EGRESS_CONTROL => InterfaceSettingInputKind::Boolean,
1091 interface_key::BITRATE
1092 | interface_key::ANNOUNCE_RATE_GRACE
1093 | interface_key::ANNOUNCE_RATE_PENALTY
1094 | interface_key::IFAC_SIZE
1095 | interface_key::DISCOVERY_STAMP_VALUE
1096 | interface_key::DISCOVERY_FREQUENCY
1097 | interface_key::DISCOVERY_BANDWIDTH
1098 | interface_key::CONNECT_TIMEOUT
1099 | interface_key::MAX_RECONNECT_TRIES
1100 | interface_key::FIXED_MTU
1101 | interface_key::SPEED
1102 | interface_key::DATABITS
1103 | interface_key::STOPBITS
1104 | interface_key::PREAMBLE
1105 | interface_key::TXTAIL
1106 | interface_key::PERSISTENCE
1107 | interface_key::SLOTTIME
1108 | interface_key::ID_INTERVAL
1109 | interface_key::SSID
1110 | interface_key::FREQUENCY
1111 | interface_key::BANDWIDTH
1112 | interface_key::SPREADINGFACTOR
1113 | interface_key::CODINGRATE => InterfaceSettingInputKind::Unsigned,
1114 interface_key::ANNOUNCE_INTERVAL
1115 | interface_key::GRAVITY
1116 | interface_key::TXPOWER
1117 | common_key::IC_MAX_HELD_ANNOUNCES => InterfaceSettingInputKind::Signed,
1118 interface_key::ANNOUNCE_CAP
1119 | interface_key::LATITUDE
1120 | interface_key::LONGITUDE
1121 | interface_key::HEIGHT
1122 | interface_key::AIRTIME_LIMIT_SHORT
1123 | interface_key::AIRTIME_LIMIT_LONG
1124 | interface_key::RESPAWN_DELAY
1125 | common_key::IC_BURST_HOLD
1126 | common_key::IC_BURST_FREQ_NEW
1127 | common_key::IC_BURST_FREQ
1128 | common_key::IC_PR_BURST_FREQ_NEW
1129 | common_key::IC_PR_BURST_FREQ
1130 | common_key::EC_PR_FREQ
1131 | common_key::IC_NEW_TIME
1132 | common_key::IC_BURST_PENALTY
1133 | common_key::IC_HELD_RELEASE_INTERVAL => InterfaceSettingInputKind::Decimal,
1134 interface_key::DEVICES | interface_key::IGNORED_DEVICES | interface_key::PEERS => {
1135 InterfaceSettingInputKind::List
1136 }
1137 interface_key::DISCOVERY_PORT
1138 | interface_key::DATA_PORT
1139 | interface_key::TARGET_PORT
1140 | interface_key::LISTEN_PORT
1141 | interface_key::FORWARD_PORT => InterfaceSettingInputKind::Port,
1142 interface_key::PORT
1143 if matches!(
1144 kind,
1145 InterfaceKind::TcpServer
1146 | InterfaceKind::Udp
1147 | InterfaceKind::Backbone
1148 | InterfaceKind::BackboneClient
1149 | InterfaceKind::PrnsWebSocketServer
1150 ) =>
1151 {
1152 InterfaceSettingInputKind::Port
1153 }
1154 _ => InterfaceSettingInputKind::Text,
1155 }
1156 }
1157
1158 pub fn accepted(self, kind: InterfaceKind) -> &'static str {
1159 match self.key.as_str() {
1160 interface_key::INTERFACE_MODE => {
1161 "full, access_point, pointtopoint, roaming, boundary, gateway, or internal"
1162 }
1163 interface_key::ANNOUNCE_CAP => "a percentage from 0 through 100",
1164 interface_key::ANNOUNCE_RATE_TARGET => {
1165 "off, no, false, or seconds as a non-negative whole number"
1166 }
1167 interface_key::ANNOUNCE_RATE_PENALTY
1168 | interface_key::CONNECT_TIMEOUT
1169 | interface_key::ID_INTERVAL => "seconds as a non-negative whole number",
1170 interface_key::RESPAWN_DELAY => "seconds as a non-negative number",
1171 interface_key::ANNOUNCE_INTERVAL => "minutes as a whole number",
1172 interface_key::IFAC_SIZE => "an IFAC size from 8 through 512 bits",
1173 interface_key::FIXED_MTU => "bytes as a non-negative whole number",
1174 interface_key::PREAMBLE | interface_key::TXTAIL | interface_key::SLOTTIME => {
1175 "milliseconds as a non-negative whole number"
1176 }
1177 interface_key::DISCOVERY_SCOPE => "link, admin, site, organisation, or global",
1178 interface_key::MULTICAST_ADDRESS_TYPE => "temporary or permanent",
1179 interface_key::BITRATE | interface_key::SPEED => {
1180 "bits per second as a non-negative whole number"
1181 }
1182 interface_key::DISCOVERY_FREQUENCY | interface_key::FREQUENCY => {
1183 "hertz as a non-negative whole number"
1184 }
1185 interface_key::DISCOVERY_BANDWIDTH | interface_key::BANDWIDTH => {
1186 "hertz as a non-negative whole number"
1187 }
1188 interface_key::TXPOWER => "dBm as a whole number",
1189 interface_key::AIRTIME_LIMIT_SHORT | interface_key::AIRTIME_LIMIT_LONG => {
1190 "a percentage"
1191 }
1192 interface_key::PARITY => "none, even, or odd",
1193 _ => match self.input_kind(kind) {
1194 InterfaceSettingInputKind::Boolean => "yes or no",
1195 InterfaceSettingInputKind::Unsigned => "a non-negative whole number",
1196 InterfaceSettingInputKind::Signed => "a whole number",
1197 InterfaceSettingInputKind::Decimal => "a number",
1198 InterfaceSettingInputKind::Text => "text",
1199 InterfaceSettingInputKind::List => "a comma-separated list",
1200 InterfaceSettingInputKind::Port => "a port from 0 through 65535",
1201 },
1202 }
1203 }
1204
1205 pub fn format_value(self, value: impl AsRef<str>) -> String {
1206 let value = value.as_ref();
1207 if self.key.as_str() == interface_key::ANNOUNCE_RATE_TARGET
1208 && announce_rate_target_is_explicit_off(value)
1209 {
1210 return "off".to_string();
1211 }
1212 if matches!(
1213 self.key.as_str(),
1214 interface_key::AIRTIME_LIMIT_SHORT | interface_key::AIRTIME_LIMIT_LONG
1215 ) {
1216 return format!("{value}%");
1217 }
1218 if matches!(
1219 self.key.as_str(),
1220 interface_key::BITRATE | interface_key::SPEED
1221 ) {
1222 return value
1223 .replace('_', "")
1224 .parse::<u64>()
1225 .map_or_else(|_| value.to_string(), format_si_bitrate);
1226 }
1227 if matches!(
1228 self.key.as_str(),
1229 interface_key::DISCOVERY_FREQUENCY
1230 | interface_key::DISCOVERY_BANDWIDTH
1231 | interface_key::FREQUENCY
1232 | interface_key::BANDWIDTH
1233 ) {
1234 return value
1235 .replace('_', "")
1236 .parse::<u64>()
1237 .map_or_else(|_| format!("{value} Hz"), format_si_frequency);
1238 }
1239 let unit = match self.key.as_str() {
1240 interface_key::ANNOUNCE_RATE_TARGET
1241 | interface_key::ANNOUNCE_RATE_PENALTY
1242 | interface_key::CONNECT_TIMEOUT
1243 | interface_key::ID_INTERVAL
1244 | interface_key::RESPAWN_DELAY
1245 | common_key::IC_BURST_HOLD
1246 | common_key::IC_NEW_TIME
1247 | common_key::IC_BURST_PENALTY
1248 | common_key::IC_HELD_RELEASE_INTERVAL => Some("seconds"),
1249 interface_key::ANNOUNCE_INTERVAL => Some("minutes"),
1250 interface_key::IFAC_SIZE => Some("bits"),
1251 common_key::IC_BURST_FREQ_NEW
1252 | common_key::IC_BURST_FREQ
1253 | common_key::IC_PR_BURST_FREQ_NEW
1254 | common_key::IC_PR_BURST_FREQ
1255 | common_key::EC_PR_FREQ => Some("Hz"),
1256 interface_key::PREAMBLE | interface_key::TXTAIL | interface_key::SLOTTIME => Some("ms"),
1257 interface_key::FIXED_MTU => Some("bytes"),
1258 interface_key::HEIGHT => Some("m"),
1259 interface_key::TXPOWER => Some("dBm"),
1260 _ => None,
1261 };
1262 unit.map_or_else(
1263 || value.to_string(),
1264 |unit| format!("{} {unit}", grouped_integer(value)),
1265 )
1266 }
1267
1268 pub fn parse(
1269 self,
1270 kind: InterfaceKind,
1271 input: &str,
1272 ) -> Result<InterfaceSetting, InterfaceSettingInputError> {
1273 if self.key.as_str() == interface_key::ANNOUNCE_RATE_TARGET {
1274 let value = if announce_rate_target_is_explicit_off(input) {
1275 InterfaceSettingValue::Text("off".to_string())
1276 } else {
1277 InterfaceSettingValue::Unsigned(
1278 cleaned_number(input)
1279 .parse()
1280 .map_err(|_| InterfaceSettingInputError::AnnounceRateTarget)?,
1281 )
1282 };
1283 return Ok(InterfaceSetting::new(self.key, value));
1284 }
1285 let value = match self.input_kind(kind) {
1286 InterfaceSettingInputKind::Boolean => parse_bool(input)
1287 .map(InterfaceSettingValue::Bool)
1288 .ok_or(InterfaceSettingInputError::Boolean)?,
1289 InterfaceSettingInputKind::Unsigned => InterfaceSettingValue::Unsigned(
1290 cleaned_number(input)
1291 .parse()
1292 .map_err(|_| InterfaceSettingInputError::Unsigned)?,
1293 ),
1294 InterfaceSettingInputKind::Signed => InterfaceSettingValue::Signed(
1295 cleaned_number(input)
1296 .parse()
1297 .map_err(|_| InterfaceSettingInputError::Signed)?,
1298 ),
1299 InterfaceSettingInputKind::Decimal => {
1300 let value = cleaned_number(input)
1301 .parse::<f64>()
1302 .map_err(|_| InterfaceSettingInputError::Decimal)?;
1303 if !value.is_finite() {
1304 return Err(InterfaceSettingInputError::Decimal);
1305 }
1306 InterfaceSettingValue::Decimal(value)
1307 }
1308 InterfaceSettingInputKind::Text => InterfaceSettingValue::Text(input.to_string()),
1309 InterfaceSettingInputKind::List => {
1310 let values = input
1311 .split(',')
1312 .map(str::trim)
1313 .filter(|value| !value.is_empty())
1314 .map(str::to_string)
1315 .collect::<Vec<_>>();
1316 if values.is_empty() {
1317 return Err(InterfaceSettingInputError::List);
1318 }
1319 InterfaceSettingValue::List(values)
1320 }
1321 InterfaceSettingInputKind::Port => InterfaceSettingValue::Unsigned(
1322 input
1323 .trim()
1324 .parse::<u16>()
1325 .map(u64::from)
1326 .map_err(|_| InterfaceSettingInputError::Port)?,
1327 ),
1328 };
1329 Ok(InterfaceSetting::new(self.key, value))
1330 }
1331
1332 pub fn is_secret(self) -> bool {
1333 self.key.is_secret()
1334 }
1335}
1336
1337fn interface_mode_name(mode: InterfaceMode) -> &'static str {
1338 match mode {
1339 InterfaceMode::Full => "full",
1340 InterfaceMode::PointToPoint => "pointtopoint",
1341 InterfaceMode::AccessPoint => "access_point",
1342 InterfaceMode::Roaming => "roaming",
1343 InterfaceMode::Boundary => "boundary",
1344 InterfaceMode::Gateway => "gateway",
1345 InterfaceMode::Internal => "internal",
1346 }
1347}
1348
1349fn yes_no(value: bool) -> String {
1350 if value { "Yes" } else { "No" }.to_string()
1351}
1352
1353fn concise_decimal(value: f64) -> String {
1354 let rendered = format!("{value:.3}");
1355 rendered
1356 .trim_end_matches('0')
1357 .trim_end_matches('.')
1358 .to_string()
1359}
1360
1361fn grouped_integer(value: &str) -> String {
1362 let cleaned = value.replace('_', "");
1363 let (sign, digits) = cleaned
1364 .strip_prefix('-')
1365 .map_or(("", cleaned.as_str()), |digits| ("-", digits));
1366 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
1367 return value.to_string();
1368 }
1369 let mut grouped = String::with_capacity(cleaned.len() + cleaned.len() / 3);
1370 grouped.push_str(sign);
1371 for (index, digit) in digits.chars().enumerate() {
1372 if index != 0 && (digits.len() - index).is_multiple_of(3) {
1373 grouped.push(',');
1374 }
1375 grouped.push(digit);
1376 }
1377 grouped
1378}
1379
1380fn format_si_bitrate(bits_per_second: u64) -> String {
1381 if bits_per_second >= 1_000_000_000 {
1382 format_scaled_quantity(bits_per_second, 1_000_000_000, 9, "Gbps")
1383 } else if bits_per_second >= 1_000_000 {
1384 format_scaled_quantity(bits_per_second, 1_000_000, 6, "Mbps")
1385 } else if bits_per_second >= 1_000 {
1386 format_scaled_quantity(bits_per_second, 1_000, 3, "kbps")
1387 } else {
1388 format!("{bits_per_second} bps")
1389 }
1390}
1391
1392fn format_si_frequency(hertz: u64) -> String {
1393 if hertz >= 1_000_000_000 {
1394 format_scaled_quantity(hertz, 1_000_000_000, 9, "GHz")
1395 } else if hertz >= 1_000_000 {
1396 format_scaled_quantity(hertz, 1_000_000, 6, "MHz")
1397 } else if hertz >= 1_000 {
1398 format_scaled_quantity(hertz, 1_000, 3, "kHz")
1399 } else {
1400 format!("{hertz} Hz")
1401 }
1402}
1403
1404fn format_scaled_quantity(value: u64, scale: u64, decimal_places: usize, unit: &str) -> String {
1405 let whole = value / scale;
1406 let remainder = value % scale;
1407 if remainder == 0 {
1408 return format!("{whole} {unit}");
1409 }
1410 let mut fractional = format!("{remainder:0decimal_places$}");
1411 while fractional.ends_with('0') {
1412 fractional.pop();
1413 }
1414 format!("{whole}.{fractional} {unit}")
1415}
1416
1417fn discovery_announcement(planned: &PlannedInterface) -> Option<&crate::DiscoveryAnnouncementPlan> {
1418 match &planned.discovery {
1419 InterfaceDiscoveryPlan::Announce(announcement) => Some(announcement),
1420 InterfaceDiscoveryPlan::Disabled | InterfaceDiscoveryPlan::Unpublishable(_) => None,
1421 }
1422}
1423
1424fn discovery_advertisement(planned: &PlannedInterface) -> Option<&DiscoveryAdvertisementPlan> {
1425 discovery_announcement(planned).map(|announcement| &announcement.advertisement)
1426}
1427
1428fn auto_plan(planned: &PlannedInterface) -> Option<&crate::AutoInterfacePlan> {
1429 match &planned.medium {
1430 PlannedMedium::AutoWifi(auto) => Some(auto),
1431 _ => None,
1432 }
1433}
1434
1435fn serial_line(planned: &PlannedInterface) -> Option<crate::SerialLinePlan> {
1436 match &planned.medium {
1437 PlannedMedium::Serial { line, .. }
1438 | PlannedMedium::Kiss { line, .. }
1439 | PlannedMedium::Ax25Kiss { line, .. } => Some(*line),
1440 _ => None,
1441 }
1442}
1443
1444impl InterfaceKind {
1445 pub fn setting_specs(self) -> Vec<InterfaceSettingSpec> {
1446 let mut specs = Vec::new();
1447 for key in ALL_SETTING_KEYS {
1448 let Some(key) = InterfaceSettingKey::parse(key) else {
1449 continue;
1450 };
1451 let canonical = key.canonical();
1452 if canonical != key
1453 || matches!(
1454 canonical.as_str(),
1455 interface_key::TYPE | interface_key::INTERFACE_ENABLED | interface_key::VPORT
1456 )
1457 || !self.accepts_setting(canonical.as_str())
1458 || specs
1459 .iter()
1460 .any(|spec: &InterfaceSettingSpec| spec.key == canonical)
1461 {
1462 continue;
1463 }
1464 specs.push(InterfaceSettingSpec { key: canonical });
1465 }
1466 specs.sort_by_key(|spec| (spec.category(), spec.key.as_str()));
1467 specs
1468 }
1469
1470 pub fn supported_setting_specs(self) -> Vec<InterfaceSettingSpec> {
1471 self.setting_specs()
1472 .into_iter()
1473 .filter(|spec| spec.is_supported(self))
1474 .collect()
1475 }
1476
1477 pub fn supports_editing_setting(self, key: InterfaceSettingKey) -> bool {
1478 self.setting_specs()
1479 .into_iter()
1480 .find(|spec| spec.key() == key.canonical())
1481 .is_some_and(|spec| spec.is_supported(self))
1482 }
1483}
1484
1485#[derive(Debug, Clone, PartialEq, Eq)]
1486pub struct ConfiguredInterfaceSetting {
1487 spec: InterfaceSettingSpec,
1488 source_key: String,
1489 value: String,
1490}
1491
1492impl ConfiguredInterfaceSetting {
1493 pub(crate) fn new(spec: InterfaceSettingSpec, source_key: String, value: String) -> Self {
1494 Self {
1495 spec,
1496 source_key,
1497 value,
1498 }
1499 }
1500
1501 pub const fn spec(&self) -> InterfaceSettingSpec {
1502 self.spec
1503 }
1504
1505 pub fn source_key(&self) -> &str {
1506 &self.source_key
1507 }
1508
1509 pub fn value(&self) -> &str {
1510 &self.value
1511 }
1512}
1513
1514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1515pub enum InterfaceSettingInputError {
1516 AnnounceRateTarget,
1517 Boolean,
1518 Unsigned,
1519 Signed,
1520 Decimal,
1521 List,
1522 Port,
1523}
1524
1525impl fmt::Display for InterfaceSettingInputError {
1526 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1527 formatter.write_str(match self {
1528 Self::AnnounceRateTarget => {
1529 "enter off, no, false, or a non-negative whole number of seconds"
1530 }
1531 Self::Boolean => "enter yes or no",
1532 Self::Unsigned => "enter a non-negative whole number",
1533 Self::Signed => "enter a whole number",
1534 Self::Decimal => "enter a finite number",
1535 Self::List => "enter at least one comma-separated value",
1536 Self::Port => "enter a port from 0 through 65535",
1537 })
1538 }
1539}
1540
1541impl std::error::Error for InterfaceSettingInputError {}
1542
1543fn parse_bool(input: &str) -> Option<bool> {
1544 match input.trim().to_ascii_lowercase().as_str() {
1545 "yes" | "true" | "on" | "1" => Some(true),
1546 "no" | "false" | "off" | "0" => Some(false),
1547 _ => None,
1548 }
1549}
1550
1551fn cleaned_number(input: &str) -> String {
1552 input
1553 .trim()
1554 .chars()
1555 .filter(|character| *character != '_')
1556 .collect()
1557}