Skip to main content

prns_config/plan/
rnode_multi.rs

1use prns_core::interfaces::rnode::multi::{RadioConfig, RadioConfigError, RadioConfigInput, VPort};
2use prns_core::interfaces::{AnnounceRateLimit, InterfaceCommonPolicy, InterfaceGravity};
3
4use crate::reference::keys::interface as interface_key;
5use crate::reference::{RNodeSubinterface, ReferenceConfigParams, ReferenceInterface};
6
7use super::interface::{
8    airtime_limit, effective_policy, plan_access, plan_interface_discovery,
9    ready_command_flow_control, station_identification, ConfiguredInterfaceLifecycle,
10    MemberEgressPolicy, PlanErrorKind, PlannedInterface, PlannedMedium, ReadyCommandFlowControl,
11    StationIdentificationPlan,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct RNodeMultiDevicePlan {
16    name: String,
17    device: String,
18    station_id: Option<StationIdentificationPlan>,
19}
20
21impl RNodeMultiDevicePlan {
22    pub fn name(&self) -> &str {
23        &self.name
24    }
25
26    pub fn device(&self) -> &str {
27        &self.device
28    }
29
30    pub fn station_id(&self) -> Option<&StationIdentificationPlan> {
31        self.station_id.as_ref()
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct RNodeMultiMemberPlan {
37    parent: RNodeMultiDevicePlan,
38    vport: VPort,
39    radio: RadioConfig,
40    flow_control: ReadyCommandFlowControl,
41}
42
43impl RNodeMultiMemberPlan {
44    pub fn parent(&self) -> &RNodeMultiDevicePlan {
45        &self.parent
46    }
47
48    pub const fn vport(&self) -> VPort {
49        self.vport
50    }
51
52    pub const fn radio(&self) -> RadioConfig {
53        self.radio
54    }
55
56    pub const fn flow_control(&self) -> ReadyCommandFlowControl {
57        self.flow_control
58    }
59}
60
61pub(super) struct PlanFailure {
62    pub(super) subinterface_name: Option<String>,
63    pub(super) kind: PlanErrorKind,
64}
65
66impl PlanFailure {
67    fn parent(kind: PlanErrorKind) -> Self {
68        Self {
69            subinterface_name: None,
70            kind,
71        }
72    }
73
74    fn member(name: &str, kind: PlanErrorKind) -> Self {
75        Self {
76            subinterface_name: Some(name.to_string()),
77            kind,
78        }
79    }
80}
81
82pub(super) fn plan(
83    interface: &ReferenceInterface,
84    global_common: InterfaceCommonPolicy,
85    global_announce_rate: Option<AnnounceRateLimit>,
86    default_gravity: InterfaceGravity,
87    transport_enabled: bool,
88) -> Result<Vec<PlannedInterface>, PlanFailure> {
89    let ReferenceConfigParams::RnodeMulti {
90        port,
91        id_callsign,
92        id_interval,
93        subinterfaces,
94    } = &interface.params
95    else {
96        return Err(PlanFailure::parent(PlanErrorKind::UnsupportedKind));
97    };
98    let device = port.clone().ok_or_else(|| {
99        PlanFailure::parent(PlanErrorKind::MissingRequiredField {
100            key: interface_key::PORT,
101        })
102    })?;
103    let station_id = station_identification(id_callsign.as_deref(), *id_interval, Some(32))
104        .map_err(PlanFailure::parent)?;
105    let parent = RNodeMultiDevicePlan {
106        name: interface.name.clone(),
107        device,
108        station_id,
109    };
110    subinterfaces
111        .iter()
112        .map(|subinterface| {
113            plan_member(
114                interface,
115                subinterface,
116                parent.clone(),
117                global_common,
118                global_announce_rate,
119                default_gravity,
120                transport_enabled,
121            )
122            .map_err(|kind| PlanFailure::member(&subinterface.name, kind))
123        })
124        .collect()
125}
126
127fn plan_member(
128    interface: &ReferenceInterface,
129    subinterface: &RNodeSubinterface,
130    parent: RNodeMultiDevicePlan,
131    global_common: InterfaceCommonPolicy,
132    global_announce_rate: Option<AnnounceRateLimit>,
133    default_gravity: InterfaceGravity,
134    transport_enabled: bool,
135) -> Result<PlannedInterface, PlanErrorKind> {
136    let member = RNodeMultiMemberPlan {
137        parent,
138        vport: VPort::new(required(subinterface.vport, interface_key::VPORT)?).ok_or(
139            PlanErrorKind::InvalidSetting {
140                key: interface_key::VPORT,
141            },
142        )?,
143        radio: radio_config(subinterface)?,
144        flow_control: ready_command_flow_control(subinterface.flow_control),
145    };
146    let medium = PlannedMedium::RnodeMulti { member };
147    let discovery = plan_interface_discovery(interface, &medium);
148    let policy = effective_policy(
149        interface,
150        &medium,
151        &discovery,
152        super::interface::InheritedInterfacePolicy {
153            common: global_common,
154            announce_rate: global_announce_rate,
155            gravity: default_gravity,
156        },
157        transport_enabled,
158        MemberEgressPolicy::from_outgoing(subinterface.outgoing),
159    )?;
160    Ok(PlannedInterface {
161        name: format!("{}[{}]", interface.name, subinterface.name),
162        policy,
163        access: plan_access(interface, &medium)?,
164        medium,
165        discovery,
166        lifecycle: if interface.bootstrap_only == Some(true) {
167            ConfiguredInterfaceLifecycle::BootstrapOnly
168        } else {
169            ConfiguredInterfaceLifecycle::Persistent
170        },
171    })
172}
173
174fn radio_config(subinterface: &RNodeSubinterface) -> Result<RadioConfig, PlanErrorKind> {
175    let short = airtime_limit(
176        subinterface.airtime_limit_short,
177        interface_key::AIRTIME_LIMIT_SHORT,
178    )?;
179    let long = airtime_limit(
180        subinterface.airtime_limit_long,
181        interface_key::AIRTIME_LIMIT_LONG,
182    )?;
183    RadioConfig::new(RadioConfigInput {
184        frequency_hz: required(subinterface.radio.frequency, interface_key::FREQUENCY)?,
185        bandwidth_hz: required(subinterface.radio.bandwidth, interface_key::BANDWIDTH)?,
186        tx_power_dbm: required(subinterface.radio.txpower, interface_key::TXPOWER)?,
187        spreading_factor: required(
188            subinterface.radio.spreadingfactor,
189            interface_key::SPREADINGFACTOR,
190        )?,
191        coding_rate: required(subinterface.radio.codingrate, interface_key::CODINGRATE)?,
192        airtime_limit_short_centi_percent: short.map(|limit| limit.get()),
193        airtime_limit_long_centi_percent: long.map(|limit| limit.get()),
194    })
195    .map_err(radio_config_error)
196}
197
198fn required<T>(value: Option<T>, key: &'static str) -> Result<T, PlanErrorKind> {
199    value.ok_or(PlanErrorKind::MissingRequiredField { key })
200}
201
202fn radio_config_error(error: RadioConfigError) -> PlanErrorKind {
203    let key = match error {
204        RadioConfigError::Frequency(_) => interface_key::FREQUENCY,
205        RadioConfigError::Bandwidth(_) => interface_key::BANDWIDTH,
206        RadioConfigError::TxPower(_) => interface_key::TXPOWER,
207        RadioConfigError::SpreadingFactor(_) => interface_key::SPREADINGFACTOR,
208        RadioConfigError::CodingRate(_) => interface_key::CODINGRATE,
209        RadioConfigError::ShortAirtimeLimit(_) => interface_key::AIRTIME_LIMIT_SHORT,
210        RadioConfigError::LongAirtimeLimit(_) => interface_key::AIRTIME_LIMIT_LONG,
211    };
212    PlanErrorKind::InvalidSetting { key }
213}
214
215#[cfg(test)]
216mod tests {
217    use prns_core::interfaces::IfacSize;
218    use prns_core::interfaces::{
219        AnnounceBandwidthCap, AnnounceRateLimit, EgressCapability, InterfaceMode,
220        TransportCapability,
221    };
222
223    use crate::plan::{
224        DaemonPlan, DiscoveryAdvertisementPlan, InterfaceAccessPlan, InterfaceDiscoveryPlan,
225        PlannedInterface, PlannedMedium, ReadyCommandFlowControl, StationIdentificationPlan,
226    };
227
228    use super::RNodeMultiMemberPlan;
229
230    fn plan_of(config: &str) -> DaemonPlan {
231        crate::parse_and_plan(config).expect("config plans").value
232    }
233
234    fn named<'a>(plan: &'a DaemonPlan, name: &str) -> &'a PlannedInterface {
235        plan.interfaces
236            .iter()
237            .find(|interface| interface.name == name)
238            .unwrap_or_else(|| panic!("interface '{name}' was planned"))
239    }
240
241    fn member(interface: &PlannedInterface) -> &RNodeMultiMemberPlan {
242        let PlannedMedium::RnodeMulti { member } = &interface.medium else {
243            panic!("RNodeMulti member expected")
244        };
245        member
246    }
247
248    #[test]
249    fn members_inherit_one_typed_parent_policy_and_access_plan() {
250        let plan = plan_of(
251            "[interfaces]\n[[Dual]]\ntype = RNodeMultiInterface\nenabled = Yes\nport = /dev/ttyACM0\n\
252             interface_mode = internal\nannounce_cap = 3.5\nannounce_rate_target = 120\n\
253             network_name = field\npassphrase = secret\nifac_size = 64\nrecursive_prs = Yes\n\
254             announces_from_internal = No\nannounces_to_internal = Yes\ningress_control = No\negress_control = Yes\n\
255             id_callsign = N0CALL\nid_interval = 600\n\
256             [[[Low]]]\ninterface_enabled = Yes\nvport = 0\nfrequency = 868000000\n\
257             bandwidth = 125000\ntxpower = -4\nspreadingfactor = 8\ncodingrate = 5\n\
258             flow_control = Yes\noutgoing = No\nairtime_limit_short = 1.5\n\
259             [[[High]]]\ninterface_enabled = Yes\nvport = 1\nfrequency = 2400000000\n\
260             bandwidth = 812500\ntxpower = 10\nspreadingfactor = 7\ncodingrate = 6\n\
261             outgoing = Yes\n",
262        );
263        assert_eq!(plan.interfaces.len(), 2);
264        let low = named(&plan, "Dual[Low]");
265        let high = named(&plan, "Dual[High]");
266        let low_member = member(low);
267        let high_member = member(high);
268
269        assert_eq!(low_member.parent(), high_member.parent());
270        assert_eq!(low_member.parent().name(), "Dual");
271        assert_eq!(low_member.parent().device(), "/dev/ttyACM0");
272        assert_eq!(
273            low_member
274                .parent()
275                .station_id()
276                .map(StationIdentificationPlan::callsign),
277            Some("N0CALL")
278        );
279        assert_eq!(low_member.vport().get(), 0);
280        assert_eq!(high_member.vport().get(), 1);
281        assert_eq!(low_member.flow_control(), ReadyCommandFlowControl::Enabled);
282        assert_eq!(
283            high_member.flow_control(),
284            ReadyCommandFlowControl::Disabled
285        );
286        assert_eq!(
287            low_member.radio().airtime_limit_short_centi_percent(),
288            Some(150)
289        );
290
291        assert_eq!(low.access, high.access);
292        assert!(matches!(
293            low.access,
294            InterfaceAccessPlan::Ifac {
295                size: IfacSize::NARROW,
296                ..
297            }
298        ));
299        assert_eq!(low.policy.mode, InterfaceMode::Internal);
300        assert_eq!(high.policy.mode, InterfaceMode::Internal);
301        assert_eq!(low.policy.common, high.policy.common);
302        assert_eq!(
303            low.policy.common.forwarding.recursive_path_requests,
304            prns_core::interfaces::RecursivePathRequestPolicy::Enabled
305        );
306        assert!(!low.policy.common.forwarding.announces_from_internal);
307        assert!(low.policy.common.forwarding.announces_to_internal);
308        assert!(!low.policy.common.ingress_control.enabled);
309        assert!(low.policy.common.path_request_egress.enabled);
310        assert_eq!(low.policy.bitrate.get(), 3_125);
311        assert_eq!(high.policy.bitrate.get(), 29_622);
312        assert_eq!(low.policy.mtu.resolve(low.policy.bitrate), Some(508));
313        assert_eq!(high.policy.mtu.resolve(high.policy.bitrate), Some(508));
314        assert_eq!(low.policy.capabilities.egress, EgressCapability::Disabled);
315        assert_eq!(
316            high.policy.capabilities.egress,
317            EgressCapability::Enabled(TransportCapability::SameInterfaceRepeat)
318        );
319        assert_eq!(
320            low.policy.announce_bandwidth_cap,
321            AnnounceBandwidthCap::Limited { cap_per_mille: 35 }
322        );
323        assert_eq!(
324            low.policy.announce_rate_limit,
325            Some(AnnounceRateLimit {
326                target_ms: 120_000,
327                grace: 0,
328                penalty_ms: 0,
329            })
330        );
331        assert_eq!(
332            low.policy.announce_rate_limit,
333            high.policy.announce_rate_limit
334        );
335    }
336
337    #[test]
338    fn parent_egress_bitrate_and_discovery_apply_to_every_member() {
339        let plan = plan_of(
340            "[interfaces]\n[[Dual]]\ntype = RNodeMultiInterface\nenabled = Yes\nport = /dev/ttyACM0\n\
341             outgoing = No\nbitrate = 500000\ndiscoverable = Yes\n\
342             [[[Low]]]\ninterface_enabled = Yes\nvport = 0\nfrequency = 868000000\n\
343             bandwidth = 125000\ntxpower = 7\nspreadingfactor = 8\ncodingrate = 5\n\
344             outgoing = Yes\n\
345             [[[High]]]\ninterface_enabled = Yes\nvport = 1\nfrequency = 2400000000\n\
346             bandwidth = 812500\ntxpower = 10\nspreadingfactor = 7\ncodingrate = 6\n",
347        );
348        let low = named(&plan, "Dual[Low]");
349        let high = named(&plan, "Dual[High]");
350        for member in [low, high] {
351            assert_eq!(member.policy.bitrate.get(), 500_000);
352            assert_eq!(member.policy.mtu.resolve(member.policy.bitrate), Some(508));
353            assert_eq!(
354                member.policy.capabilities.egress,
355                EgressCapability::Disabled
356            );
357            assert_eq!(member.policy.mode, InterfaceMode::AccessPoint);
358        }
359        let InterfaceDiscoveryPlan::Announce(low_discovery) = &low.discovery else {
360            panic!("low radio discovery plan expected")
361        };
362        let InterfaceDiscoveryPlan::Announce(high_discovery) = &high.discovery else {
363            panic!("high radio discovery plan expected")
364        };
365        assert_eq!(
366            low_discovery.advertisement,
367            DiscoveryAdvertisementPlan::RNode {
368                frequency_hz: 868_000_000,
369                bandwidth_hz: 125_000,
370                spreading_factor: 8,
371                coding_rate: 5,
372            }
373        );
374        assert_eq!(
375            high_discovery.advertisement,
376            DiscoveryAdvertisementPlan::RNode {
377                frequency_hz: 2_400_000_000,
378                bandwidth_hz: 812_500,
379                spreading_factor: 7,
380                coding_rate: 6,
381            }
382        );
383    }
384
385    #[test]
386    fn discoverable_members_preserve_explicit_internal_mode() {
387        let plan = plan_of(
388            "[interfaces]\n[[Dual]]\ntype = RNodeMultiInterface\nenabled = Yes\nport = /dev/ttyACM0\n\
389             mode = internal\ndiscoverable = Yes\n\
390             [[[Radio]]]\ninterface_enabled = Yes\nvport = 0\nfrequency = 868000000\n\
391             bandwidth = 125000\ntxpower = 7\nspreadingfactor = 8\ncodingrate = 5\n",
392        );
393
394        assert_eq!(
395            named(&plan, "Dual[Radio]").policy.mode,
396            InterfaceMode::Internal,
397        );
398    }
399}