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}