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}