1use 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#[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
152impl 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 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 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 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
279impl 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 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 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
355impl 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 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 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
478impl 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 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 }
570
571impl 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
614impl 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
663impl 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
709impl 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
749pub mod error;
754pub mod paths;