prns-config 0.3.4

Config discovery, parsing, and reconciliation for the Personal Reticulum daemon
Documentation
use prns_core::interface_discovery::{StampCost, DEFAULT_STAMP_COST};
use prns_core::interfaces::tcp::TcpWireFraming;
use prns_core::units::DurationMillis;

use super::medium::PlannedMedium;
use crate::reference::keys::interface as interface_key;
use crate::reference::{ReferenceConfigParams, ReferenceInterface};

#[derive(Debug, Clone, PartialEq)]
pub enum InterfaceDiscoveryPlan {
    Disabled,
    Announce(DiscoveryAnnouncementPlan),
    Unpublishable(DiscoveryPublicationProblem),
}

#[derive(Debug, Clone, PartialEq)]
pub struct DiscoveryAnnouncementPlan {
    pub interval: DurationMillis,
    pub stamp_cost: StampCost,
    pub name: Option<String>,
    pub encryption: DiscoveryEncryption,
    pub ifac: DiscoveryIfacPublication,
    pub location: DiscoveryLocationPlan,
    pub advertisement: DiscoveryAdvertisementPlan,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiscoveryAdvertisementPlan {
    Backbone {
        reachable_on: String,
        port: u16,
    },
    TcpServer {
        reachable_on: String,
        port: u16,
    },
    RNode {
        frequency_hz: u64,
        bandwidth_hz: u32,
        spreading_factor: u8,
        coding_rate: u8,
    },
    Kiss {
        frequency_hz: u64,
        bandwidth_hz: u32,
        modulation: String,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveryPublicationProblem {
    UnsupportedInterfaceType,
    MissingRequiredSetting { key: &'static str },
    IncompatibleSetting { key: &'static str },
}

#[derive(Debug, Clone, PartialEq)]
pub enum DiscoveryEncryption {
    Plaintext,
    NetworkIdentity,
}

#[derive(Debug, Clone, PartialEq)]
pub enum DiscoveryIfacPublication {
    Omit,
    Include,
}

#[derive(Debug, Clone, PartialEq)]
pub struct DiscoveryLocationPlan {
    pub latitude: Option<f64>,
    pub longitude: Option<f64>,
    pub height: Option<f64>,
}

pub(in crate::plan) fn plan_interface_discovery(
    interface: &ReferenceInterface,
    medium: &PlannedMedium,
) -> InterfaceDiscoveryPlan {
    if interface.discovery.discoverable != Some(true) {
        return InterfaceDiscoveryPlan::Disabled;
    }
    let advertisement = match plan_discovery_advertisement(interface, medium) {
        Ok(advertisement) => advertisement,
        Err(problem) => return InterfaceDiscoveryPlan::Unpublishable(problem),
    };
    let minutes = interface
        .discovery
        .announce_interval_minutes
        .unwrap_or(6 * 60)
        .max(5) as u64;
    InterfaceDiscoveryPlan::Announce(DiscoveryAnnouncementPlan {
        interval: DurationMillis(minutes.saturating_mul(60 * 1_000)),
        stamp_cost: interface.discovery.stamp_cost.unwrap_or(DEFAULT_STAMP_COST),
        name: interface.discovery.name.clone(),
        encryption: if interface.discovery.encrypt == Some(true) {
            DiscoveryEncryption::NetworkIdentity
        } else {
            DiscoveryEncryption::Plaintext
        },
        ifac: if interface.discovery.publish_ifac == Some(true) {
            DiscoveryIfacPublication::Include
        } else {
            DiscoveryIfacPublication::Omit
        },
        location: DiscoveryLocationPlan {
            latitude: interface.discovery.latitude,
            longitude: interface.discovery.longitude,
            height: interface.discovery.height,
        },
        advertisement,
    })
}

fn plan_discovery_advertisement(
    interface: &ReferenceInterface,
    medium: &PlannedMedium,
) -> Result<DiscoveryAdvertisementPlan, DiscoveryPublicationProblem> {
    let reachable_on = || {
        interface.discovery.reachable_on.clone().ok_or(
            DiscoveryPublicationProblem::MissingRequiredSetting {
                key: interface_key::REACHABLE_ON,
            },
        )
    };
    let kiss = || {
        Ok(DiscoveryAdvertisementPlan::Kiss {
            frequency_hz: interface.discovery.frequency_hz.ok_or(
                DiscoveryPublicationProblem::MissingRequiredSetting {
                    key: interface_key::DISCOVERY_FREQUENCY,
                },
            )?,
            bandwidth_hz: interface.discovery.bandwidth_hz.ok_or(
                DiscoveryPublicationProblem::MissingRequiredSetting {
                    key: interface_key::DISCOVERY_BANDWIDTH,
                },
            )?,
            modulation: interface.discovery.modulation.clone().ok_or(
                DiscoveryPublicationProblem::MissingRequiredSetting {
                    key: interface_key::DISCOVERY_MODULATION,
                },
            )?,
        })
    };
    match (medium, &interface.params) {
        (
            PlannedMedium::Backbone { .. },
            ReferenceConfigParams::Backbone {
                listen_port, port, ..
            },
        ) => Ok(DiscoveryAdvertisementPlan::Backbone {
            reachable_on: reachable_on()?,
            port: interface
                .discovery
                .reachable_port
                .or(*port)
                .or(*listen_port)
                .ok_or(DiscoveryPublicationProblem::MissingRequiredSetting {
                    key: interface_key::LISTEN_PORT,
                })?,
        }),
        (
            PlannedMedium::TcpServer { .. },
            ReferenceConfigParams::TcpServer {
                listen_port, port, ..
            },
        ) => Ok(DiscoveryAdvertisementPlan::TcpServer {
            reachable_on: reachable_on()?,
            port: interface
                .discovery
                .reachable_port
                .or(*port)
                .or(*listen_port)
                .ok_or(DiscoveryPublicationProblem::MissingRequiredSetting {
                    key: interface_key::LISTEN_PORT,
                })?,
        }),
        (
            PlannedMedium::Rnode {
                frequency_hz,
                bandwidth_hz,
                spreading_factor,
                coding_rate,
                ..
            },
            ReferenceConfigParams::Rnode { .. },
        ) => Ok(rnode_discovery_advertisement(
            *frequency_hz,
            *bandwidth_hz,
            *spreading_factor,
            *coding_rate,
        )),
        (PlannedMedium::RnodeMulti { member }, ReferenceConfigParams::RnodeMulti { .. }) => {
            let radio = member.radio();
            Ok(rnode_discovery_advertisement(
                u64::from(radio.frequency().hz()),
                radio.bandwidth_hz(),
                radio.spreading_factor(),
                radio.coding_rate(),
            ))
        }
        (PlannedMedium::Kiss { .. }, ReferenceConfigParams::Kiss { .. }) => kiss(),
        (
            PlannedMedium::TcpClient {
                framing: TcpWireFraming::Kiss,
                ..
            },
            ReferenceConfigParams::TcpClient { .. },
        ) => kiss(),
        (PlannedMedium::TcpClient { .. }, ReferenceConfigParams::TcpClient { .. }) => {
            Err(DiscoveryPublicationProblem::IncompatibleSetting {
                key: interface_key::KISS_FRAMING,
            })
        }
        _ => Err(DiscoveryPublicationProblem::UnsupportedInterfaceType),
    }
}

fn rnode_discovery_advertisement(
    frequency_hz: u64,
    bandwidth_hz: u32,
    spreading_factor: u8,
    coding_rate: u8,
) -> DiscoveryAdvertisementPlan {
    DiscoveryAdvertisementPlan::RNode {
        frequency_hz,
        bandwidth_hz,
        spreading_factor,
        coding_rate,
    }
}