Skip to main content

prns_config/plan/interface/
medium.rs

1use std::time::Duration;
2
3use prns_core::interfaces::rnode::policy as rnode_policy;
4use prns_core::interfaces::tcp::TcpWireFraming;
5pub use prns_core::interfaces::wifi_auto::{
6    DiscoveryScope as AutoInterfaceDiscoveryScope,
7    MulticastAddressType as AutoInterfaceMulticastAddressType,
8};
9use prns_core::interfaces::wifi_auto::{DEFAULT_DATA_PORT, DEFAULT_DISCOVERY_PORT, GROUP_NAME};
10use prns_core::interfaces::{BitrateBps, InterfaceDefaults};
11
12use super::PlanErrorKind;
13use crate::plan::rnode::RNodeTransportPlan;
14use crate::plan::RNodeMultiMemberPlan;
15use crate::reference::i2p::{validate_peer, validate_peers};
16use crate::reference::keys::interface as interface_key;
17use crate::reference::{ReferenceConfigParams, ReferenceInterface};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AddressFamilyPreference {
21    System,
22    Ipv4,
23    Ipv6,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum TcpTunnelMode {
28    Direct,
29    I2p,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ReconnectLimit {
34    Unlimited,
35    Attempts(u32),
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct ConnectTimeoutSeconds(u64);
40
41impl ConnectTimeoutSeconds {
42    pub const fn new(seconds: u64) -> Self {
43        Self(seconds)
44    }
45
46    pub const fn get(self) -> u64 {
47        self.0
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct TcpDialPlan {
53    pub host: String,
54    pub port: u16,
55    pub connect_timeout: ConnectTimeoutSeconds,
56    pub reconnect_limit: ReconnectLimit,
57    pub address_family: AddressFamilyPreference,
58    pub tunnel: TcpTunnelMode,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum TcpListenHost {
63    Any,
64    Address(String),
65    Device(String),
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct TcpListenPlan {
70    pub host: TcpListenHost,
71    pub port: u16,
72    pub address_family: AddressFamilyPreference,
73    pub tunnel: TcpTunnelMode,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum UdpEndpointHost {
78    Address(String),
79    DeviceBroadcast(String),
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct UdpEndpointPlan {
84    pub host: UdpEndpointHost,
85    pub port: u16,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum UdpFlowPlan {
90    ReceiveOnly {
91        listen: UdpEndpointPlan,
92    },
93    SendOnly {
94        forward: UdpEndpointPlan,
95    },
96    Bidirectional {
97        listen: UdpEndpointPlan,
98        forward: UdpEndpointPlan,
99    },
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum SerialDataBits {
104    Five,
105    Six,
106    Seven,
107    Eight,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum SerialParity {
112    None,
113    Even,
114    Odd,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum SerialStopBits {
119    One,
120    Two,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct SerialLinePlan {
125    pub(in crate::plan) baud: u32,
126    pub(in crate::plan) data_bits: SerialDataBits,
127    pub(in crate::plan) parity: SerialParity,
128    pub(in crate::plan) stop_bits: SerialStopBits,
129}
130
131impl SerialLinePlan {
132    pub const fn baud(self) -> u32 {
133        self.baud
134    }
135
136    pub const fn data_bits(self) -> SerialDataBits {
137        self.data_bits
138    }
139
140    pub const fn parity(self) -> SerialParity {
141        self.parity
142    }
143
144    pub const fn stop_bits(self) -> SerialStopBits {
145        self.stop_bits
146    }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum ReadyCommandFlowControl {
151    Disabled,
152    Enabled,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct StationIdentificationPlan {
157    pub(in crate::plan) callsign: String,
158    pub(in crate::plan) interval_seconds: u64,
159}
160
161impl StationIdentificationPlan {
162    pub fn callsign(&self) -> &str {
163        &self.callsign
164    }
165
166    pub const fn interval_seconds(&self) -> u64 {
167        self.interval_seconds
168    }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct AirtimeLimitCentiPercent(pub(in crate::plan) u16);
173
174impl AirtimeLimitCentiPercent {
175    pub const fn get(self) -> u16 {
176        self.0
177    }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct PipeRespawnDelay(pub(in crate::plan) std::time::Duration);
182
183impl PipeRespawnDelay {
184    pub const fn get(self) -> std::time::Duration {
185        self.0
186    }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct PipeCommandPlan {
191    pub(in crate::plan) source: String,
192    pub(in crate::plan) argv: Vec<String>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct WebSocketTargetPlan(String);
197
198impl WebSocketTargetPlan {
199    fn from_configured(target: String) -> Result<Self, PlanErrorKind> {
200        let target = target.trim();
201        if !crate::reference::supported_websocket_target(target) {
202            return Err(PlanErrorKind::InvalidSetting {
203                key: interface_key::TARGET,
204            });
205        }
206        Ok(Self(target.to_string()))
207    }
208
209    pub fn as_str(&self) -> &str {
210        &self.0
211    }
212}
213
214impl PipeCommandPlan {
215    pub fn source(&self) -> &str {
216        &self.source
217    }
218
219    pub fn argv(&self) -> &[String] {
220        &self.argv
221    }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
225pub struct I2pPeerPlan(String);
226
227impl I2pPeerPlan {
228    fn new(value: String) -> Result<Self, PlanErrorKind> {
229        validate_peer(&value).map_err(|_| PlanErrorKind::InvalidSetting {
230            key: interface_key::PEERS,
231        })?;
232        Ok(Self(value))
233    }
234
235    pub fn as_str(&self) -> &str {
236        &self.0
237    }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct I2pPeersPlan(Vec<I2pPeerPlan>);
242
243impl I2pPeersPlan {
244    fn new(peers: Vec<String>) -> Result<Self, PlanErrorKind> {
245        validate_peers(peers.iter().map(String::as_str)).map_err(|_| {
246            PlanErrorKind::InvalidSetting {
247                key: interface_key::PEERS,
248            }
249        })?;
250        peers
251            .into_iter()
252            .map(I2pPeerPlan::new)
253            .collect::<Result<Vec<_>, _>>()
254            .map(Self)
255    }
256
257    pub fn iter(&self) -> impl Iterator<Item = &I2pPeerPlan> {
258        self.0.iter()
259    }
260
261    pub fn is_empty(&self) -> bool {
262        self.0.is_empty()
263    }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum I2pReachabilityPlan {
268    OutboundOnly,
269    Connectable,
270}
271
272impl I2pReachabilityPlan {
273    pub const fn is_connectable(self) -> bool {
274        matches!(self, Self::Connectable)
275    }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct AutoInterfaceGroupId(String);
280
281impl AutoInterfaceGroupId {
282    pub fn as_str(&self) -> &str {
283        &self.0
284    }
285
286    pub fn as_bytes(&self) -> &[u8] {
287        self.0.as_bytes()
288    }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub struct AutoInterfaceDiscoveryPort(u16);
293
294impl AutoInterfaceDiscoveryPort {
295    fn new(port: u16) -> Option<Self> {
296        (port != 0 && port < u16::MAX).then_some(Self(port))
297    }
298
299    pub const fn get(self) -> u16 {
300        self.0
301    }
302
303    pub const fn reverse_discovery_port(self) -> u16 {
304        self.0 + 1
305    }
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct AutoInterfaceDataPort(u16);
310
311impl AutoInterfaceDataPort {
312    fn new(port: u16) -> Option<Self> {
313        (port != 0).then_some(Self(port))
314    }
315
316    pub const fn get(self) -> u16 {
317        self.0
318    }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct AutoInterfaceDevicePolicy {
323    allowed: Vec<String>,
324    ignored: Vec<String>,
325}
326
327impl AutoInterfaceDevicePolicy {
328    pub fn allowed(&self) -> &[String] {
329        &self.allowed
330    }
331
332    pub fn ignored(&self) -> &[String] {
333        &self.ignored
334    }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct AutoInterfacePlan {
339    group_id: AutoInterfaceGroupId,
340    discovery_scope: AutoInterfaceDiscoveryScope,
341    discovery_port: AutoInterfaceDiscoveryPort,
342    data_port: AutoInterfaceDataPort,
343    devices: AutoInterfaceDevicePolicy,
344    multicast_address_type: AutoInterfaceMulticastAddressType,
345}
346
347impl AutoInterfacePlan {
348    pub fn group_id(&self) -> &AutoInterfaceGroupId {
349        &self.group_id
350    }
351
352    pub const fn discovery_scope(&self) -> AutoInterfaceDiscoveryScope {
353        self.discovery_scope
354    }
355
356    pub const fn discovery_port(&self) -> AutoInterfaceDiscoveryPort {
357        self.discovery_port
358    }
359
360    pub const fn data_port(&self) -> AutoInterfaceDataPort {
361        self.data_port
362    }
363
364    pub const fn devices(&self) -> &AutoInterfaceDevicePolicy {
365        &self.devices
366    }
367
368    pub const fn multicast_address_type(&self) -> AutoInterfaceMulticastAddressType {
369        self.multicast_address_type
370    }
371}
372
373/// The wire a planned interface runs on. Only mediums a host can stand up appear here.
374#[derive(Debug, Clone, PartialEq, Eq)]
375pub enum PlannedMedium {
376    /// RNS `AutoInterface`: multicast LAN discovery plus unicast peers (our `AutoWifi`).
377    AutoWifi(AutoInterfacePlan),
378    /// RNS `TCPClientInterface`: dial one peer.
379    TcpClient {
380        connection: TcpDialPlan,
381        framing: TcpWireFraming,
382    },
383    /// RNS `TCPServerInterface`: accept peers on the configured listener.
384    TcpServer {
385        listener: TcpListenPlan,
386        framing: TcpWireFraming,
387    },
388    /// RNS `UDPInterface`: receive, send, or do both over configured datagram endpoints.
389    Udp {
390        flow: UdpFlowPlan,
391    },
392    /// RNS `SerialInterface`: a configured serial device.
393    Serial {
394        device: String,
395        line: SerialLinePlan,
396    },
397    /// RNS `KISSInterface`: a KISS TNC on a configured serial line, with the CSMA/timing config
398    /// written to the TNC at startup (the millisecond values as the operator gave them).
399    Kiss {
400        device: String,
401        line: SerialLinePlan,
402        preamble_ms: u32,
403        txtail_ms: u32,
404        persistence: u8,
405        slottime_ms: u32,
406        flow_control: ReadyCommandFlowControl,
407        station_id: Option<StationIdentificationPlan>,
408    },
409    /// RNS `AX25KISSInterface`: a KISS TNC carrying AX.25 UI frames, sourced from `callsign`/`ssid`.
410    /// The callsign/SSID are validated before the daemon plan is constructed.
411    Ax25Kiss {
412        device: String,
413        line: SerialLinePlan,
414        preamble_ms: u32,
415        txtail_ms: u32,
416        persistence: u8,
417        slottime_ms: u32,
418        flow_control: ReadyCommandFlowControl,
419        callsign: String,
420        ssid: u8,
421    },
422    /// RNS `PipeInterface`: a subprocess `command` whose stdout/stdin carries HDLC-framed packets,
423    /// respawned after the configured delay when it exits.
424    Pipe {
425        command: PipeCommandPlan,
426        respawn_delay: PipeRespawnDelay,
427    },
428    /// RNS `RNodeInterface`: a LoRa RNode driven over a host byte stream, configured to a radio
429    /// channel at bring-up. The radio parameters are required; the airtime locks are the wire-scaled
430    /// `int(percent * 100)` values, absent when unconfigured.
431    Rnode {
432        transport: RNodeTransportPlan,
433        frequency_hz: u64,
434        bandwidth_hz: u32,
435        tx_power_dbm: i16,
436        spreading_factor: u8,
437        coding_rate: u8,
438        flow_control: ReadyCommandFlowControl,
439        station_id: Option<StationIdentificationPlan>,
440        airtime_limit_short: Option<AirtimeLimitCentiPercent>,
441        airtime_limit_long: Option<AirtimeLimitCentiPercent>,
442    },
443    RnodeMulti {
444        member: RNodeMultiMemberPlan,
445    },
446    /// RNS `BackboneInterface`: the listening end of a TCP backbone link.
447    Backbone {
448        listener: TcpListenPlan,
449    },
450    /// RNS `BackboneClientInterface`: dial one backbone peer. Wire-identical to
451    /// [`TcpClient`](Self::TcpClient).
452    BackboneClient {
453        connection: TcpDialPlan,
454    },
455    I2p {
456        peers: I2pPeersPlan,
457        reachability: I2pReachabilityPlan,
458    },
459    Weave {
460        device: String,
461    },
462    PrnsUsbAuto,
463    PrnsBluetoothAuto,
464    PrnsWebSocketClient {
465        target: WebSocketTargetPlan,
466    },
467    PrnsWebSocketServer {
468        listener: TcpListenPlan,
469    },
470}
471
472pub(super) fn rnode_defaults(
473    spreading_factor: u8,
474    coding_rate: u8,
475    bandwidth_hz: u32,
476) -> Result<InterfaceDefaults, PlanErrorKind> {
477    let raw = rnode_policy::nominal_bitrate_bps(spreading_factor, coding_rate, bandwidth_hz);
478    let bitrate = BitrateBps::new(u64::from(raw)).ok_or(PlanErrorKind::InvalidSetting {
479        key: interface_key::BANDWIDTH,
480    })?;
481    Ok(rnode_policy::defaults_for_bitrate(bitrate))
482}
483
484pub(super) fn plan_medium(interface: &ReferenceInterface) -> Result<PlannedMedium, PlanErrorKind> {
485    match &interface.params {
486        ReferenceConfigParams::Auto {
487            group_id,
488            discovery_scope,
489            discovery_port,
490            data_port,
491            devices,
492            ignored_devices,
493            multicast_address_type,
494        } => Ok(PlannedMedium::AutoWifi(auto_interface_plan(
495            group_id,
496            discovery_scope,
497            *discovery_port,
498            *data_port,
499            devices,
500            ignored_devices,
501            multicast_address_type,
502        )?)),
503        ReferenceConfigParams::TcpClient {
504            target_host,
505            target_port,
506            kiss_framing,
507            i2p_tunneled,
508            connect_timeout,
509            max_reconnect_tries,
510            fixed_mtu: _,
511        } => {
512            let host = target_host
513                .clone()
514                .ok_or(PlanErrorKind::MissingRequiredField {
515                    key: interface_key::TARGET_HOST,
516                })?;
517            let port = target_port.ok_or(PlanErrorKind::MissingRequiredField {
518                key: interface_key::TARGET_PORT,
519            })?;
520            Ok(PlannedMedium::TcpClient {
521                connection: tcp_dial_plan(
522                    host,
523                    port,
524                    *connect_timeout,
525                    *max_reconnect_tries,
526                    AddressFamilyPreference::System,
527                    *i2p_tunneled,
528                ),
529                framing: if *kiss_framing == Some(true) {
530                    TcpWireFraming::Kiss
531                } else {
532                    TcpWireFraming::Hdlc
533                },
534            })
535        }
536        ReferenceConfigParams::TcpServer {
537            listen_ip,
538            listen_port,
539            device,
540            port,
541            prefer_ipv6,
542            i2p_tunneled,
543            kiss_framing,
544            fixed_mtu: _,
545        } => {
546            let listen_port = port
547                .or(*listen_port)
548                .ok_or(PlanErrorKind::MissingRequiredField {
549                    key: interface_key::LISTEN_PORT,
550                })?;
551            Ok(PlannedMedium::TcpServer {
552                listener: TcpListenPlan {
553                    host: tcp_listen_host(listen_ip, device),
554                    port: listen_port,
555                    address_family: preferred_ip_family(*prefer_ipv6),
556                    tunnel: tunnel_mode(*i2p_tunneled),
557                },
558                framing: if *kiss_framing == Some(true) {
559                    TcpWireFraming::Kiss
560                } else {
561                    TcpWireFraming::Hdlc
562                },
563            })
564        }
565        ReferenceConfigParams::Udp {
566            listen_ip,
567            listen_port,
568            forward_ip,
569            forward_port,
570            device,
571            port,
572        } => {
573            let listen = udp_endpoint(
574                listen_ip.as_deref(),
575                port.or(*listen_port),
576                device.as_deref(),
577                interface_key::LISTEN_PORT,
578            )?;
579            let forward = udp_endpoint(
580                forward_ip.as_deref(),
581                port.or(*forward_port),
582                device.as_deref(),
583                interface_key::FORWARD_PORT,
584            )?;
585            let flow = match (listen, forward) {
586                (Some(listen), Some(forward)) => UdpFlowPlan::Bidirectional { listen, forward },
587                (Some(listen), None) => UdpFlowPlan::ReceiveOnly { listen },
588                (None, Some(forward)) => UdpFlowPlan::SendOnly { forward },
589                (None, None) => {
590                    return Err(PlanErrorKind::MissingRequiredField {
591                        key: interface_key::LISTEN_IP,
592                    })
593                }
594            };
595            Ok(PlannedMedium::Udp { flow })
596        }
597        ReferenceConfigParams::Serial {
598            port,
599            speed,
600            databits,
601            parity,
602            stopbits,
603        } => {
604            let device = port.clone().ok_or(PlanErrorKind::MissingRequiredField {
605                key: interface_key::PORT,
606            })?;
607            Ok(PlannedMedium::Serial {
608                device,
609                line: serial_line(*speed, *databits, parity.as_deref(), *stopbits)?,
610            })
611        }
612        ReferenceConfigParams::Kiss {
613            port,
614            speed,
615            databits,
616            parity,
617            stopbits,
618            flow_control,
619            preamble,
620            txtail,
621            persistence,
622            slottime,
623            id_callsign,
624            id_interval,
625        } => {
626            let device = port.clone().ok_or(PlanErrorKind::MissingRequiredField {
627                key: interface_key::PORT,
628            })?;
629            Ok(PlannedMedium::Kiss {
630                device,
631                line: serial_line(*speed, *databits, parity.as_deref(), *stopbits)?,
632                preamble_ms: preamble.unwrap_or(RNS_KISS_DEFAULT_PREAMBLE_MS),
633                txtail_ms: txtail.unwrap_or(RNS_KISS_DEFAULT_TXTAIL_MS),
634                persistence: persistence
635                    .map(|p| p.min(u8::MAX as u32) as u8)
636                    .unwrap_or(RNS_KISS_DEFAULT_PERSISTENCE),
637                slottime_ms: slottime.unwrap_or(RNS_KISS_DEFAULT_SLOTTIME_MS),
638                flow_control: ready_command_flow_control(*flow_control),
639                station_id: station_identification(id_callsign.as_deref(), *id_interval, None)?,
640            })
641        }
642        ReferenceConfigParams::Ax25Kiss {
643            port,
644            speed,
645            databits,
646            parity,
647            stopbits,
648            flow_control,
649            preamble,
650            txtail,
651            persistence,
652            slottime,
653            callsign,
654            ssid,
655        } => {
656            let device = port.clone().ok_or(PlanErrorKind::MissingRequiredField {
657                key: interface_key::PORT,
658            })?;
659            let callsign = callsign
660                .clone()
661                .ok_or(PlanErrorKind::MissingRequiredField {
662                    key: interface_key::CALLSIGN,
663                })?;
664            let ssid = ssid.ok_or(PlanErrorKind::MissingRequiredField {
665                key: interface_key::SSID,
666            })?;
667            Ok(PlannedMedium::Ax25Kiss {
668                device,
669                line: serial_line(*speed, *databits, parity.as_deref(), *stopbits)?,
670                preamble_ms: preamble.unwrap_or(RNS_KISS_DEFAULT_PREAMBLE_MS),
671                txtail_ms: txtail.unwrap_or(RNS_KISS_DEFAULT_TXTAIL_MS),
672                persistence: persistence
673                    .map(|p| p.min(u8::MAX as u32) as u8)
674                    .unwrap_or(RNS_KISS_DEFAULT_PERSISTENCE),
675                slottime_ms: slottime.unwrap_or(RNS_KISS_DEFAULT_SLOTTIME_MS),
676                flow_control: ready_command_flow_control(*flow_control),
677                callsign,
678                ssid,
679            })
680        }
681        ReferenceConfigParams::Rnode {
682            port,
683            radio,
684            flow_control,
685            id_callsign,
686            id_interval,
687            airtime_limit_short,
688            airtime_limit_long,
689        } => {
690            let configured_port = port.clone().ok_or(PlanErrorKind::MissingRequiredField {
691                key: interface_key::PORT,
692            })?;
693            let transport = RNodeTransportPlan::from_configured_port(configured_port)?;
694            let frequency_hz = radio.frequency.ok_or(PlanErrorKind::MissingRequiredField {
695                key: interface_key::FREQUENCY,
696            })?;
697            let bandwidth_hz = radio.bandwidth.ok_or(PlanErrorKind::MissingRequiredField {
698                key: interface_key::BANDWIDTH,
699            })?;
700            let spreading_factor =
701                radio
702                    .spreadingfactor
703                    .ok_or(PlanErrorKind::MissingRequiredField {
704                        key: interface_key::SPREADINGFACTOR,
705                    })?;
706            let coding_rate = radio
707                .codingrate
708                .ok_or(PlanErrorKind::MissingRequiredField {
709                    key: interface_key::CODINGRATE,
710                })?;
711            let tx_power_dbm = radio.txpower.ok_or(PlanErrorKind::MissingRequiredField {
712                key: interface_key::TXPOWER,
713            })?;
714            Ok(PlannedMedium::Rnode {
715                transport,
716                frequency_hz,
717                bandwidth_hz,
718                tx_power_dbm,
719                spreading_factor,
720                coding_rate,
721                flow_control: ready_command_flow_control(*flow_control),
722                station_id: station_identification(id_callsign.as_deref(), *id_interval, Some(32))?,
723                airtime_limit_short: airtime_limit(
724                    *airtime_limit_short,
725                    interface_key::AIRTIME_LIMIT_SHORT,
726                )?,
727                airtime_limit_long: airtime_limit(
728                    *airtime_limit_long,
729                    interface_key::AIRTIME_LIMIT_LONG,
730                )?,
731            })
732        }
733        ReferenceConfigParams::Pipe {
734            command,
735            respawn_delay,
736        } => {
737            let command = command
738                .as_deref()
739                .ok_or(PlanErrorKind::MissingRequiredField {
740                    key: interface_key::COMMAND,
741                })?;
742            Ok(PlannedMedium::Pipe {
743                command: pipe_command(command)?,
744                respawn_delay: pipe_respawn_delay(*respawn_delay)?,
745            })
746        }
747        ReferenceConfigParams::Backbone {
748            listen_ip,
749            listen_port,
750            target_host,
751            target_port,
752            port,
753            device,
754            prefer_ipv6,
755            i2p_tunneled,
756            connect_timeout,
757            max_reconnect_tries,
758        } => {
759            if target_host.is_some() || interface.type_name == "BackboneClientInterface" {
760                let host = target_host
761                    .clone()
762                    .ok_or(PlanErrorKind::MissingRequiredField {
763                        key: interface_key::TARGET_HOST,
764                    })?;
765                let port = port
766                    .or(*target_port)
767                    .ok_or(PlanErrorKind::MissingRequiredField {
768                        key: interface_key::TARGET_PORT,
769                    })?;
770                Ok(PlannedMedium::BackboneClient {
771                    connection: tcp_dial_plan(
772                        host,
773                        port,
774                        *connect_timeout,
775                        *max_reconnect_tries,
776                        preferred_ip_family(*prefer_ipv6),
777                        *i2p_tunneled,
778                    ),
779                })
780            } else {
781                let bind_port =
782                    (*port)
783                        .or(*listen_port)
784                        .ok_or(PlanErrorKind::MissingRequiredField {
785                            key: interface_key::LISTEN_PORT,
786                        })?;
787                Ok(PlannedMedium::Backbone {
788                    listener: TcpListenPlan {
789                        host: tcp_listen_host(listen_ip, device),
790                        port: bind_port,
791                        address_family: preferred_ip_family(*prefer_ipv6),
792                        tunnel: TcpTunnelMode::Direct,
793                    },
794                })
795            }
796        }
797        ReferenceConfigParams::I2p { peers, connectable } => Ok(PlannedMedium::I2p {
798            peers: I2pPeersPlan::new(peers.clone().unwrap_or_default())?,
799            reachability: if *connectable == Some(true) {
800                I2pReachabilityPlan::Connectable
801            } else {
802                I2pReachabilityPlan::OutboundOnly
803            },
804        }),
805        ReferenceConfigParams::Weave { port } => Ok(PlannedMedium::Weave {
806            device: port.clone().ok_or(PlanErrorKind::MissingRequiredField {
807                key: interface_key::PORT,
808            })?,
809        }),
810        ReferenceConfigParams::PrnsUsbAuto => Ok(PlannedMedium::PrnsUsbAuto),
811        ReferenceConfigParams::PrnsBluetoothAuto => Ok(PlannedMedium::PrnsBluetoothAuto),
812        ReferenceConfigParams::PrnsWebSocketClient { target } => {
813            let target = target.clone().ok_or(PlanErrorKind::MissingRequiredField {
814                key: interface_key::TARGET,
815            })?;
816            Ok(PlannedMedium::PrnsWebSocketClient {
817                target: WebSocketTargetPlan::from_configured(target)?,
818            })
819        }
820        ReferenceConfigParams::PrnsWebSocketServer {
821            listen_ip,
822            listen_port,
823            device,
824            port,
825            prefer_ipv6,
826        } => {
827            let port = port
828                .or(*listen_port)
829                .ok_or(PlanErrorKind::MissingRequiredField {
830                    key: interface_key::LISTEN_PORT,
831                })?;
832            Ok(PlannedMedium::PrnsWebSocketServer {
833                listener: TcpListenPlan {
834                    host: tcp_listen_host(listen_ip, device),
835                    port,
836                    address_family: preferred_ip_family(*prefer_ipv6),
837                    tunnel: TcpTunnelMode::Direct,
838                },
839            })
840        }
841        _ => Err(PlanErrorKind::UnsupportedKind),
842    }
843}
844
845#[allow(clippy::too_many_arguments)]
846fn auto_interface_plan(
847    group_id: &Option<String>,
848    discovery_scope: &Option<String>,
849    discovery_port: Option<u16>,
850    data_port: Option<u16>,
851    devices: &Option<Vec<String>>,
852    ignored_devices: &Option<Vec<String>>,
853    multicast_address_type: &Option<String>,
854) -> Result<AutoInterfacePlan, PlanErrorKind> {
855    let discovery_scope =
856        discovery_scope
857            .as_deref()
858            .map_or(Ok(AutoInterfaceDiscoveryScope::Link), |value| {
859                AutoInterfaceDiscoveryScope::from_name(value.trim()).ok_or(
860                    PlanErrorKind::InvalidSetting {
861                        key: interface_key::DISCOVERY_SCOPE,
862                    },
863                )
864            })?;
865    let multicast_address_type = multicast_address_type.as_deref().map_or(
866        Ok(AutoInterfaceMulticastAddressType::Temporary),
867        |value| {
868            AutoInterfaceMulticastAddressType::from_name(value.trim()).ok_or(
869                PlanErrorKind::InvalidSetting {
870                    key: interface_key::MULTICAST_ADDRESS_TYPE,
871                },
872            )
873        },
874    )?;
875    let discovery_port = AutoInterfaceDiscoveryPort::new(
876        discovery_port.unwrap_or(DEFAULT_DISCOVERY_PORT),
877    )
878    .ok_or(PlanErrorKind::InvalidSetting {
879        key: interface_key::DISCOVERY_PORT,
880    })?;
881    let data_port = AutoInterfaceDataPort::new(data_port.unwrap_or(DEFAULT_DATA_PORT)).ok_or(
882        PlanErrorKind::InvalidSetting {
883            key: interface_key::DATA_PORT,
884        },
885    )?;
886    Ok(AutoInterfacePlan {
887        group_id: AutoInterfaceGroupId(group_id.clone().unwrap_or_else(|| GROUP_NAME.to_string())),
888        discovery_scope,
889        discovery_port,
890        data_port,
891        devices: AutoInterfaceDevicePolicy {
892            allowed: devices.clone().unwrap_or_default(),
893            ignored: ignored_devices.clone().unwrap_or_default(),
894        },
895        multicast_address_type,
896    })
897}
898
899fn tcp_dial_plan(
900    host: String,
901    port: u16,
902    connect_timeout_seconds: Option<u64>,
903    max_reconnect_tries: Option<u32>,
904    address_family: AddressFamilyPreference,
905    i2p_tunneled: Option<bool>,
906) -> TcpDialPlan {
907    TcpDialPlan {
908        host,
909        port,
910        connect_timeout: ConnectTimeoutSeconds::new(
911            connect_timeout_seconds.unwrap_or(RNS_TCP_CONNECT_TIMEOUT_SECONDS),
912        ),
913        reconnect_limit: max_reconnect_tries
914            .map(ReconnectLimit::Attempts)
915            .unwrap_or(ReconnectLimit::Unlimited),
916        address_family,
917        tunnel: tunnel_mode(i2p_tunneled),
918    }
919}
920
921fn tcp_listen_host(listen_ip: &Option<String>, device: &Option<String>) -> TcpListenHost {
922    match (device, listen_ip) {
923        (Some(device), _) => TcpListenHost::Device(device.clone()),
924        (None, Some(address)) => TcpListenHost::Address(address.clone()),
925        (None, None) => TcpListenHost::Any,
926    }
927}
928
929const fn preferred_ip_family(prefer_ipv6: Option<bool>) -> AddressFamilyPreference {
930    match prefer_ipv6 {
931        Some(true) => AddressFamilyPreference::Ipv6,
932        Some(false) | None => AddressFamilyPreference::Ipv4,
933    }
934}
935
936const fn tunnel_mode(i2p_tunneled: Option<bool>) -> TcpTunnelMode {
937    match i2p_tunneled {
938        Some(true) => TcpTunnelMode::I2p,
939        Some(false) | None => TcpTunnelMode::Direct,
940    }
941}
942
943fn udp_endpoint(
944    address: Option<&str>,
945    port: Option<u16>,
946    device: Option<&str>,
947    port_key: &'static str,
948) -> Result<Option<UdpEndpointPlan>, PlanErrorKind> {
949    if address.is_none() && port.is_none() {
950        return Ok(None);
951    }
952    let host = match (address, device) {
953        (Some(address), _) => Some(UdpEndpointHost::Address(address.to_string())),
954        (None, Some(device)) => Some(UdpEndpointHost::DeviceBroadcast(device.to_string())),
955        (None, None) => None,
956    };
957    match (host, port) {
958        (Some(host), Some(port)) => Ok(Some(UdpEndpointPlan { host, port })),
959        (Some(_), None) => Err(PlanErrorKind::MissingRequiredField { key: port_key }),
960        (None, _) => Ok(None),
961    }
962}
963
964pub(in crate::plan) const RNS_DEFAULT_SERIAL_BAUD: u32 = 9_600;
965const RNS_TCP_CONNECT_TIMEOUT_SECONDS: u64 = 5;
966
967/// RNS `KISSInterface` TNC defaults, mirrored from `interfaces::kiss` (kept in this crate so
968/// the config planner stays independent of the interface crate): 350 ms preamble, 20 ms TX-tail,
969/// persistence 64, 20 ms slot time.
970const RNS_KISS_DEFAULT_PREAMBLE_MS: u32 = 350;
971const RNS_KISS_DEFAULT_TXTAIL_MS: u32 = 20;
972const RNS_KISS_DEFAULT_PERSISTENCE: u8 = 64;
973const RNS_KISS_DEFAULT_SLOTTIME_MS: u32 = 20;
974
975const RNS_PIPE_DEFAULT_RESPAWN_SECONDS: u64 = 5;
976
977fn serial_line(
978    speed: Option<u32>,
979    data_bits: Option<u8>,
980    parity: Option<&str>,
981    stop_bits: Option<u8>,
982) -> Result<SerialLinePlan, PlanErrorKind> {
983    let baud = speed.unwrap_or(RNS_DEFAULT_SERIAL_BAUD);
984    if u64::from(baud) < BitrateBps::MINIMUM {
985        return Err(PlanErrorKind::InvalidSetting {
986            key: interface_key::SPEED,
987        });
988    }
989    let data_bits = match data_bits.unwrap_or(8) {
990        5 => SerialDataBits::Five,
991        6 => SerialDataBits::Six,
992        7 => SerialDataBits::Seven,
993        8 => SerialDataBits::Eight,
994        _ => {
995            return Err(PlanErrorKind::InvalidSetting {
996                key: interface_key::DATABITS,
997            })
998        }
999    };
1000    let parity = match parity.unwrap_or("n").trim().to_ascii_lowercase().as_str() {
1001        "n" | "none" => SerialParity::None,
1002        "e" | "even" => SerialParity::Even,
1003        "o" | "odd" => SerialParity::Odd,
1004        _ => {
1005            return Err(PlanErrorKind::InvalidSetting {
1006                key: interface_key::PARITY,
1007            })
1008        }
1009    };
1010    let stop_bits = match stop_bits.unwrap_or(1) {
1011        1 => SerialStopBits::One,
1012        2 => SerialStopBits::Two,
1013        _ => {
1014            return Err(PlanErrorKind::InvalidSetting {
1015                key: interface_key::STOPBITS,
1016            })
1017        }
1018    };
1019    Ok(SerialLinePlan {
1020        baud,
1021        data_bits,
1022        parity,
1023        stop_bits,
1024    })
1025}
1026
1027pub(in crate::plan) fn ready_command_flow_control(
1028    configured: Option<bool>,
1029) -> ReadyCommandFlowControl {
1030    match configured {
1031        Some(true) => ReadyCommandFlowControl::Enabled,
1032        Some(false) | None => ReadyCommandFlowControl::Disabled,
1033    }
1034}
1035
1036pub(in crate::plan) fn station_identification(
1037    callsign: Option<&str>,
1038    interval_seconds: Option<u64>,
1039    maximum_callsign_bytes: Option<usize>,
1040) -> Result<Option<StationIdentificationPlan>, PlanErrorKind> {
1041    let (callsign, interval_seconds) = match (callsign, interval_seconds) {
1042        (None, None) => return Ok(None),
1043        (Some(_), None) => {
1044            return Err(PlanErrorKind::MissingRequiredField {
1045                key: interface_key::ID_INTERVAL,
1046            })
1047        }
1048        (None, Some(_)) => {
1049            return Err(PlanErrorKind::MissingRequiredField {
1050                key: interface_key::ID_CALLSIGN,
1051            })
1052        }
1053        (Some(callsign), Some(interval_seconds)) => (callsign, interval_seconds),
1054    };
1055    if callsign.is_empty() || maximum_callsign_bytes.is_some_and(|maximum| callsign.len() > maximum)
1056    {
1057        return Err(PlanErrorKind::InvalidSetting {
1058            key: interface_key::ID_CALLSIGN,
1059        });
1060    }
1061    Ok(Some(StationIdentificationPlan {
1062        callsign: callsign.to_string(),
1063        interval_seconds,
1064    }))
1065}
1066
1067pub(in crate::plan) fn airtime_limit(
1068    percent: Option<f64>,
1069    key: &'static str,
1070) -> Result<Option<AirtimeLimitCentiPercent>, PlanErrorKind> {
1071    let Some(percent) = percent else {
1072        return Ok(None);
1073    };
1074    if !percent.is_finite() || !(0.0..=100.0).contains(&percent) {
1075        return Err(PlanErrorKind::InvalidSetting { key });
1076    }
1077    Ok(Some(AirtimeLimitCentiPercent((percent * 100.0) as u16)))
1078}
1079
1080fn pipe_respawn_delay(seconds: Option<f64>) -> Result<PipeRespawnDelay, PlanErrorKind> {
1081    let duration = match seconds {
1082        Some(seconds) => {
1083            Duration::try_from_secs_f64(seconds).map_err(|_| PlanErrorKind::InvalidSetting {
1084                key: interface_key::RESPAWN_DELAY,
1085            })?
1086        }
1087        None => Duration::from_secs(RNS_PIPE_DEFAULT_RESPAWN_SECONDS),
1088    };
1089    Ok(PipeRespawnDelay(duration))
1090}
1091
1092fn pipe_command(source: &str) -> Result<PipeCommandPlan, PlanErrorKind> {
1093    let argv = shlex::split(source).filter(|argv| !argv.is_empty()).ok_or(
1094        PlanErrorKind::InvalidSetting {
1095            key: interface_key::COMMAND,
1096        },
1097    )?;
1098    Ok(PipeCommandPlan {
1099        source: source.to_string(),
1100        argv,
1101    })
1102}