Skip to main content

rfham_antennas/
dipoles.rs

1//!
2//! One-line description.
3//!
4//! More detailed description.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use rfham_antennas::SimpleDipole;
10//! use rfham_bands::us_fcc::arrl_voluntary_band_plan;
11//! use rfham_itu::allocations::FrequencyAllocation::Band2M;
12//!
13//! let my_dipole = SimpleDipole::new_in_plan(
14//!     Band2M,
15//!     arrl_voluntary_band_plan()
16//! );
17//! assert_eq!(
18//!     Some("1.0266865 m".to_string()),
19//!     my_dipole.antenna_length().map(|v|v.to_string())
20//! );
21//! ```
22//!
23
24use colored::Colorize;
25use rfham_bands::BandPlan;
26use rfham_core::frequencies::{FrequencyRange, Wavelength, meters};
27use rfham_itu::allocations::FrequencyAllocation;
28use rfham_markdown::{
29    MarkdownError, ToMarkdown, blank_line, fenced_code_block_end, fenced_code_block_start, header,
30    italic_to_string, numbered_list_item, plain_text,
31};
32use std::fmt::Display;
33
34// ------------------------------------------------------------------------------------------------
35// Public Types
36// ------------------------------------------------------------------------------------------------
37
38/// Describe this struct.
39///
40/// # Fields
41///
42/// - `band` (`FrequencyAllocation`) - Describe this field.
43/// - `band_plan` (`Option<BandPlan>`) - Describe this field.
44///
45/// # Examples
46///
47/// ```
48/// use rfham_antennas::SimpleDipole;
49/// use rfham_bands::us_fcc::arrl_voluntary_band_plan;
50/// use rfham_itu::allocations::FrequencyAllocation::Band2M;
51/// use rfham_markdown::ToMarkdown;
52/// use std::io::stdout;
53///
54/// let my_dipole = SimpleDipole::new_in_plan(
55///     Band2M,
56///     arrl_voluntary_band_plan()
57/// );
58/// my_dipole.write_markdown(&mut stdout()).unwrap();
59/// ```
60///
61/// Results in the following output.
62///
63/// ```markdown
64/// # Classical half-wave dipole antenna for 2m band.
65///
66/// ~~~text
67/// |<──────────────────────── λ/2 = 1.027 meters ─────────────────────────>|
68/// |<─── λ/4 = 51.334 centimeters ───>| |<─── λ/4 = 51.334 centimeters ───>|
69/// ────────────────────────────────────┳────────────────────────────────────
70///                                     │  ∧
71///                                     │  │
72///                                     │  │ λ/2 = 1.027 meters
73///                                     │  │
74///                                     │  ∨
75/// ~~~
76///
77/// Notes:
78///
79/// 1. Frequency range for 2m band is 144.000 MHz - 148.000 MHz.
80///    1. From the *US Amateur Radio Bands* by The American Radio Relay League (ARRL).
81/// 2. Mid-point of band is 146.000 MHz.
82/// 3. Wavelength of mid-point is 2.053 m.
83/// 4. Half-wave length is λ/2 = 1.027 meters for overall antenna.
84/// 5. Quarter-wave length is λ/4 = 51.334 centimeters for each antenna pole.
85/// ```
86#[derive(Clone, Debug, PartialEq)]
87pub struct SimpleDipole {
88    band: FrequencyAllocation,
89    band_plan: Option<BandPlan>,
90}
91
92// ------------------------------------------------------------------------------------------------
93// Implementations
94// ------------------------------------------------------------------------------------------------
95
96impl Display for SimpleDipole {
97    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        todo!()
99    }
100}
101
102impl ToMarkdown for SimpleDipole {
103    fn write_markdown<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MarkdownError> {
104        if let Some(quarter_wavelength) = self.pole_length() {
105            const QUARTER_WAVE_PADDING: usize = "|<--- ".len() + " --->|".len();
106            const HALF_WAVE_PADDING: usize = "|< ".len() + " >|".len();
107
108            let wl_4 = format!("λ/4 = {quarter_wavelength:#.3}");
109            let wl_4_len: usize = wl_4.len() - 1;
110            let wl_4_padded_len = wl_4_len + QUARTER_WAVE_PADDING;
111            let wl_2 = meters(quarter_wavelength.value() * 2.0);
112            let wl_2 = format!("λ/2 = {wl_2:#.3}");
113            let wl_2_len = wl_2.len() - 1;
114            let width = wl_4_padded_len * 2 + 1;
115            let pad_width = (width - (wl_2_len + HALF_WAVE_PADDING)) / 2;
116            let pad_str = "─".repeat(pad_width);
117
118            header(
119                writer,
120                1,
121                format!("Classical half-wave dipole antenna for {} band.", self.band),
122            )?;
123            blank_line(writer)?;
124            fenced_code_block_start(writer)?;
125            let left_pad = format!("|<{pad_str}").blue().dimmed();
126            let right_pad = format!("{pad_str}{}>|", if wl_2_len % 2 == 1 { "" } else { "─" },)
127                .blue()
128                .dimmed();
129            writeln!(writer, "{} {} {}", left_pad, wl_2.bold(), right_pad)?;
130            let quarter_measure = format!(
131                "{} {} {}",
132                "|<───".blue().dimmed(),
133                wl_4.bold(),
134                "───>|".blue().dimmed(),
135            );
136            writeln!(writer, "{quarter_measure} {quarter_measure}",)?;
137            plain_text(
138                writer,
139                format!(
140                    "{}┳{}",
141                    "─".repeat(wl_4_padded_len),
142                    "─".repeat(wl_4_padded_len)
143                ),
144            )?;
145            writeln!(
146                writer,
147                "{}│  {}",
148                " ".repeat(wl_4_padded_len),
149                "∧".blue().dimmed()
150            )?;
151            writeln!(
152                writer,
153                "{}│  {}",
154                " ".repeat(wl_4_padded_len),
155                "│".blue().dimmed()
156            )?;
157            writeln!(
158                writer,
159                "{}│  {} {}",
160                " ".repeat(wl_4_padded_len),
161                "│".blue().dimmed(),
162                wl_2.bold()
163            )?;
164            writeln!(
165                writer,
166                "{}│  {}",
167                " ".repeat(wl_4_padded_len),
168                "│".blue().dimmed()
169            )?;
170            writeln!(
171                writer,
172                "{}│  {}",
173                " ".repeat(wl_4_padded_len),
174                "∨".blue().dimmed()
175            )?;
176            fenced_code_block_end(writer)?;
177            blank_line(writer)?;
178
179            plain_text(writer, "Notes:")?;
180            blank_line(writer)?;
181
182            // This is safe because it's required to calculate the side length.
183            let range = self.band_range().unwrap();
184            numbered_list_item(
185                writer,
186                1,
187                1,
188                format!("Frequency range for {} band is {:.3}.", self.band, range,),
189            )?;
190            numbered_list_item(
191                writer,
192                2,
193                1,
194                format!(
195                    "From the {}.",
196                    if let Some(band_plan) = &self.band_plan {
197                        format!(
198                            "{} by {}",
199                            italic_to_string(band_plan.name()),
200                            band_plan.maintaining_agency()
201                        )
202                    } else {
203                        "ITU frequency allocation".to_string()
204                    }
205                ),
206            )?;
207            numbered_list_item(
208                writer,
209                1,
210                2,
211                format!("Mid-point of band is {:.3}.", range.mid_band()),
212            )?;
213            numbered_list_item(
214                writer,
215                1,
216                3,
217                format!(
218                    "Wavelength of mid-point is {:.3}.",
219                    range.mid_band().to_wavelength()
220                ),
221            )?;
222            numbered_list_item(
223                writer,
224                1,
225                4,
226                format!("Half-wave length is {wl_2} for overall antenna."),
227            )?;
228            numbered_list_item(
229                writer,
230                1,
231                5,
232                format!("Quarter-wave length is {wl_4} for each antenna pole."),
233            )?;
234        } else {
235            println!(
236                "{}",
237                "Error: could not determine wavelength for antenna".red()
238            );
239        }
240        Ok(())
241    }
242}
243
244impl SimpleDipole {
245    pub fn new(band: FrequencyAllocation) -> Self {
246        Self {
247            band,
248            band_plan: None,
249        }
250    }
251
252    pub fn new_in_plan(band: FrequencyAllocation, band_plan: BandPlan) -> Self {
253        Self {
254            band,
255            band_plan: Some(band_plan),
256        }
257    }
258
259    fn band_range(&self) -> Option<FrequencyRange> {
260        if let Some(band_plan) = &self.band_plan {
261            band_plan
262                .band(&self.band)
263                .map(|band| band.band().range())
264                .cloned()
265        } else {
266            Some(self.band.total_range())
267        }
268    }
269
270    pub fn antenna_length(&self) -> Option<Wavelength> {
271        if let Some(range) = self.band_range() {
272            let mid_band = range.mid_band();
273            let wavelength = mid_band.to_wavelength();
274            Some(meters(wavelength.value() / 2.0))
275        } else {
276            None
277        }
278    }
279
280    pub fn pole_length(&self) -> Option<Wavelength> {
281        self.antenna_length().map(|v| meters(v.value() / 2.0))
282    }
283}
284
285// ------------------------------------------------------------------------------------------------
286// Unit Tests
287// ------------------------------------------------------------------------------------------------
288
289#[cfg(test)]
290mod tests {
291    use super::SimpleDipole;
292    use rfham_itu::allocations::FrequencyAllocation;
293
294    #[test]
295    fn test_antenna_length_2m() {
296        let dipole = SimpleDipole::new(FrequencyAllocation::Band2M);
297        let length = dipole.antenna_length().unwrap();
298        // 2m mid-band ≈ 146 MHz → λ/2 ≈ 1.027 m (c/f/2)
299        assert!(length.value() > 1.0 && length.value() < 1.1);
300    }
301
302    #[test]
303    fn test_pole_length_is_half_antenna() {
304        let dipole = SimpleDipole::new(FrequencyAllocation::Band2M);
305        let antenna = dipole.antenna_length().unwrap().value();
306        let pole = dipole.pole_length().unwrap().value();
307        assert!((antenna / 2.0 - pole).abs() < 1e-9);
308    }
309
310    #[test]
311    fn test_antenna_length_40m() {
312        let dipole = SimpleDipole::new(FrequencyAllocation::Band40M);
313        let length = dipole.antenna_length().unwrap();
314        // 40m mid-band ≈ 7.15 MHz → λ/2 ≈ 20.98 m
315        assert!(length.value() > 20.0 && length.value() < 22.0);
316    }
317}