Skip to main content

prns_config/editing/
catalog.rs

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}