kenwood_thd75/types/scan.rs
1//! Scan and band scope types.
2//!
3//! The TH-D75 supports multiple scan modes including VFO band scan,
4//! memory scan, program scan, MHz scan, group link scan, priority scan,
5//! call scan, and band scope (spectrum display).
6//!
7//! Scan resume behavior (how the radio continues scanning after stopping
8//! on a signal) is configured via the SR command. The scan range for
9//! program scan is configured via the SF command.
10//!
11//! Per User Manual Chapter 9:
12//!
13//! - Adjust squelch before scanning; too-low squelch causes immediate stops.
14//! - While using CTCSS or DCS, scan stops on any signal but immediately
15//! resumes if the signal lacks the matching tone/code.
16//! - Pressing and holding `[PTT]` temporarily stops scan on a non-TX band.
17//! - The 1 MHz decimal point blinks on the display during active scanning.
18//! - Resume methods have separate settings for analog (Menu No. 130) and
19//! digital DV/DR modes (Menu No. 131). Default: Time for analog, Seek
20//! for digital.
21//! - Time-operate restart time: Menu No. 132, 1-10 seconds, default 5.
22//! - Carrier-operate restart time: Menu No. 133, 1-10 seconds, default 2.
23//!
24//! See TH-D75 User Manual, Chapter 9: Scan.
25
26use super::Frequency;
27
28/// Scan resume method — controls how the radio resumes scanning after
29/// stopping on an active signal.
30///
31/// Configured via Menu No. 130 (analog) or Menu No. 131 (digital DV/DR)
32/// on the radio, and via the SR CAT command. Default: Time for analog,
33/// Seek for digital.
34///
35/// # Safety warning
36/// The SR command with value 0 has been observed to reboot the radio
37/// on some firmware versions.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum ScanResumeMethod {
40 /// Time-operated: resume scanning after a configurable delay even
41 /// if the signal is still present. Default hold time is 5 seconds
42 /// (Menu No. 132, range 1-10 seconds).
43 TimeOperated,
44 /// Carrier-operated: resume scanning when the received signal
45 /// drops below the squelch threshold and stays closed for the
46 /// configured restart time. Default restart time is 2 seconds
47 /// (Menu No. 133, range 1-10 seconds).
48 CarrierOperated,
49 /// Seek: stop on the first active signal and remain there.
50 /// The user must manually resume scanning.
51 Seek,
52}
53
54impl ScanResumeMethod {
55 /// Number of valid scan resume method values (0-2).
56 pub const COUNT: u8 = 3;
57
58 /// Convert from the SR command's numeric value.
59 ///
60 /// Returns `None` for unrecognized values.
61 #[must_use]
62 pub const fn from_raw(value: u8) -> Option<Self> {
63 match value {
64 0 => Some(Self::TimeOperated),
65 1 => Some(Self::CarrierOperated),
66 2 => Some(Self::Seek),
67 _ => None,
68 }
69 }
70
71 /// Convert to the SR command's numeric value.
72 #[must_use]
73 pub const fn to_raw(self) -> u8 {
74 match self {
75 Self::TimeOperated => 0,
76 Self::CarrierOperated => 1,
77 Self::Seek => 2,
78 }
79 }
80}
81
82impl TryFrom<u8> for ScanResumeMethod {
83 type Error = crate::error::ValidationError;
84
85 fn try_from(value: u8) -> Result<Self, Self::Error> {
86 Self::from_raw(value).ok_or(crate::error::ValidationError::SettingOutOfRange {
87 name: "scan resume method",
88 value,
89 detail: "must be 0-2",
90 })
91 }
92}
93
94/// Scan type — the different scanning modes available on the TH-D75.
95///
96/// The active scan type depends on the current radio mode (VFO vs Memory)
97/// and the key sequence used to start the scan.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum ScanType {
100 /// VFO band scan: scans the frequency range stored in Menu No. 100
101 /// (Programmable VFO) using the current step size. Started by
102 /// pressing and holding `[VFO]` in VFO mode.
103 BandScan,
104 /// Memory channel scan: scans through all stored memory channels that
105 /// are not locked out. Started by pressing and holding `[MR]`.
106 /// At least 2 non-locked-out memory channels must contain data.
107 /// Program scan memory and the priority channel are excluded.
108 /// If the recall method (Menu No. 202) is set to `Current Band`,
109 /// only channels in the same frequency band are scanned.
110 MemoryScan,
111 /// Program scan: scans between two user-defined frequency limits.
112 /// There are 50 program scan memories (L0/U0 through L49/U49).
113 /// Started by pressing and holding `[VFO]` when the VFO frequency
114 /// is within a registered program scan range.
115 ProgramScan,
116 /// MHz scan: scans within a 1 MHz range around the current frequency.
117 /// For example, if tuned to 145.400 MHz, scans 145.000-145.995 MHz.
118 /// Started by pressing and holding `[MHz]`.
119 MhzScan,
120 /// Group link scan: scans memory channels within linked memory groups.
121 /// Groups are linked via Menu No. 203. Up to 30 groups can be linked.
122 /// Started by pressing and holding `[MHz]` in memory mode.
123 /// If no groups are linked, memory scan is executed instead.
124 GroupLinkScan,
125 /// Priority scan: checks the frequency registered in the priority
126 /// channel `[Pri]` every 3 seconds. When that channel is busy, the
127 /// radio switches to it. Menu No. 134 enables/disables this.
128 /// The priority channel must be on Band B. Not available in
129 /// single-band-A mode or when FM radio mode is active.
130 PriorityScan,
131 /// Call scan: alternates between the current VFO frequency (or memory
132 /// channel) and the call channel. Started by pressing and holding
133 /// `[CALL]`. The selected memory channel is scanned even if locked out.
134 CallScan,
135}
136
137/// Visual scan range for the band scope display.
138///
139/// The band scope shows signal activity around the current center frequency.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub struct VisualScanRange {
142 /// Center frequency of the visual scan.
143 pub center: Frequency,
144 /// Display range around the center.
145 pub range: VisualRange,
146}
147
148/// Band scope display width.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum VisualRange {
151 /// +/- 1 MHz around center.
152 Narrow,
153 /// +/- 2.5 MHz around center.
154 Medium,
155 /// +/- 5 MHz around center.
156 Wide,
157}
158
159/// Program scan edges — lower and upper frequency limits for program scan.
160///
161/// These correspond to the Programmable VFO settings (Menu No. 100),
162/// which define the lower and upper tunable frequency boundaries.
163/// Program scan sweeps between these two limits.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub struct ProgramScanEdge {
166 /// Lower frequency limit.
167 pub start: Frequency,
168 /// Upper frequency limit.
169 pub end: Frequency,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn scan_resume_roundtrip() {
178 for raw in 0..=2 {
179 let method = ScanResumeMethod::from_raw(raw).unwrap();
180 assert_eq!(method.to_raw(), raw);
181 }
182 }
183
184 #[test]
185 fn scan_resume_invalid() {
186 assert!(ScanResumeMethod::from_raw(3).is_none());
187 assert!(ScanResumeMethod::from_raw(255).is_none());
188 }
189
190 #[test]
191 fn scan_resume_values() {
192 assert_eq!(ScanResumeMethod::TimeOperated.to_raw(), 0);
193 assert_eq!(ScanResumeMethod::CarrierOperated.to_raw(), 1);
194 assert_eq!(ScanResumeMethod::Seek.to_raw(), 2);
195 }
196
197 #[test]
198 fn visual_scan_range_construction() {
199 let range = VisualScanRange {
200 center: Frequency::new(145_000_000),
201 range: VisualRange::Medium,
202 };
203 assert_eq!(range.center.as_hz(), 145_000_000);
204 assert_eq!(range.range, VisualRange::Medium);
205 }
206
207 #[test]
208 fn program_scan_edge_construction() {
209 let edge = ProgramScanEdge {
210 start: Frequency::new(144_000_000),
211 end: Frequency::new(148_000_000),
212 };
213 assert_eq!(edge.start.as_hz(), 144_000_000);
214 assert_eq!(edge.end.as_hz(), 148_000_000);
215 }
216
217 #[test]
218 fn scan_type_debug() {
219 // Ensure all variants are representable and Debug works
220 let types = [
221 ScanType::BandScan,
222 ScanType::MemoryScan,
223 ScanType::ProgramScan,
224 ScanType::MhzScan,
225 ScanType::GroupLinkScan,
226 ScanType::PriorityScan,
227 ScanType::CallScan,
228 ];
229 for t in types {
230 let _ = format!("{t:?}");
231 }
232 }
233}