Skip to main content

rfham_bands/
lib.rs

1//! Amateur radio band plan data structures.
2//!
3//! A [`BandPlan`] groups frequency segments, license classes, calling frequencies, and
4//! power/mode restrictions for a specific regulatory authority and ITU region.  Concrete
5//! plans (US ARRL, UK RSGB) live in the sub-modules [`us_fcc`] and [`uk_rsgb`].
6//!
7//! | Type | Purpose |
8//! |------|---------|
9//! | [`BandPlan`] | Top-level plan with agency metadata and a map of [`PlanBand`]s |
10//! | [`Band`] | A frequency range for one ITU allocation |
11//! | [`PlanBand`] | A `Band` plus restrictions, segments, and calling frequencies |
12//! | [`Segment`] | A sub-range within a band with its own restrictions |
13//! | [`BandRestrictions`] | License, usage, power, and bandwidth constraints |
14//!
15//! # Examples
16//!
17//! ```rust,no_run
18//! use rfham_bands::us_fcc::arrl_voluntary_band_plan;
19//! use rfham_markdown::ToMarkdownWith;
20//! use std::io::stdout;
21//!
22//! arrl_voluntary_band_plan()
23//!     .write_markdown_with(&mut stdout(), vec![])
24//!     .unwrap();
25//! ```
26
27use colored::Colorize;
28use rfham_core::{
29    agencies::Agency,
30    countries::CountryCode,
31    error::CoreError,
32    frequencies::{Frequency, FrequencyRange},
33    power::Power,
34};
35use rfham_itu::{allocations::FrequencyAllocation, regions::Region};
36use rfham_markdown::{
37    MarkdownError, Table, ToMarkdown, ToMarkdownWith, blank_line, bulleted_list,
38    bulleted_list_item, header, link_to_string, numbered_list_item, plain_text,
39};
40use serde::{Deserialize, Serialize};
41use serde_with::{DeserializeFromStr, SerializeDisplay};
42use std::{cmp::Ordering, collections::HashMap, fmt::Display, str::FromStr};
43
44// ------------------------------------------------------------------------------------------------
45// Public Types
46// ------------------------------------------------------------------------------------------------
47
48pub type DisplayName = String;
49pub type LicenseKey = String;
50
51#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
52pub struct BandPlan {
53    name: DisplayName,
54    maintainer: Agency,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    regulator: Option<Agency>,
57    region: Region,
58    #[serde(skip_serializing_if = "Vec::is_empty")]
59    countries: Vec<CountryCode>,
60    #[serde(skip_serializing_if = "HashMap::is_empty")]
61    licenses: HashMap<LicenseKey, LicenseClass>,
62    #[serde(skip_serializing_if = "HashMap::is_empty")]
63    bands: HashMap<FrequencyAllocation, PlanBand>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    default_max_power: Option<Power>,
66    #[serde(skip_serializing_if = "Vec::is_empty")]
67    notes: Vec<String>,
68}
69
70#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
71pub struct Band {
72    allocation: FrequencyAllocation,
73    #[serde(flatten)]
74    range: FrequencyRange,
75}
76
77#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
78pub struct PlanBand {
79    band: Band,
80    is_primary_user: bool,
81    #[serde(skip_serializing_if = "BandRestrictions::is_unrestricted")]
82    restrictions: BandRestrictions,
83    #[serde(skip_serializing_if = "Vec::is_empty")]
84    segments: Vec<Segment>,
85    #[serde(skip_serializing_if = "Vec::is_empty")]
86    calling_frequencies: Vec<CallingFrequency>,
87    #[serde(skip_serializing_if = "Vec::is_empty")]
88    notes: Vec<String>,
89}
90
91#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
92pub struct Segment {
93    #[serde(flatten)]
94    range: FrequencyRange,
95    #[serde(skip_serializing_if = "BandRestrictions::is_unrestricted")]
96    restrictions: BandRestrictions,
97    #[serde(skip_serializing_if = "Vec::is_empty")]
98    notes: Vec<String>,
99}
100
101#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
102pub struct BandRestrictions {
103    #[serde(skip_serializing_if = "Vec::is_empty")]
104    license_restrictions: Vec<LicenseKey>,
105    #[serde(skip_serializing_if = "Vec::is_empty")]
106    usage_restrictions: Vec<UsageRestriction>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    power_restriction: Option<PowerRestriction>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    bandwidth_restriction: Option<BandwidthRestriction>,
111}
112
113#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
114pub struct CallingFrequency {
115    frequency: Frequency,
116    name: Option<DisplayName>,
117    #[serde(skip_serializing_if = "Vec::is_empty")]
118    usage_restrictions: Vec<UsageRestriction>,
119}
120
121#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
122pub struct LicenseClass {
123    ordinal: u32,
124    name: DisplayName,
125    is_active: bool,
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
129pub enum UsageRestriction {
130    Beacon,
131    CW,
132    Data,
133    Digital,
134    Dx,
135    EarthMoonEarth,
136    Phone,
137    Qrp,
138    RemoteControl,
139    Repeater(RepeaterUsage),
140    Rtty,
141    Satellite(SatelliteUsage),
142    Simplex,
143    SlowScanTv,
144    Test(RepeaterUsage),
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
148pub enum PhoneMode {
149    AmplitudeModulated,
150    FrequencyModulated,
151    SingleSideband,
152    SingleSidebandLower,
153    SingleSidebandUpper,
154}
155
156#[derive(Clone, Copy, Debug, Default, PartialEq, DeserializeFromStr, SerializeDisplay)]
157pub enum PowerMeasure {
158    #[default]
159    PeakEnvelopePower,
160    EffectiveRadiatedPower,
161    EffectiveIsotropicRadiatedPower,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
165pub struct PowerRestriction {
166    max_power: Power,
167    measure: PowerMeasure,
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
171pub enum RepeaterUsage {
172    Input,
173    Output,
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
177pub enum SatelliteUsage {
178    Downlink,
179    Uplink,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
183pub struct BandwidthRestriction {
184    max_bandwidth: Frequency,
185}
186
187// ------------------------------------------------------------------------------------------------
188// Private Types
189// ------------------------------------------------------------------------------------------------
190
191const START_COL_WIDTH: usize = 10;
192const END_COL_WIDTH: usize = 10;
193const CLASS_COL_WIDTH: usize = 14;
194const USAGE_COL_WIDTH: usize = 20;
195const POWER_COL_WIDTH: usize = 14;
196const BANDWIDTH_COL_WIDTH: usize = 14;
197const CLASS_OR_USAGE_DEFAULT: &str = "any";
198const POWER_DEFAULT: &str = "full power";
199const BANDWIDTH_DEFAULT: &str = "-";
200
201// ------------------------------------------------------------------------------------------------
202// Implementations
203// ------------------------------------------------------------------------------------------------
204
205impl ToMarkdownWith for BandPlan {
206    type Context = Vec<FrequencyAllocation>;
207
208    fn write_markdown_with<W: std::io::Write>(
209        &self,
210        writer: &mut W,
211        context: Self::Context,
212    ) -> Result<(), MarkdownError> {
213        header(writer, 1, &self.name)?;
214        blank_line(writer)?;
215
216        if let Some(regulator) = self.regulator.as_ref() {
217            bulleted_list_item(
218                writer,
219                1,
220                &if let Some(url) = &regulator.url() {
221                    format!(
222                        "Regulatory Agency: {}",
223                        link_to_string(regulator.to_string(), url)
224                    )
225                } else {
226                    format!("Regulatory Agency: {}", regulator)
227                },
228            )?;
229        }
230        bulleted_list_item(
231            writer,
232            1,
233            &if let Some(url) = &self.maintainer.url() {
234                format!(
235                    "Maintaining Agency: {}",
236                    link_to_string(self.maintainer.to_string(), url)
237                )
238            } else {
239                format!("Maintaining Agency: {}", self.maintainer)
240            },
241        )?;
242        bulleted_list_item(writer, 1, format!("Region: {:#}", self.region))?;
243        if !self.countries.is_empty() {
244            bulleted_list_item(
245                writer,
246                1,
247                format!(
248                    "Countries: {}",
249                    self.countries
250                        .iter()
251                        .map(|c| c.as_str())
252                        .collect::<Vec<_>>()
253                        .join(", ")
254                ),
255            )?;
256        }
257        if let Some(default_max_power) = &self.default_max_power {
258            bulleted_list_item(
259                writer,
260                1,
261                format!("Default maximum power: {default_max_power}"),
262            )?;
263        }
264        blank_line(writer)?;
265
266        if !self.notes.is_empty() {
267            plain_text(writer, "Notes:")?;
268            blank_line(writer)?;
269            bulleted_list(writer, 1, &self.notes)?;
270            blank_line(writer)?;
271        }
272
273        header(writer, 2, "License Classes")?;
274        blank_line(writer)?;
275
276        let mut ordered_licenses = self.licenses.iter().collect::<Vec<_>>();
277        ordered_licenses.sort_by(|a, b| a.1.ordinal.cmp(&b.1.ordinal));
278
279        for (number, (key, license)) in ordered_licenses.iter().enumerate() {
280            numbered_list_item(
281                writer,
282                1,
283                number,
284                format!(
285                    "{} ({}){}",
286                    license.name,
287                    key,
288                    if !license.is_active {
289                        format!(" {}", "inactive class".italic())
290                    } else {
291                        String::default()
292                    }
293                ),
294            )?;
295        }
296        blank_line(writer)?;
297
298        let mut ordered_bands = self.bands.values().collect::<Vec<_>>();
299        ordered_bands.sort_by(|a, b| {
300            a.band
301                .range
302                .start()
303                .value()
304                .partial_cmp(&b.band.range.start().value())
305                .unwrap_or(Ordering::Equal)
306        });
307
308        for band in ordered_bands {
309            if context.is_empty() || context.contains(&band.band.allocation) {
310                band.write_markdown(writer)?;
311            }
312        }
313
314        Ok(())
315    }
316}
317
318impl BandPlan {
319    // TODO: factor this out in the future.
320    #[allow(clippy::too_many_arguments)]
321    const fn inner_new(
322        maintaining_agency: Agency,
323        name: String,
324        region: Region,
325        countries: Vec<CountryCode>,
326        licenses: HashMap<String, LicenseClass>,
327        bands: HashMap<FrequencyAllocation, PlanBand>,
328        default_max_power: Option<Power>,
329        notes: Vec<String>,
330    ) -> Self {
331        Self {
332            maintainer: maintaining_agency,
333            regulator: None,
334            name,
335            region,
336            countries,
337            licenses,
338            bands,
339            default_max_power,
340            notes,
341        }
342    }
343
344    pub fn new<S: Into<String>>(maintainer: Agency, region: Region, name: S) -> Self {
345        Self::inner_new(
346            maintainer,
347            name.into(),
348            region,
349            Vec::default(),
350            HashMap::default(),
351            HashMap::default(),
352            Option::default(),
353            Vec::default(),
354        )
355    }
356
357    pub fn with_regulator(mut self, regulator: Agency) -> Self {
358        self.regulator = Some(regulator);
359        self
360    }
361
362    pub fn in_country(mut self, country: CountryCode) -> Self {
363        self.countries.push(country);
364        self
365    }
366
367    pub fn in_countries(mut self, countries: &[CountryCode]) -> Self {
368        self.countries.extend(countries.to_vec());
369        self
370    }
371
372    pub fn with_licenses(mut self, licenses: HashMap<LicenseKey, LicenseClass>) -> Self {
373        self.licenses = licenses;
374        self
375    }
376
377    pub fn with_license_list(mut self, licenses: Vec<(DisplayName, bool)>) -> Self {
378        let licenses: HashMap<LicenseKey, LicenseClass> = licenses
379            .into_iter()
380            .enumerate()
381            .map(|(ordinal, (name, is_active))| {
382                (
383                    name.clone(),
384                    LicenseClass::new(ordinal as u32, &name, is_active),
385                )
386            })
387            .collect();
388        self.licenses = licenses;
389        self
390    }
391
392    pub fn with_bands(mut self, bands: HashMap<FrequencyAllocation, PlanBand>) -> Self {
393        self.bands = bands;
394        self
395    }
396
397    pub fn with_bands_list(self, bands: Vec<PlanBand>) -> Self {
398        self.with_bands(
399            bands
400                .into_iter()
401                .map(|band| (band.band.allocation, band))
402                .collect(),
403        )
404    }
405
406    pub fn with_default_max_power(mut self, default_max_power: Power) -> Self {
407        self.default_max_power = Some(default_max_power);
408        self
409    }
410
411    pub fn with_notes(mut self, notes: Vec<String>) -> Self {
412        self.notes = notes;
413        self
414    }
415
416    pub fn name(&self) -> &str {
417        self.name.as_str()
418    }
419
420    pub fn band(&self, itu: &FrequencyAllocation) -> Option<&PlanBand> {
421        self.bands.get(itu)
422    }
423
424    pub fn maintaining_agency(&self) -> &Agency {
425        &self.maintainer
426    }
427}
428
429// ------------------------------------------------------------------------------------------------
430
431impl Display for Band {
432    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433        write!(f, "{} ({})", self.allocation, self.range)
434    }
435}
436
437impl Band {
438    pub fn new_default(allocation: FrequencyAllocation, region: Region) -> Self {
439        let range = allocation
440            .range(region)
441            .expect("Could not create band from allocation missing in {region:#}.");
442        Self::new(allocation, range.start(), range.end())
443    }
444
445    pub fn new(allocation: FrequencyAllocation, start: Frequency, end: Frequency) -> Self {
446        assert!(start < end);
447        Self {
448            allocation,
449            range: FrequencyRange::new(start, end),
450        }
451    }
452
453    pub fn allocation(&self) -> &FrequencyAllocation {
454        &self.allocation
455    }
456
457    pub fn range(&self) -> &FrequencyRange {
458        &self.range
459    }
460}
461
462// ------------------------------------------------------------------------------------------------
463
464impl ToMarkdown for PlanBand {
465    fn write_markdown<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MarkdownError> {
466        let actual = &self.band;
467        header(
468            writer,
469            2,
470            format!("{} Band ({})", actual.allocation, actual.range),
471        )?;
472        blank_line(writer)?;
473
474        plain_text(
475            writer,
476            format!(
477                "Note: amateur operators are the {} users on this band.",
478                if self.is_primary_user {
479                    "primary"
480                } else {
481                    "secondary"
482                }
483                .bold()
484            ),
485        )?;
486        blank_line(writer)?;
487
488        if !self.calling_frequencies.is_empty() {
489            plain_text(writer, "Calling Frequencies:")?;
490            blank_line(writer)?;
491
492            let mut ordered_calling = self.calling_frequencies.iter().collect::<Vec<_>>();
493            ordered_calling.sort_by(|a, b| {
494                a.frequency
495                    .value()
496                    .partial_cmp(&b.frequency.value())
497                    .unwrap_or(Ordering::Equal)
498            });
499
500            for (i, calling) in ordered_calling.iter().enumerate() {
501                numbered_list_item(
502                    writer,
503                    1,
504                    i,
505                    format!(
506                        "{}{}{}",
507                        calling.frequency,
508                        if let Some(label) = &calling.name {
509                            format!(" {label}")
510                        } else {
511                            String::default()
512                        },
513                        if !calling.usage_restrictions.is_empty() {
514                            format!(
515                                " ({})",
516                                calling
517                                    .usage_restrictions
518                                    .iter()
519                                    .map(|r| r.to_string())
520                                    .collect::<Vec<_>>()
521                                    .join(", ")
522                            )
523                        } else {
524                            String::default()
525                        }
526                    ),
527                )?;
528            }
529        }
530
531        if !self.restrictions.is_unrestricted() || !self.segments.is_empty() {
532            plain_text(writer, "Band restrictions:")?;
533            blank_line(writer)?;
534
535            let table = Table::new(vec![
536                ("Start", START_COL_WIDTH).into(),
537                ("End", END_COL_WIDTH).into(),
538                ("License Class", CLASS_COL_WIDTH).into(),
539                ("Usage/Mode", USAGE_COL_WIDTH).into(),
540                ("Power", POWER_COL_WIDTH).into(),
541                ("Max Bandwidth", BANDWIDTH_COL_WIDTH).into(),
542            ]);
543            table.headers(writer)?;
544
545            if !self.restrictions.is_unrestricted() {
546                self.restrictions
547                    .write_markdown_with(writer, (actual.range.clone(), table.clone()))?;
548            }
549
550            if !self.segments.is_empty() {
551                let mut ordered_segments = self.segments.iter().collect::<Vec<_>>();
552                ordered_segments.sort_by(|a, b| {
553                    a.range
554                        .start()
555                        .value()
556                        .partial_cmp(&b.range.start().value())
557                        .unwrap_or(Ordering::Equal)
558                });
559
560                for segment in ordered_segments {
561                    segment.write_markdown_with(writer, (segment.range.clone(), table.clone()))?;
562                }
563                blank_line(writer)?;
564            }
565        }
566
567        if !self.notes.is_empty() {
568            plain_text(writer, "Notes:")?;
569            blank_line(writer)?;
570
571            bulleted_list(writer, 1, &self.notes)?;
572        }
573        blank_line(writer)?;
574        Ok(())
575    }
576}
577
578impl PlanBand {
579    fn inner_new(
580        band: Band,
581        primary_user: bool,
582        restrictions: BandRestrictions,
583        segments: Vec<Segment>,
584        calling_frequencies: Vec<CallingFrequency>,
585        notes: Vec<String>,
586    ) -> Self {
587        Self {
588            band,
589            is_primary_user: primary_user,
590            restrictions,
591            segments,
592            calling_frequencies,
593            notes,
594        }
595    }
596    pub fn new(band: Band) -> Self {
597        Self::inner_new(
598            band,
599            true,
600            BandRestrictions::none(),
601            Vec::default(),
602            Vec::default(),
603            Vec::default(),
604        )
605    }
606    pub fn with_is_primary_user(mut self, is_primary_user: bool) -> Self {
607        self.is_primary_user = is_primary_user;
608        self
609    }
610    pub fn with_restrictions(mut self, restrictions: BandRestrictions) -> Self {
611        self.restrictions = restrictions;
612        self
613    }
614    pub fn with_segments(mut self, segments: Vec<Segment>) -> Self {
615        self.segments = segments;
616        self
617    }
618    pub fn with_calling_frequencies(mut self, calling_frequencies: Vec<CallingFrequency>) -> Self {
619        self.calling_frequencies = calling_frequencies;
620        self
621    }
622    pub fn with_notes(mut self, notes: Vec<String>) -> Self {
623        self.notes = notes;
624        self
625    }
626
627    pub fn band(&self) -> &Band {
628        &self.band
629    }
630}
631
632// ------------------------------------------------------------------------------------------------
633
634impl ToMarkdownWith for Segment {
635    type Context = (FrequencyRange, Table);
636
637    fn write_markdown_with<W: std::io::Write>(
638        &self,
639        writer: &mut W,
640        context: Self::Context,
641    ) -> Result<(), MarkdownError> {
642        let (range, table) = context;
643        if self.restrictions.is_unrestricted() {
644            table.data_row(
645                writer,
646                &[
647                    range.start().to_string(),
648                    range.end().to_string(),
649                    CLASS_OR_USAGE_DEFAULT.italic().to_string(),
650                    CLASS_OR_USAGE_DEFAULT.italic().to_string(),
651                    POWER_DEFAULT.italic().to_string(),
652                    BANDWIDTH_DEFAULT.to_string(),
653                ],
654            )?;
655        } else {
656            table.data_row(
657                writer,
658                &[
659                    range.start().to_string(),
660                    range.end().to_string(),
661                    if self.restrictions.license_restrictions.is_empty() {
662                        CLASS_OR_USAGE_DEFAULT.to_string()
663                    } else {
664                        #[allow(clippy::iter_cloned_collect)] // false positive
665                        self.restrictions
666                            .license_restrictions
667                            .iter()
668                            .cloned()
669                            .collect::<Vec<_>>()
670                            .join(", ")
671                    },
672                    if self.restrictions.usage_restrictions.is_empty() {
673                        CLASS_OR_USAGE_DEFAULT.to_string()
674                    } else if self.restrictions.usage_restrictions.len() == 1 {
675                        format!(
676                            "{} only",
677                            self.restrictions.usage_restrictions.first().unwrap()
678                        )
679                    } else {
680                        #[allow(clippy::iter_cloned_collect)] // false positive
681                        self.restrictions
682                            .usage_restrictions
683                            .iter()
684                            .map(|r| r.to_string())
685                            .collect::<Vec<_>>()
686                            .join(", ")
687                    },
688                    if let Some(power_restriction) = self.restrictions.power_restriction() {
689                        power_restriction.to_string()
690                    } else {
691                        POWER_DEFAULT.to_string()
692                    },
693                    if let Some(bandwidth_restriction) = self.restrictions.bandwidth_restriction() {
694                        bandwidth_restriction.to_string()
695                    } else {
696                        BANDWIDTH_DEFAULT.to_string()
697                    },
698                ],
699            )?;
700        }
701        Ok(())
702    }
703}
704
705impl Segment {
706    pub fn new(frequency_start: Frequency, frequency_end: Frequency) -> Self {
707        Self {
708            range: FrequencyRange::new(frequency_start, frequency_end),
709            restrictions: Default::default(),
710            notes: Vec::default(),
711        }
712    }
713    pub fn with_restrictions(mut self, restrictions: BandRestrictions) -> Self {
714        self.restrictions = restrictions;
715        self
716    }
717    pub fn with_notes(mut self, notes: Vec<String>) -> Self {
718        self.notes = notes;
719        self
720    }
721}
722
723// ------------------------------------------------------------------------------------------------
724
725impl ToMarkdownWith for BandRestrictions {
726    type Context = (FrequencyRange, Table);
727
728    fn write_markdown_with<W: std::io::Write>(
729        &self,
730        writer: &mut W,
731        context: Self::Context,
732    ) -> Result<(), MarkdownError> {
733        let (range, table) = context;
734        table.data_row(
735            writer,
736            &[
737                range.start().to_string(),
738                range.end().to_string(),
739                if self.license_restrictions.is_empty() {
740                    CLASS_OR_USAGE_DEFAULT.to_string()
741                } else {
742                    #[allow(clippy::iter_cloned_collect)] // false positive
743                    self.license_restrictions
744                        .iter()
745                        .cloned()
746                        .collect::<Vec<_>>()
747                        .join(", ")
748                },
749                if self.usage_restrictions.is_empty() {
750                    CLASS_OR_USAGE_DEFAULT.to_string()
751                } else if self.usage_restrictions.len() == 1 {
752                    format!("{} only", self.usage_restrictions.first().unwrap())
753                } else {
754                    #[allow(clippy::iter_cloned_collect)] // false positive
755                    self.usage_restrictions
756                        .iter()
757                        .map(|r| r.to_string())
758                        .collect::<Vec<_>>()
759                        .join(", ")
760                },
761                if let Some(power_restriction) = self.power_restriction {
762                    power_restriction.to_string()
763                } else {
764                    POWER_DEFAULT.to_string()
765                },
766                if let Some(bandwidth_restriction) = self.bandwidth_restriction() {
767                    bandwidth_restriction.to_string()
768                } else {
769                    BANDWIDTH_DEFAULT.to_string()
770                },
771            ],
772        )?;
773        blank_line(writer)?;
774        Ok(())
775    }
776}
777
778impl BandRestrictions {
779    pub fn none() -> Self {
780        Self::default()
781    }
782    pub fn with_license_restrictions(mut self, license_restrictions: Vec<LicenseKey>) -> Self {
783        self.license_restrictions = license_restrictions;
784        self
785    }
786    pub fn with_usage_restrictions(mut self, usage_restrictions: Vec<UsageRestriction>) -> Self {
787        self.usage_restrictions = usage_restrictions;
788        self
789    }
790    pub fn with_power_restriction(mut self, power_restriction: PowerRestriction) -> Self {
791        self.power_restriction = Some(power_restriction);
792        self
793    }
794    pub fn with_bandwidth_restriction(mut self, max_bandwidth: BandwidthRestriction) -> Self {
795        self.bandwidth_restriction = Some(max_bandwidth);
796        self
797    }
798    pub fn is_unrestricted(&self) -> bool {
799        self.license_restrictions.is_empty()
800            && self.usage_restrictions.is_empty()
801            && self.power_restriction.is_none()
802    }
803    pub fn license_restrictions(&self) -> impl Iterator<Item = &LicenseKey> {
804        self.license_restrictions.iter()
805    }
806    pub fn usage_restrictions(&self) -> impl Iterator<Item = &UsageRestriction> {
807        self.usage_restrictions.iter()
808    }
809    pub fn power_restriction(&self) -> Option<&PowerRestriction> {
810        self.power_restriction.as_ref()
811    }
812    pub fn bandwidth_restriction(&self) -> Option<&BandwidthRestriction> {
813        self.bandwidth_restriction.as_ref()
814    }
815}
816
817// ------------------------------------------------------------------------------------------------
818
819impl Display for CallingFrequency {
820    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
821        write!(
822            f,
823            "{:.3}: Calling{}",
824            self.frequency,
825            if self.usage_restrictions.is_empty() {
826                String::default()
827            } else {
828                self.usage_restrictions
829                    .iter()
830                    .map(|v| v.to_string())
831                    .collect::<Vec<_>>()
832                    .join(" ")
833            }
834        )
835    }
836}
837
838impl CallingFrequency {
839    pub const fn new(frequency: Frequency, usage_restrictions: Vec<UsageRestriction>) -> Self {
840        Self {
841            frequency,
842            name: None,
843            usage_restrictions,
844        }
845    }
846    pub const fn unrestricted(frequency: Frequency) -> Self {
847        Self {
848            frequency,
849            name: None,
850            usage_restrictions: Vec::new(),
851        }
852    }
853    pub fn with_usage_restrictions(mut self, usage_restrictions: Vec<UsageRestriction>) -> Self {
854        self.usage_restrictions = usage_restrictions;
855        self
856    }
857    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
858        self.name = Some(label.into());
859        self
860    }
861    pub const fn frequency(&self) -> Frequency {
862        self.frequency
863    }
864    pub fn usage_restrictions(&self) -> impl Iterator<Item = &UsageRestriction> {
865        self.usage_restrictions.iter()
866    }
867}
868
869// ------------------------------------------------------------------------------------------------
870
871impl Display for LicenseClass {
872    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
873        write!(
874            f,
875            "{}",
876            if f.alternate() {
877                format!(
878                    "{:02}: {} ({})",
879                    self.ordinal,
880                    self.name,
881                    if self.is_active { "active" } else { "inactive" }
882                )
883            } else {
884                format!(
885                    "{}{}",
886                    self.name,
887                    if self.is_active { "" } else { " (inactive)" }
888                )
889            }
890        )
891    }
892}
893
894impl LicenseClass {
895    pub fn new(ordinal: u32, name: &str, is_active: bool) -> Self {
896        Self {
897            name: name.to_string(),
898            ordinal,
899            is_active,
900        }
901    }
902    pub fn active(ordinal: u32, name: &str) -> Self {
903        Self {
904            name: name.to_string(),
905            ordinal,
906            is_active: true,
907        }
908    }
909    pub fn inactive(ordinal: u32, name: &str) -> Self {
910        Self {
911            name: name.to_string(),
912            ordinal,
913            is_active: false,
914        }
915    }
916
917    pub fn name(&self) -> &DisplayName {
918        &self.name
919    }
920
921    pub fn ordinal(&self) -> u32 {
922        self.ordinal
923    }
924
925    pub fn is_active(&self) -> bool {
926        self.is_active
927    }
928}
929
930// ------------------------------------------------------------------------------------------------
931
932impl Display for UsageRestriction {
933    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
934        write!(
935            f,
936            "{}",
937            match self {
938                UsageRestriction::Beacon => "beacon".to_string(),
939                UsageRestriction::CW => "CW".to_string(),
940                UsageRestriction::Data => "data".to_string(),
941                UsageRestriction::Digital => "digital".to_string(),
942                UsageRestriction::Dx => "DX".to_string(),
943                UsageRestriction::EarthMoonEarth => "EME".to_string(),
944                UsageRestriction::Phone => "phone".to_string(),
945                UsageRestriction::Qrp => "QRP".to_string(),
946                UsageRestriction::RemoteControl => "remote-control".to_string(),
947                UsageRestriction::Repeater(repeater_usage) => format!("repeater {repeater_usage}"),
948                UsageRestriction::Rtty => "RTTY".to_string(),
949                UsageRestriction::Satellite(satellite_usage) =>
950                    format!("satellite {satellite_usage}"),
951                UsageRestriction::Simplex => "simplex".to_string(),
952                UsageRestriction::SlowScanTv => "SSTV".to_string(),
953                UsageRestriction::Test(repeater_usage) => format!("test-pair {repeater_usage}"),
954            }
955        )
956    }
957}
958
959impl FromStr for UsageRestriction {
960    type Err = CoreError;
961
962    fn from_str(s: &str) -> Result<Self, Self::Err> {
963        match s {
964            "beacon" => Ok(Self::Beacon),
965            "CW" => Ok(Self::CW),
966            "data" => Ok(Self::Data),
967            "digital" => Ok(Self::Digital),
968            "DX" => Ok(Self::Dx),
969            "EQE" => Ok(Self::EarthMoonEarth),
970            "QRP" => Ok(Self::Qrp),
971            "remote-control" => Ok(Self::RemoteControl),
972            "RTTY" => Ok(Self::Rtty),
973            "simplex" => Ok(Self::Simplex),
974            "SSTV" => Ok(Self::SlowScanTv),
975            _ => {
976                if let Some(repeater) = s.strip_prefix("repeater") {
977                    Ok(Self::Repeater(RepeaterUsage::from_str(repeater.trim())?))
978                } else if let Some(satellite) = s.strip_prefix("satellite") {
979                    Ok(Self::Satellite(SatelliteUsage::from_str(satellite.trim())?))
980                } else if let Some(test_pair) = s.strip_prefix("test-pair") {
981                    Ok(Self::Test(RepeaterUsage::from_str(test_pair.trim())?))
982                } else {
983                    Err(CoreError::InvalidValueFromStr(
984                        s.to_string(),
985                        "UsageRestriction",
986                    ))
987                }
988            }
989        }
990    }
991}
992
993// ------------------------------------------------------------------------------------------------
994
995impl Display for PhoneMode {
996    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
997        write!(
998            f,
999            "{}",
1000            match (self, f.alternate()) {
1001                (PhoneMode::AmplitudeModulated, true) => "Amplitude Modulation (AM)",
1002                (PhoneMode::AmplitudeModulated, false) => "AM",
1003                (PhoneMode::FrequencyModulated, true) => "FrequencyModulation (FM)",
1004                (PhoneMode::FrequencyModulated, false) => "FM",
1005                (PhoneMode::SingleSideband, true) => "Single Sideband (SSB)",
1006                (PhoneMode::SingleSideband, false) => "SSB",
1007                (PhoneMode::SingleSidebandLower, true) => "Lower Sideband (LSB)",
1008                (PhoneMode::SingleSidebandLower, false) => "LSB",
1009                (PhoneMode::SingleSidebandUpper, true) => "Upper Sideband (USB)",
1010                (PhoneMode::SingleSidebandUpper, false) => "USB",
1011            }
1012        )
1013    }
1014}
1015
1016impl FromStr for PhoneMode {
1017    type Err = CoreError;
1018
1019    fn from_str(s: &str) -> Result<Self, Self::Err> {
1020        match s {
1021            "AM" => Ok(Self::AmplitudeModulated),
1022            "FM" => Ok(Self::FrequencyModulated),
1023            "SSB" => Ok(Self::SingleSideband),
1024            "LSB" => Ok(Self::SingleSidebandLower),
1025            "USB" => Ok(Self::SingleSidebandUpper),
1026            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "PhoneMode")),
1027        }
1028    }
1029}
1030
1031// ------------------------------------------------------------------------------------------------
1032
1033impl Display for PowerMeasure {
1034    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1035        write!(
1036            f,
1037            "{}",
1038            match (self, f.alternate()) {
1039                (PowerMeasure::PeakEnvelopePower, true) => "Peak Envelope Power (PEP)",
1040                (PowerMeasure::PeakEnvelopePower, false) => "PEP",
1041                (PowerMeasure::EffectiveRadiatedPower, true) => "Effective Radiated Power (ERP)",
1042                (PowerMeasure::EffectiveRadiatedPower, false) => "ERP",
1043                (PowerMeasure::EffectiveIsotropicRadiatedPower, true) =>
1044                    "Effective Isotropic Radiated Power (EIRP)",
1045                (PowerMeasure::EffectiveIsotropicRadiatedPower, false) => "EIRP",
1046            }
1047        )
1048    }
1049}
1050
1051impl FromStr for PowerMeasure {
1052    type Err = CoreError;
1053
1054    fn from_str(s: &str) -> Result<Self, Self::Err> {
1055        match s {
1056            "PEP" => Ok(Self::PeakEnvelopePower),
1057            "ERP" => Ok(Self::EffectiveRadiatedPower),
1058            "EIRP" => Ok(Self::EffectiveIsotropicRadiatedPower),
1059            _ => Err(CoreError::InvalidValueFromStr(
1060                s.to_string(),
1061                "PowerMeasure",
1062            )),
1063        }
1064    }
1065}
1066
1067// ------------------------------------------------------------------------------------------------
1068
1069impl Display for PowerRestriction {
1070    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1071        write!(f, "{} {}", self.max_power, self.measure)
1072    }
1073}
1074
1075impl<T> From<T> for PowerRestriction
1076where
1077    T: Into<Power>,
1078{
1079    fn from(value: T) -> Self {
1080        Self::pep(value.into())
1081    }
1082}
1083
1084impl PowerRestriction {
1085    pub fn new(max_power: Power, measure: PowerMeasure) -> Self {
1086        Self { max_power, measure }
1087    }
1088    pub fn pep(max_power: Power) -> Self {
1089        Self {
1090            max_power,
1091            measure: PowerMeasure::PeakEnvelopePower,
1092        }
1093    }
1094    pub fn erp(max_power: Power) -> Self {
1095        Self {
1096            max_power,
1097            measure: PowerMeasure::EffectiveRadiatedPower,
1098        }
1099    }
1100    pub fn eirp(max_power: Power) -> Self {
1101        Self {
1102            max_power,
1103            measure: PowerMeasure::EffectiveIsotropicRadiatedPower,
1104        }
1105    }
1106    pub fn max_power(&self) -> Power {
1107        self.max_power
1108    }
1109    pub fn measure(&self) -> PowerMeasure {
1110        self.measure
1111    }
1112}
1113
1114// ------------------------------------------------------------------------------------------------
1115
1116impl Display for RepeaterUsage {
1117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1118        write!(
1119            f,
1120            "{}",
1121            match self {
1122                RepeaterUsage::Input => "input",
1123                RepeaterUsage::Output => "output",
1124            }
1125        )
1126    }
1127}
1128
1129impl FromStr for RepeaterUsage {
1130    type Err = CoreError;
1131
1132    fn from_str(s: &str) -> Result<Self, Self::Err> {
1133        match s {
1134            "input" => Ok(Self::Input),
1135            "output" => Ok(Self::Output),
1136            _ => Err(CoreError::InvalidValueFromStr(
1137                s.to_string(),
1138                "RepeaterUsage",
1139            )),
1140        }
1141    }
1142}
1143
1144// ------------------------------------------------------------------------------------------------
1145
1146impl Display for SatelliteUsage {
1147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1148        write!(
1149            f,
1150            "{}",
1151            match self {
1152                SatelliteUsage::Downlink => "downlink",
1153                SatelliteUsage::Uplink => "uplink",
1154            }
1155        )
1156    }
1157}
1158
1159impl FromStr for SatelliteUsage {
1160    type Err = CoreError;
1161
1162    fn from_str(s: &str) -> Result<Self, Self::Err> {
1163        match s {
1164            "downlink" => Ok(Self::Downlink),
1165            "uplink" => Ok(Self::Uplink),
1166            _ => Err(CoreError::InvalidValueFromStr(
1167                s.to_string(),
1168                "SatelliteUsage",
1169            )),
1170        }
1171    }
1172}
1173
1174// ------------------------------------------------------------------------------------------------
1175
1176impl Display for BandwidthRestriction {
1177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1178        write!(f, "{}", self.max_bandwidth)
1179    }
1180}
1181
1182impl From<Frequency> for BandwidthRestriction {
1183    fn from(value: Frequency) -> Self {
1184        Self::new(value)
1185    }
1186}
1187
1188impl From<f64> for BandwidthRestriction {
1189    fn from(value: f64) -> Self {
1190        Self::new(value)
1191    }
1192}
1193
1194impl BandwidthRestriction {
1195    pub fn new<F: Into<Frequency>>(max_bandwidth: F) -> Self {
1196        Self {
1197            max_bandwidth: max_bandwidth.into(),
1198        }
1199    }
1200
1201    pub fn max_bandwidth(&self) -> Frequency {
1202        self.max_bandwidth
1203    }
1204}
1205
1206// ------------------------------------------------------------------------------------------------
1207// Sub-Modules
1208// ------------------------------------------------------------------------------------------------
1209pub mod uk_rsgb;
1210pub mod us_fcc;