kenwood_thd75/types/
cw.rs

1//! CW (Continuous Wave / Morse Code) configuration types.
2//!
3//! The TH-D75 supports CW mode on SSB-capable bands with configurable
4//! break-in timing, sidetone pitch frequency, and CW-on-FM operation.
5//! Break-in allows the receiver to activate between transmitted elements;
6//! full break-in (QSK) provides instantaneous receive between every
7//! dit and dah.
8//!
9//! # CW reverse (per Operating Tips §5.10.2)
10//!
11//! Menu No. 171 controls CW sideband selection:
12//! - Normal: USB (Upper Side Band)
13//! - Reverse: LSB (Lower Side Band)
14//!
15//! These types model CW settings from the TH-D75's menu system.
16//! Derived from the capability gap analysis features 133-136.
17
18// ---------------------------------------------------------------------------
19// CW configuration
20// ---------------------------------------------------------------------------
21
22/// CW (Morse code) operating configuration.
23///
24/// Controls break-in timing, sidetone pitch, and the CW-on-FM feature
25/// that allows sending CW tones over an FM carrier.
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub struct CwConfig {
28    /// Enable break-in (receive between transmitted CW elements).
29    pub break_in: bool,
30    /// Break-in delay time (time to hold TX after last element).
31    pub delay_time: CwDelay,
32    /// CW sidetone pitch frequency.
33    pub pitch_frequency: CwPitch,
34    /// Enable CW tone generation on FM mode.
35    pub cw_on_fm: bool,
36}
37
38impl Default for CwConfig {
39    fn default() -> Self {
40        Self {
41            break_in: false,
42            delay_time: CwDelay::Ms300,
43            pitch_frequency: CwPitch::default(),
44            cw_on_fm: false,
45        }
46    }
47}
48
49// ---------------------------------------------------------------------------
50// CW delay time
51// ---------------------------------------------------------------------------
52
53/// CW break-in delay time.
54///
55/// Controls how long the transmitter stays keyed after the last CW
56/// element before switching back to receive. `Full` provides QSK
57/// (full break-in) with instantaneous TX/RX switching.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum CwDelay {
60    /// Full break-in (QSK) -- instantaneous TX/RX switching.
61    Full,
62    /// 50 ms delay.
63    Ms50,
64    /// 100 ms delay.
65    Ms100,
66    /// 150 ms delay.
67    Ms150,
68    /// 200 ms delay.
69    Ms200,
70    /// 250 ms delay.
71    Ms250,
72    /// 300 ms delay.
73    Ms300,
74}
75
76// ---------------------------------------------------------------------------
77// CW pitch frequency
78// ---------------------------------------------------------------------------
79
80/// CW sidetone pitch frequency (400-1000 Hz in 100 Hz steps).
81///
82/// The sidetone is the locally generated audio tone heard while
83/// transmitting CW. The pitch can be adjusted to the operator's
84/// preference: 400 / 500 / 600 / 700 / 800 / 900 / 1000 Hz.
85/// Default: 800 Hz.
86///
87/// Per User Manual Chapter 12: this also sets the center frequency
88/// of the CW bandwidth filter (Menu No. 121). The CW filter is
89/// centered on the pitch frequency.
90///
91/// Source: Operating Tips §5.10.2, Menu No. 170.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub struct CwPitch(u16);
94
95impl CwPitch {
96    /// Minimum pitch frequency in Hz.
97    pub const MIN_HZ: u16 = 400;
98
99    /// Maximum pitch frequency in Hz.
100    pub const MAX_HZ: u16 = 1000;
101
102    /// Step size in Hz (100 Hz per Operating Tips §5.10.2).
103    pub const STEP_HZ: u16 = 100;
104
105    /// Creates a new CW pitch frequency.
106    ///
107    /// # Errors
108    ///
109    /// Returns `None` if the frequency is outside the 400-1000 Hz range
110    /// or is not a multiple of 100 Hz.
111    #[must_use]
112    pub const fn new(hz: u16) -> Option<Self> {
113        if hz >= Self::MIN_HZ && hz <= Self::MAX_HZ && hz.is_multiple_of(Self::STEP_HZ) {
114            Some(Self(hz))
115        } else {
116            None
117        }
118    }
119
120    /// Returns the pitch frequency in Hz.
121    #[must_use]
122    pub const fn hz(self) -> u16 {
123        self.0
124    }
125}
126
127impl Default for CwPitch {
128    fn default() -> Self {
129        // 800 Hz is a typical default CW pitch.
130        Self(800)
131    }
132}
133
134// ---------------------------------------------------------------------------
135// Tests
136// ---------------------------------------------------------------------------
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn cw_config_default() {
144        let cfg = CwConfig::default();
145        assert!(!cfg.break_in);
146        assert_eq!(cfg.delay_time, CwDelay::Ms300);
147        assert_eq!(cfg.pitch_frequency.hz(), 800);
148        assert!(!cfg.cw_on_fm);
149    }
150
151    #[test]
152    fn cw_pitch_valid_range() {
153        let mut count = 0;
154        let mut hz = CwPitch::MIN_HZ;
155        while hz <= CwPitch::MAX_HZ {
156            assert!(CwPitch::new(hz).is_some(), "valid pitch {hz} rejected");
157            count += 1;
158            hz += CwPitch::STEP_HZ;
159        }
160        // 400, 500, 600, 700, 800, 900, 1000 = 7 valid values.
161        assert_eq!(count, 7);
162    }
163
164    #[test]
165    fn cw_pitch_invalid_below_min() {
166        assert!(CwPitch::new(350).is_none());
167    }
168
169    #[test]
170    fn cw_pitch_invalid_above_max() {
171        assert!(CwPitch::new(1050).is_none());
172    }
173
174    #[test]
175    fn cw_pitch_invalid_not_step() {
176        assert!(CwPitch::new(425).is_none());
177        assert!(CwPitch::new(801).is_none());
178    }
179
180    #[test]
181    fn cw_pitch_boundary_values() {
182        assert!(CwPitch::new(400).is_some());
183        assert!(CwPitch::new(1000).is_some());
184    }
185
186    #[test]
187    fn cw_pitch_default() {
188        let pitch = CwPitch::default();
189        assert_eq!(pitch.hz(), 800);
190    }
191}