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}