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