1use 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
44pub 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
187const 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
201impl 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) = ®ulator.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 #[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
429impl 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
462impl 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
632impl 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)] 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)] 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
723impl 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)] 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)] 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
817impl 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
869impl 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
930impl 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
993impl 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
1031impl 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
1067impl 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
1114impl 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
1144impl 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
1174impl 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
1206pub mod uk_rsgb;
1210pub mod us_fcc;