Skip to main content

rfham_config/
lib.rs

1//! Station configuration types for RF-Ham.
2//!
3//! [`Configuration`] is the root serialisable type, stored as TOML in the user's
4//! XDG/platform config directory under `rfham/rfham-config.toml`.
5//!
6//! | Type | Purpose |
7//! |------|---------|
8//! | [`Configuration`] | Root config: station + equipment list |
9//! | [`Station`] | Operator callsign, location, and name |
10//! | [`Location`] | Maidenhead locator, ITU region, country, postal address |
11//! | [`Equipment`] | Radio or accessory with power, bands, modes, mobility |
12//! | [`LocationKind`] | `home`, `alternate`, `remote`, `club` |
13//! | [`Mobility`] | `station-fixed`, `portable`, `mobile`, `handheld` |
14//! | [`Usage`] | Operating purpose: `local`, `qrp`, `dx`, `emcomm`, etc. |
15//! | [`Mode`] | `am`, `fm`, `ssb`, `rtty`, `digital`, `image` |
16//!
17//! The [`Dump`] trait writes a human-readable tree to any `Write` sink.
18//!
19//! # Examples
20//!
21//! ```rust,no_run
22//! use rfham_config::Configuration;
23//!
24//! let config = Configuration::load().unwrap_or_default();
25//! if let Some(station) = config.station() {
26//!     println!("Callsign: {}", station.callsign());
27//! }
28//! ```
29
30use crate::error::{ConfigError, ConfigResult};
31use rfham_core::{CountryCode, Name, Power, callsigns::CallSign, error::CoreError};
32use rfham_itu::{bands::FrequencyBand, regions::Region};
33use rfham_maidenhead::MaidenheadLocator;
34use serde::{Deserialize, Serialize};
35use serde_with::{DeserializeFromStr, SerializeDisplay};
36use std::{
37    fmt::Display,
38    fs::{self, File, create_dir_all},
39    io::Write,
40    path::{Path, PathBuf},
41    str::FromStr,
42};
43use tracing::trace;
44
45// ────────────────────────────────────────────────────────────────────────────────────────────────
46// Public Types
47// ────────────────────────────────────────────────────────────────────────────────────────────────
48
49#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
50#[serde(rename_all = "kebab-case")]
51pub struct Configuration {
52    #[serde(skip)]
53    path: Option<PathBuf>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    station: Option<Station>,
56    #[serde(skip_serializing_if = "Vec::is_empty")]
57    equipment: Vec<Equipment>,
58}
59
60#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
61#[serde(rename_all = "kebab-case")]
62pub struct Station {
63    callsign: CallSign,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    location: Option<Location>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    operator_name: Option<String>,
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
71#[serde(rename_all = "kebab-case")]
72pub struct Location {
73    kind: LocationKind,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    label: Option<String>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    grid_locator: Option<MaidenheadLocator>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    itu_region: Option<Region>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    country: Option<CountryCode>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    mailing_address: Option<String>,
84}
85
86#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
87#[serde(rename_all = "kebab-case")]
88pub struct Equipment {
89    brand: Name,
90    model: Name,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    label: Option<String>,
93    #[serde(skip_serializing_if = "Vec::is_empty")]
94    usage: Vec<Usage>,
95    #[serde(skip_serializing_if = "Vec::is_empty")]
96    modes: Vec<Mode>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    mobility: Option<Mobility>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    max_power: Option<Power>,
101    #[serde(skip_serializing_if = "Vec::is_empty")]
102    bands: Vec<FrequencyBand>,
103    #[serde(skip_serializing_if = "Vec::is_empty")]
104    using: Vec<Equipment>,
105}
106
107#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
108pub enum LocationKind {
109    #[default]
110    Home,
111    Alternate,
112    Remote,
113    Club,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
117pub enum Mobility {
118    StationFixed,
119    Portable,
120    Mobile,
121    Handheld,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
125pub enum Usage {
126    Local,
127    Qrp,
128    Dx,
129    EmComm,
130    Activation,
131    Satellite,
132    Scanning,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
136pub enum Mode {
137    Am,
138    Fm,
139    Ssb,
140    Rtty,
141    Digital,
142    Image,
143}
144
145pub const CONFIG_DIR_NAME: &str = "rfham";
146pub const CONFIG_FILE_NAME: &str = "rfham-config.toml";
147
148pub trait Dump {
149    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()>;
150}
151
152// ────────────────────────────────────────────────────────────────────────────────────────────────
153// Implementations
154// ────────────────────────────────────────────────────────────────────────────────────────────────
155
156impl Dump for Configuration {
157    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
158        writeln!(writer, "Configuration:")?;
159        if let Some(path) = &self.path {
160            writeln!(writer, "{prefix}└── path to file: {path:?}")?;
161        }
162        if let Some(station) = &self.station {
163            station.dump(writer, "    ")?;
164        }
165        if !self.equipment.is_empty() {
166            writeln!(writer, "{prefix}└── Equipment:")?;
167            for equipment in &self.equipment {
168                equipment.dump(writer, "    ")?;
169            }
170        }
171        Ok(())
172    }
173}
174
175impl Configuration {
176    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
177    // Constructors
178    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
179    pub fn with_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
180        self.path = path.map(|p| p.into());
181        self
182    }
183
184    pub fn with_station(mut self, station: Option<Station>) -> Self {
185        self.station = station;
186        self
187    }
188
189    pub fn with_equipment(mut self, equipment: Vec<Equipment>) -> Self {
190        self.equipment = equipment;
191        self
192    }
193
194    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
195    // Field accessors
196    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
197
198    pub fn path(&self) -> Option<&PathBuf> {
199        self.path.as_ref()
200    }
201
202    pub fn set_path<P: Into<PathBuf>>(&mut self, path: P) {
203        self.path = Some(path.into())
204    }
205
206    pub fn unset_path(&mut self) {
207        self.path = None
208    }
209
210    pub fn station(&self) -> Option<&Station> {
211        self.station.as_ref()
212    }
213
214    pub fn set_station(&mut self, station: Station) {
215        self.station = Some(station)
216    }
217
218    pub fn unset_station(&mut self) {
219        self.station = None
220    }
221
222    pub fn exists(&self) -> bool {
223        Self::default_file_path()
224            .map(|path| path.exists())
225            .unwrap_or_default()
226    }
227
228    pub fn default_file_path() -> ConfigResult<PathBuf> {
229        Ok(xdirs::config_dir_for(CONFIG_DIR_NAME)
230            .ok_or(ConfigError::ConfigDir)?
231            .join(CONFIG_FILE_NAME))
232    }
233
234    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
235    // File I/O
236    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
237
238    pub fn load() -> ConfigResult<Self> {
239        Self::load_from(Self::default_file_path()?)
240    }
241
242    pub fn load_from<P: AsRef<Path>>(path: P) -> ConfigResult<Self> {
243        let content = fs::read_to_string(path.as_ref())?;
244        let mut config: Configuration = toml::from_str(&content)?;
245        config.set_path(PathBuf::from(path.as_ref()));
246        Ok(config)
247    }
248
249    pub fn save_to<P: AsRef<Path>>(&mut self, path: P, overwrite: bool) -> ConfigResult<()> {
250        self.path = Some(PathBuf::from(path.as_ref()));
251        let parent_dir = self.path.as_ref().map(|p| p.parent()).unwrap_or_default();
252        if let Some(actual_parent_dir) = parent_dir
253            && !actual_parent_dir.is_dir()
254        {
255            trace!("creating parent directory for config file");
256            create_dir_all(actual_parent_dir)?;
257        }
258        let mut file = if overwrite {
259            File::create(path)
260        } else {
261            File::create_new(path)
262        }?;
263        let content = toml::to_string_pretty(self)?;
264        file.write_all(content.as_bytes())?;
265
266        Ok(())
267    }
268
269    pub fn save(&mut self, overwrite: bool) -> ConfigResult<()> {
270        let path = if let Some(path) = &self.path {
271            path.clone()
272        } else {
273            Self::default_file_path()?
274        };
275        self.save_to(path, overwrite)
276    }
277}
278
279// ════════════════════════════════════════════════════════════════════════════════════════════════
280
281impl Dump for Station {
282    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
283        writeln!(writer, "{prefix}Station:")?;
284        writeln!(writer, "{prefix}└── call sign: {}", self.callsign)?;
285        if let Some(operator_name) = &self.operator_name {
286            writeln!(writer, "{prefix}└── operator name: {operator_name}")?;
287        }
288        if let Some(location) = &self.location {
289            location.dump(writer, &format!("{prefix}    "))?;
290        }
291        Ok(())
292    }
293}
294
295impl Station {
296    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
297    // Constructors
298    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
299
300    pub const fn new(callsign: CallSign) -> Self {
301        Self {
302            callsign,
303            location: None,
304            operator_name: None,
305        }
306    }
307
308    pub fn with_location(mut self, location: Option<Location>) -> Self {
309        self.location = location;
310        self
311    }
312
313    pub fn with_operator_name<S: Into<String>>(mut self, operator_name: Option<S>) -> Self {
314        self.operator_name = operator_name.map(|s| s.into());
315        self
316    }
317
318    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
319    // Field Accessors
320    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
321
322    pub const fn callsign(&self) -> &CallSign {
323        &self.callsign
324    }
325
326    pub fn set_callsign(&mut self, callsign: CallSign) {
327        self.callsign = callsign
328    }
329
330    pub const fn location(&self) -> Option<&Location> {
331        self.location.as_ref()
332    }
333
334    pub fn set_location(&mut self, location: Location) {
335        self.location = Some(location);
336    }
337
338    pub fn unset_location(&mut self) {
339        self.location = None
340    }
341
342    pub const fn operator_name(&self) -> Option<&String> {
343        self.operator_name.as_ref()
344    }
345
346    pub fn set_operator_name<S: Into<String>>(&mut self, operator_name: S) {
347        self.operator_name = Some(operator_name.into())
348    }
349
350    pub fn unset_operator_name(&mut self) {
351        self.operator_name = None
352    }
353}
354
355// ════════════════════════════════════════════════════════════════════════════════════════════════
356
357impl Dump for Location {
358    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
359        writeln!(writer, "{prefix}Location:")?;
360        if let Some(grid_locator) = &self.grid_locator {
361            writeln!(writer, "{prefix}└── grid locator: {grid_locator}")?;
362        }
363        if let Some(region) = &self.itu_region {
364            writeln!(writer, "{prefix}└── ITU region: {region}")?;
365        }
366        if let Some(country) = &self.country {
367            writeln!(writer, "{prefix}└── country: {country}")?;
368        }
369        if let Some(mailing_address) = &self.mailing_address {
370            writeln!(writer, "{prefix}└── mailing address: {mailing_address}")?;
371        }
372        Ok(())
373    }
374}
375
376impl Location {
377    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
378    // Constructors
379    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
380
381    pub fn new(kind: LocationKind) -> Self {
382        Self {
383            kind,
384            label: None,
385            grid_locator: None,
386            itu_region: None,
387            country: None,
388            mailing_address: None,
389        }
390    }
391
392    pub fn with_label<S: Into<String>>(mut self, label: Option<S>) -> Self {
393        self.label = label.map(|s| s.into());
394        self
395    }
396
397    pub fn with_grid_locator(mut self, grid_locator: Option<MaidenheadLocator>) -> Self {
398        self.grid_locator = grid_locator;
399        self
400    }
401
402    pub fn with_itu_region(mut self, itu_region: Option<Region>) -> Self {
403        self.itu_region = itu_region;
404        self
405    }
406
407    pub fn with_country(mut self, country: Option<CountryCode>) -> Self {
408        self.country = country;
409        self
410    }
411
412    pub fn with_mailing_address<S: Into<String>>(mut self, mailing_address: Option<S>) -> Self {
413        self.mailing_address = mailing_address.map(|s| s.into());
414        self
415    }
416
417    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
418    // Field Accessors
419    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
420
421    pub const fn kind(&self) -> LocationKind {
422        self.kind
423    }
424
425    pub fn set_kind(&mut self, kind: LocationKind) {
426        self.kind = kind;
427    }
428
429    pub const fn grid_locator(&self) -> Option<&MaidenheadLocator> {
430        self.grid_locator.as_ref()
431    }
432
433    pub fn set_grid_locator(&mut self, grid_locator: MaidenheadLocator) {
434        self.grid_locator = Some(grid_locator)
435    }
436
437    pub fn unset_grid_locator(&mut self) {
438        self.grid_locator = None;
439    }
440
441    pub const fn itu_region(&self) -> Option<&Region> {
442        self.itu_region.as_ref()
443    }
444
445    pub fn set_itu_region(&mut self, itu_region: Region) {
446        self.itu_region = Some(itu_region)
447    }
448
449    pub fn unset_itu_region(&mut self) {
450        self.itu_region = None;
451    }
452
453    pub const fn country(&self) -> Option<&CountryCode> {
454        self.country.as_ref()
455    }
456
457    pub fn set_country(&mut self, country: CountryCode) {
458        self.country = Some(country)
459    }
460
461    pub fn unset_country(&mut self) {
462        self.country = None
463    }
464
465    pub const fn mailing_address(&self) -> Option<&String> {
466        self.mailing_address.as_ref()
467    }
468
469    pub fn set_mailing_address<S: Into<String>>(&mut self, mailing_address: S) {
470        self.mailing_address = Some(mailing_address.into())
471    }
472
473    pub fn unset_mailing_address(&mut self) {
474        self.mailing_address = None
475    }
476}
477
478// ════════════════════════════════════════════════════════════════════════════════════════════════
479
480impl Dump for Equipment {
481    fn dump<W: Write>(&self, writer: &mut W, prefix: &str) -> ConfigResult<()> {
482        if let Some(label) = &self.label {
483            writeln!(writer, "{prefix}{label}:")?;
484            writeln!(writer, "{prefix}└── brand: {}", self.brand)?;
485            writeln!(writer, "{prefix}└── model: {}", self.model)?;
486        } else {
487            writeln!(writer, "{prefix}{} {}:", self.brand, self.model)?;
488        }
489        if let Some(max_power) = &self.max_power {
490            writeln!(writer, "{prefix}└── max power: {}", max_power)?;
491        }
492        if !self.bands.is_empty() {
493            writeln!(
494                writer,
495                "{prefix}└── operating bands: {}",
496                self.bands
497                    .iter()
498                    .map(|b| b.to_string())
499                    .collect::<Vec<_>>()
500                    .join(", ")
501            )?;
502        }
503        if !self.using.is_empty() {
504            writeln!(writer, "{prefix}└── Using:")?;
505            for equipment in &self.using {
506                equipment.dump(writer, &format!("{prefix}    "))?;
507            }
508        }
509        Ok(())
510    }
511}
512
513impl Equipment {
514    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
515    // Constructors
516    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
517
518    pub fn new<S: Into<String>>(brand: Name, model: Name) -> Self {
519        Self {
520            brand,
521            model,
522            label: None,
523            usage: Vec::default(),
524            modes: Vec::default(),
525            mobility: None,
526            max_power: None,
527            bands: Vec::default(),
528            using: Vec::default(),
529        }
530    }
531
532    pub fn with_label<S: Into<String>>(mut self, label: Option<S>) -> Self {
533        self.label = label.map(|s| s.into());
534        self
535    }
536
537    pub fn with_mobility(mut self, mobility: Option<Mobility>) -> Self {
538        self.mobility = mobility;
539        self
540    }
541
542    pub fn with_max_power(mut self, max_power: Option<Power>) -> Self {
543        self.max_power = max_power;
544        self
545    }
546
547    pub fn with_usage(mut self, usage: Vec<Usage>) -> Self {
548        self.usage = usage;
549        self
550    }
551
552    pub fn with_modes(mut self, modes: Vec<Mode>) -> Self {
553        self.modes = modes;
554        self
555    }
556
557    pub fn with_bands(mut self, bands: Vec<FrequencyBand>) -> Self {
558        self.bands = bands;
559        self
560    }
561
562    pub fn with_using(mut self, using: Vec<Equipment>) -> Self {
563        self.using = using;
564        self
565    }
566    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
567    // Field Accessors
568    // ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
569}
570
571// ════════════════════════════════════════════════════════════════════════════════════════════════
572
573impl Display for LocationKind {
574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575        write!(
576            f,
577            "{}",
578            if f.alternate() {
579                match self {
580                    Self::Home => "Home or Primary licensed station",
581                    Self::Alternate => "Alternate licensed station",
582                    Self::Remote => "Remote operating location",
583                    Self::Club => "Club location",
584                }
585            } else {
586                match self {
587                    Self::Home => "home",
588                    Self::Alternate => "alternate",
589                    Self::Remote => "remote",
590                    Self::Club => "club",
591                }
592            }
593        )
594    }
595}
596
597impl FromStr for LocationKind {
598    type Err = CoreError;
599
600    fn from_str(s: &str) -> Result<Self, Self::Err> {
601        match s {
602            "home" => Ok(Self::Home),
603            "alternate" => Ok(Self::Alternate),
604            "remote" => Ok(Self::Alternate),
605            "club" => Ok(Self::Club),
606            _ => Err(CoreError::InvalidValueFromStr(
607                s.to_string(),
608                "LocationKind",
609            )),
610        }
611    }
612}
613
614// ════════════════════════════════════════════════════════════════════════════════════════════════
615
616impl Display for Usage {
617    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618        write!(
619            f,
620            "{}",
621            if f.alternate() {
622                match self {
623                    Self::Local => "Local area, including repeaters",
624                    Self::Qrp => "QRP, low power, operation",
625                    Self::Dx => "DX, distance, operation",
626                    Self::EmComm => "Emergency Communications, including ARES/RACES",
627                    Self::Activation => "POTA, SOTA, IOTA, etc. activation",
628                    Self::Satellite => "Satellite opertions",
629                    Self::Scanning => "Scanning",
630                }
631            } else {
632                match self {
633                    Self::Local => "local",
634                    Self::Qrp => "qrp",
635                    Self::Dx => "dx",
636                    Self::EmComm => "emcomm",
637                    Self::Activation => "activation",
638                    Self::Satellite => "satellite",
639                    Self::Scanning => "scanning",
640                }
641            }
642        )
643    }
644}
645
646impl FromStr for Usage {
647    type Err = CoreError;
648
649    fn from_str(s: &str) -> Result<Self, Self::Err> {
650        match s {
651            "local" => Ok(Self::Local),
652            "qrp" => Ok(Self::Qrp),
653            "dx" => Ok(Self::Dx),
654            "emcomm" => Ok(Self::EmComm),
655            "activation" => Ok(Self::Activation),
656            "satellite" => Ok(Self::Satellite),
657            "scanning" => Ok(Self::Scanning),
658            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "Usage")),
659        }
660    }
661}
662
663// ════════════════════════════════════════════════════════════════════════════════════════════════
664
665impl Display for Mode {
666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667        write!(
668            f,
669            "{}",
670            if f.alternate() {
671                match self {
672                    Self::Am => "AM",
673                    Self::Fm => "FM",
674                    Self::Ssb => "SSB",
675                    Self::Rtty => "RTTY",
676                    Self::Digital => "Digital modes",
677                    Self::Image => "Images and SSTV",
678                }
679            } else {
680                match self {
681                    Self::Am => "am",
682                    Self::Fm => "fm",
683                    Self::Ssb => "ssb",
684                    Self::Rtty => "rtty",
685                    Self::Digital => "digital",
686                    Self::Image => "image",
687                }
688            }
689        )
690    }
691}
692
693impl FromStr for Mode {
694    type Err = CoreError;
695
696    fn from_str(s: &str) -> Result<Self, Self::Err> {
697        match s {
698            "am" => Ok(Self::Am),
699            "fm" => Ok(Self::Fm),
700            "ssb" => Ok(Self::Ssb),
701            "rtty" => Ok(Self::Rtty),
702            "digital" => Ok(Self::Digital),
703            "image" => Ok(Self::Image),
704            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "Mode")),
705        }
706    }
707}
708
709// ════════════════════════════════════════════════════════════════════════════════════════════════
710
711impl Display for Mobility {
712    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
713        write!(
714            f,
715            "{}",
716            if f.alternate() {
717                match self {
718                    Self::StationFixed => "Station only",
719                    Self::Portable => "Portable from station",
720                    Self::Mobile => "Mobile, vehicle/vessel mounted",
721                    Self::Handheld => "Handheld",
722                }
723            } else {
724                match self {
725                    Self::StationFixed => "station-fixed",
726                    Self::Portable => "portable",
727                    Self::Mobile => "mobile",
728                    Self::Handheld => "handheld",
729                }
730            }
731        )
732    }
733}
734
735impl FromStr for Mobility {
736    type Err = CoreError;
737
738    fn from_str(s: &str) -> Result<Self, Self::Err> {
739        match s {
740            "station-fixed" => Ok(Self::StationFixed),
741            "portable" => Ok(Self::Portable),
742            "mobile" => Ok(Self::Mobile),
743            "handheld" => Ok(Self::Handheld),
744            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "Mobility")),
745        }
746    }
747}
748
749// ────────────────────────────────────────────────────────────────────────────────────────────────
750// Sub-modules
751// ────────────────────────────────────────────────────────────────────────────────────────────────
752
753pub mod error;
754pub mod paths;