kenwood_thd75/types/
aprs.rs

1//! APRS (Automatic Packet Reporting System) configuration types.
2//!
3//! APRS is a tactical real-time digital communications protocol used by ham
4//! radio operators for position reporting, messaging, and telemetry. The
5//! TH-D75 supports APRS on VHF with features including position beaconing,
6//! two-way messaging, `SmartBeaconing`, digipeater path configuration,
7//! packet filtering, and QSY information exchange.
8//!
9//! # QSY function (per Operating Tips §2.3.3-§2.3.5)
10//!
11//! APRS beacons can embed a voice frequency (QSY information) so that
12//! other stations can tune directly to a voice channel. In FM mode, the
13//! beacon includes the current Band A or B voice frequency. In D-STAR DR
14//! mode, the beacon also includes the repeater callsign; in DV mode, only
15//! the frequency is included. Per Operating Tips §2.3.4.
16//!
17//! QSY display distance can be restricted via Menu No. 523 (per Operating
18//! Tips §2.3.5), limiting which QSY beacons are shown based on the
19//! transmitting station's distance from the receiver.
20//!
21//! # Fixed-position beacon during GPS track logging (per Operating Tips §2.3.6)
22//!
23//! When GPS track logging is active, APRS beacons can be transmitted from
24//! a fixed position (set via Menu No. 401) instead of the live GPS position.
25//! This is useful when operating from a known location while still logging
26//! a GPS track.
27//!
28//! # Digipeated beacon registration (per Operating Tips §2.3.7)
29//!
30//! Beacons received via digipeaters are registered in the station list.
31//! The station list shows the digipeater path used.
32//!
33//! # `VoiceAlert` (per Operating Tips §5.3)
34//!
35//! `VoiceAlert` is a CTCSS-based mechanism: APRS beacons are transmitted
36//! with a CTCSS tone so that stations monitoring the APRS frequency with
37//! matching tone squelch hear an audible alert, enabling quick voice
38//! contact. Menu No. 910 controls the balance between `VoiceAlert` audio
39//! and normal APRS audio.
40//!
41//! These types model every APRS setting accessible through the TH-D75's
42//! menu system (Chapter 14 of the user manual) and MCP programming memory
43//! (pages 0x0151+ in the memory map).
44
45use crate::types::tone::ToneCode;
46
47// ---------------------------------------------------------------------------
48// Top-level APRS configuration
49// ---------------------------------------------------------------------------
50
51/// Complete APRS configuration for the TH-D75.
52///
53/// Covers all settings from the radio's APRS menu tree, including station
54/// identity, beaconing, messaging, filtering, digipeating, and notification
55/// options. Derived from the capability gap analysis features 63-94.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct AprsConfig {
58    /// APRS station callsign with optional SSID (up to 9 characters,
59    /// e.g. "N0CALL-9"). Stored in MCP memory at the APRS settings region.
60    pub my_callsign: AprsCallsign,
61    /// APRS map icon (symbol table + symbol code pair).
62    pub icon: AprsIcon,
63    /// Position comment (selected from 15 predefined phrases).
64    pub position_comment: PositionComment,
65    /// Status text slots (5 configurable messages, up to 62 characters each).
66    pub status_texts: [StatusText; 5],
67    /// Active status text slot index (0-4).
68    pub active_status_text: u8,
69    /// Digipeater packet path configuration.
70    pub packet_path: PacketPath,
71    /// APRS data speed (1200 or 9600 bps).
72    pub data_speed: AprsDataSpeed,
73    /// Band used for APRS data transmission.
74    pub data_band: AprsBand,
75    /// DCD (Data Carrier Detect) sense mode.
76    pub dcd_sense: DcdSense,
77    /// TX delay before packet transmission (in 10 ms units, range 1-50,
78    /// representing 10-500 ms).
79    pub tx_delay: TxDelay,
80    /// Beacon transmission control settings.
81    pub beacon_control: BeaconControl,
82    /// `SmartBeaconing` configuration (speed-adaptive beaconing).
83    pub smart_beaconing: McpSmartBeaconingConfig,
84    /// APRS lock (prevent accidental APRS setting changes).
85    pub aprs_lock: bool,
86    /// Position ambiguity level (0 = full precision, 1-4 = progressively
87    /// less precise, each level removes one decimal digit).
88    pub position_ambiguity: PositionAmbiguity,
89    /// Waypoint output configuration.
90    pub waypoint: WaypointConfig,
91    /// Packet filter settings.
92    pub packet_filter: PacketFilter,
93    /// Auto-reply message configuration.
94    pub auto_reply: AutoReplyConfig,
95    /// Notification sound configuration.
96    pub notification: NotificationConfig,
97    /// Digipeater configuration.
98    pub digipeat: DigipeatConfig,
99    /// QSY (frequency change) information configuration.
100    pub qsy: QsyConfig,
101    /// Enable APRS object functions (transmit/edit objects).
102    pub object_functions: bool,
103    /// Voice alert (transmit CTCSS tone with APRS packets to alert
104    /// nearby stations monitoring with tone squelch).
105    pub voice_alert: VoiceAlertConfig,
106    /// Message group code filter string (up to 9 characters).
107    pub message_group_code: GroupCode,
108    /// Bulletin group code filter string (up to 9 characters).
109    pub bulletin_group_code: GroupCode,
110    /// NAVITRA (navigation/tracking) settings.
111    pub navitra: NavitraConfig,
112    /// APRS network identifier.
113    pub network: AprsNetwork,
114    /// Display area setting for incoming APRS packets.
115    pub display_area: DisplayArea,
116    /// Interrupt time for incoming APRS data display (seconds).
117    pub interrupt_time: InterruptTime,
118    /// APRS voice announcement on receive.
119    pub aprs_voice: bool,
120}
121
122impl Default for AprsConfig {
123    fn default() -> Self {
124        Self {
125            my_callsign: AprsCallsign::default(),
126            icon: AprsIcon::default(),
127            position_comment: PositionComment::OffDuty,
128            status_texts: Default::default(),
129            active_status_text: 0,
130            packet_path: PacketPath::default(),
131            data_speed: AprsDataSpeed::Bps1200,
132            data_band: AprsBand::A,
133            dcd_sense: DcdSense::Both,
134            tx_delay: TxDelay::default(),
135            beacon_control: BeaconControl::default(),
136            smart_beaconing: McpSmartBeaconingConfig::default(),
137            aprs_lock: false,
138            position_ambiguity: PositionAmbiguity::Full,
139            waypoint: WaypointConfig::default(),
140            packet_filter: PacketFilter::default(),
141            auto_reply: AutoReplyConfig::default(),
142            notification: NotificationConfig::default(),
143            digipeat: DigipeatConfig::default(),
144            qsy: QsyConfig::default(),
145            object_functions: false,
146            voice_alert: VoiceAlertConfig::default(),
147            message_group_code: GroupCode::default(),
148            bulletin_group_code: GroupCode::default(),
149            navitra: NavitraConfig::default(),
150            network: AprsNetwork::default(),
151            display_area: DisplayArea::EntireDisplay,
152            interrupt_time: InterruptTime::Sec10,
153            aprs_voice: false,
154        }
155    }
156}
157
158// ---------------------------------------------------------------------------
159// Station identity
160// ---------------------------------------------------------------------------
161
162/// APRS callsign with optional SSID (up to 9 characters, e.g. "N0CALL-9").
163///
164/// The SSID suffix (0-15) conventionally indicates the station type:
165/// -0 fixed, -1 digi, -7 handheld, -9 mobile, -15 generic.
166#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
167pub struct AprsCallsign(String);
168
169impl AprsCallsign {
170    /// Maximum length of an APRS callsign with SSID.
171    pub const MAX_LEN: usize = 9;
172
173    /// Creates a new APRS callsign.
174    ///
175    /// # Errors
176    ///
177    /// Returns `None` if the callsign exceeds 9 characters.
178    #[must_use]
179    pub fn new(callsign: &str) -> Option<Self> {
180        if callsign.len() <= Self::MAX_LEN {
181            Some(Self(callsign.to_owned()))
182        } else {
183            None
184        }
185    }
186
187    /// Returns the callsign as a string slice.
188    #[must_use]
189    pub fn as_str(&self) -> &str {
190        &self.0
191    }
192}
193
194// ---------------------------------------------------------------------------
195// Icon / symbol
196// ---------------------------------------------------------------------------
197
198/// APRS map icon (symbol table + symbol code).
199///
200/// APRS uses a two-character encoding: the first character selects the
201/// symbol table (`/` for primary, `\` for alternate), and the second
202/// character selects the specific icon within that table.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
204pub enum AprsIcon {
205    /// House (primary table `/`).
206    House,
207    /// Car / automobile (primary table `/`).
208    Car,
209    /// Portable / HT (primary table `/`).
210    #[default]
211    Portable,
212    /// Jogger / runner (primary table `/`).
213    Jogger,
214    /// Bicycle (primary table `/`).
215    Bicycle,
216    /// Motorcycle (primary table `/`).
217    Motorcycle,
218    /// Yacht / sailboat (primary table `/`).
219    Yacht,
220    /// Ambulance (primary table `/`).
221    Ambulance,
222    /// Fire truck (primary table `/`).
223    FireTruck,
224    /// Helicopter (primary table `/`).
225    Helicopter,
226    /// Aircraft / small plane (primary table `/`).
227    Aircraft,
228    /// Weather station (primary table `/`).
229    WeatherStation,
230    /// Digipeater (primary table `/`).
231    Digipeater,
232    /// `IGate` (alternate table `\`).
233    IGate,
234    /// Truck (primary table `/`).
235    Truck,
236    /// Custom icon specified by raw table and code characters.
237    Custom {
238        /// Symbol table identifier (`/` = primary, `\` = alternate,
239        /// or overlay character `0`-`9`, `A`-`Z`).
240        table: char,
241        /// Symbol code character (ASCII 0x21-0x7E).
242        code: char,
243    },
244}
245
246// ---------------------------------------------------------------------------
247// Data speed / band / DCD
248// ---------------------------------------------------------------------------
249
250/// APRS data transmission speed.
251///
252/// Most APRS activity on VHF uses 1200 bps (AFSK on 144.390 MHz in North
253/// America). 9600 bps is used for high-speed data on UHF.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
255pub enum AprsDataSpeed {
256    /// 1200 bps (standard VHF APRS).
257    Bps1200,
258    /// 9600 bps (UHF high-speed data).
259    Bps9600,
260}
261
262/// Band used for APRS data transmission and reception.
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
264pub enum AprsBand {
265    /// Band A only.
266    A,
267    /// Band B only.
268    B,
269    /// Both bands A and B.
270    Both,
271}
272
273/// DCD (Data Carrier Detect) sense mode.
274///
275/// Controls how the radio detects channel activity before transmitting
276/// APRS packets.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278pub enum DcdSense {
279    /// Sense both voice and data activity on the channel.
280    Both,
281    /// Sense data activity only (ignore voice signals).
282    DataOnly,
283}
284
285// ---------------------------------------------------------------------------
286// TX delay
287// ---------------------------------------------------------------------------
288
289/// APRS TX delay before packet transmission.
290///
291/// Delay is specified in 10 ms increments. The valid range is 100-500 ms
292/// (values 1-50 in 10 ms units). Default is 300 ms.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
294pub struct TxDelay(u8);
295
296impl TxDelay {
297    /// Minimum TX delay value (10 ms units), representing 100 ms.
298    pub const MIN: u8 = 1;
299    /// Maximum TX delay value (10 ms units), representing 500 ms.
300    pub const MAX: u8 = 50;
301
302    /// Creates a new TX delay value.
303    ///
304    /// # Errors
305    ///
306    /// Returns `None` if the value is outside the range 1-50.
307    #[must_use]
308    pub const fn new(units_10ms: u8) -> Option<Self> {
309        if units_10ms >= Self::MIN && units_10ms <= Self::MAX {
310            Some(Self(units_10ms))
311        } else {
312            None
313        }
314    }
315
316    /// Returns the delay in 10 ms units.
317    #[must_use]
318    pub const fn as_units(self) -> u8 {
319        self.0
320    }
321
322    /// Returns the delay in milliseconds.
323    #[must_use]
324    pub const fn as_ms(self) -> u16 {
325        self.0 as u16 * 10
326    }
327}
328
329impl Default for TxDelay {
330    fn default() -> Self {
331        // Default TX delay: 300 ms = 30 units of 10 ms.
332        Self(30)
333    }
334}
335
336// ---------------------------------------------------------------------------
337// Beacon control
338// ---------------------------------------------------------------------------
339
340/// Beacon transmission control settings.
341///
342/// Controls how and when APRS position beacons are transmitted.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
344pub struct BeaconControl {
345    /// Beacon transmission method.
346    pub method: BeaconMethod,
347    /// Initial beacon interval in seconds (range 30-9999).
348    pub initial_interval: u16,
349    /// Enable beacon decay algorithm (doubles interval after each
350    /// transmission until reaching 30 minutes).
351    pub decay: bool,
352    /// Enable proportional pathing (vary digipeater path based on
353    /// elapsed time since last beacon).
354    pub proportional_pathing: bool,
355}
356
357impl Default for BeaconControl {
358    fn default() -> Self {
359        Self {
360            method: BeaconMethod::Manual,
361            initial_interval: 180,
362            decay: false,
363            proportional_pathing: false,
364        }
365    }
366}
367
368/// Beacon transmission method.
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
370pub enum BeaconMethod {
371    /// Manual beacon only (press button to transmit).
372    Manual,
373    /// Beacon on PTT release.
374    Ptt,
375    /// Automatic periodic beaconing at the configured interval.
376    Auto,
377    /// `SmartBeaconing` (speed and course-adaptive intervals).
378    SmartBeaconing,
379}
380
381// ---------------------------------------------------------------------------
382// SmartBeaconing
383// ---------------------------------------------------------------------------
384
385/// `SmartBeaconing` configuration.
386///
387/// `SmartBeaconing` adapts the beacon interval based on speed and course
388/// changes. At high speed, beacons are sent more frequently; at low speed,
389/// less frequently. Course changes trigger immediate beacons.
390///
391/// Settings correspond to the 7 parameters under the
392/// APRS > `SmartBeaconing` menu on the TH-D75.
393#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
394pub struct McpSmartBeaconingConfig {
395    /// Low speed threshold in mph (range 1-30). Below this speed,
396    /// beacons are sent at `slow_rate`.
397    pub low_speed: u8,
398    /// High speed threshold in mph (range 2-90). At or above this speed,
399    /// beacons are sent at `fast_rate`.
400    pub high_speed: u8,
401    /// Slow beacon rate in seconds (range 1-100 minutes, stored as seconds).
402    pub slow_rate: u16,
403    /// Fast beacon rate in seconds (range 10-180 seconds).
404    pub fast_rate: u8,
405    /// Minimum course change in degrees to trigger a beacon (range 5-90).
406    pub turn_angle: u8,
407    /// Turn slope factor (range 1-255). Higher values require more speed
408    /// before a turn triggers a beacon.
409    pub turn_slope: u8,
410    /// Minimum time between turn-triggered beacons in seconds (range 5-180).
411    pub turn_time: u8,
412}
413
414impl Default for McpSmartBeaconingConfig {
415    fn default() -> Self {
416        Self {
417            low_speed: 5,
418            high_speed: 60,
419            slow_rate: 1800,
420            fast_rate: 60,
421            turn_angle: 28,
422            turn_slope: 26,
423            turn_time: 30,
424        }
425    }
426}
427
428// ---------------------------------------------------------------------------
429// Position ambiguity
430// ---------------------------------------------------------------------------
431
432/// Position ambiguity level for APRS position reports.
433///
434/// Each level removes one digit of precision from the transmitted
435/// latitude/longitude, progressively obscuring the station's exact
436/// location.
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
438pub enum PositionAmbiguity {
439    /// Full precision (no ambiguity). Approximately 60 feet.
440    Full,
441    /// 1 digit removed. Approximately 1/10 mile.
442    Level1,
443    /// 2 digits removed. Approximately 1 mile.
444    Level2,
445    /// 3 digits removed. Approximately 10 miles.
446    Level3,
447    /// 4 digits removed. Approximately 60 miles.
448    Level4,
449}
450
451// ---------------------------------------------------------------------------
452// Packet path
453// ---------------------------------------------------------------------------
454
455/// Digipeater packet path for APRS transmissions.
456///
457/// The packet path determines which digipeaters relay the station's
458/// packets. Common paths include WIDE1-1,WIDE2-1 for typical VHF
459/// APRS operation.
460///
461/// # New-N Paradigm (per Operating Tips §2.6.1)
462///
463/// The TH-D75 defaults to the New-N Paradigm with WIDE1-1 On and
464/// Total Hops = 2 (i.e. WIDE1-1,WIDE2-1). When the user configures
465/// a total hop count greater than 2, the radio displays a warning
466/// because excessive hops congest the APRS network.
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
468pub enum PacketPath {
469    /// Off (no digipeater path).
470    Off,
471    /// WIDE1-1 (one hop via fill-in digipeaters).
472    Wide1_1,
473    /// WIDE1-1,WIDE2-1 (standard two-hop path).
474    #[default]
475    Wide1_1Wide2_1,
476    /// WIDE1-1,WIDE2-2 (three-hop path).
477    Wide1_1Wide2_2,
478    /// Path 1 (user-configurable, stored in MCP memory).
479    User1,
480    /// Path 2 (user-configurable, stored in MCP memory).
481    User2,
482    /// Path 3 (user-configurable, stored in MCP memory).
483    User3,
484}
485
486// ---------------------------------------------------------------------------
487// Position comment
488// ---------------------------------------------------------------------------
489
490/// Predefined APRS position comment phrases.
491///
492/// The TH-D75 provides 15 selectable position comment phrases that are
493/// transmitted as part of the APRS position report. These match the
494/// standard APRS position comment codes.
495#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
496pub enum PositionComment {
497    /// "Off Duty" -- station is not actively monitoring.
498    OffDuty,
499    /// "En Route" -- station is in transit.
500    EnRoute,
501    /// "In Service" -- station is actively operating.
502    InService,
503    /// "Returning" -- station is returning to base.
504    Returning,
505    /// "Committed" -- station is committed to a task.
506    Committed,
507    /// "Special" -- special event or activity.
508    Special,
509    /// "Priority" -- priority traffic.
510    Priority,
511    /// "Custom 0" -- user-defined comment slot 0.
512    Custom0,
513    /// "Custom 1" -- user-defined comment slot 1.
514    Custom1,
515    /// "Custom 2" -- user-defined comment slot 2.
516    Custom2,
517    /// "Custom 3" -- user-defined comment slot 3.
518    Custom3,
519    /// "Custom 4" -- user-defined comment slot 4.
520    Custom4,
521    /// "Custom 5" -- user-defined comment slot 5.
522    Custom5,
523    /// "Custom 6" -- user-defined comment slot 6.
524    Custom6,
525    /// "Emergency" -- distress / emergency.
526    Emergency,
527}
528
529// ---------------------------------------------------------------------------
530// Status text
531// ---------------------------------------------------------------------------
532
533/// APRS status text message (up to 62 characters).
534///
535/// The TH-D75 provides 5 status text slots. The active slot is
536/// transmitted as part of the APRS status report.
537#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
538pub struct StatusText(String);
539
540impl StatusText {
541    /// Maximum length of a status text message.
542    pub const MAX_LEN: usize = 62;
543
544    /// Creates a new status text.
545    ///
546    /// # Errors
547    ///
548    /// Returns `None` if the text exceeds 62 characters.
549    #[must_use]
550    pub fn new(text: &str) -> Option<Self> {
551        if text.len() <= Self::MAX_LEN {
552            Some(Self(text.to_owned()))
553        } else {
554            None
555        }
556    }
557
558    /// Returns the status text as a string slice.
559    #[must_use]
560    pub fn as_str(&self) -> &str {
561        &self.0
562    }
563}
564
565// ---------------------------------------------------------------------------
566// Waypoint configuration
567// ---------------------------------------------------------------------------
568
569/// Waypoint output configuration.
570///
571/// Controls how APRS waypoint data is formatted and output to external
572/// GPS devices or PC software.
573#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
574pub struct WaypointConfig {
575    /// Waypoint output format.
576    pub format: WaypointFormat,
577    /// Number of waypoints to output (range 1-99, or 0 for all).
578    pub length: u8,
579    /// Enable waypoint output to the serial port.
580    pub output: bool,
581}
582
583impl Default for WaypointConfig {
584    fn default() -> Self {
585        Self {
586            format: WaypointFormat::Kenwood,
587            length: 0,
588            output: false,
589        }
590    }
591}
592
593/// Waypoint output format.
594#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
595pub enum WaypointFormat {
596    /// Kenwood proprietary format.
597    Kenwood,
598    /// Magellan GPS format.
599    Magellan,
600    /// NMEA `$GPWPL` sentence format.
601    Nmea,
602}
603
604// ---------------------------------------------------------------------------
605// Packet filter
606// ---------------------------------------------------------------------------
607
608/// APRS packet filter configuration.
609///
610/// Controls which received APRS packets are displayed and processed.
611#[derive(Debug, Clone, PartialEq, Eq)]
612pub struct PacketFilter {
613    /// Enable position limit filter (only show stations within a
614    /// certain distance).
615    pub position_limit: bool,
616    /// Packet filter type selection.
617    pub filter_type: PacketFilterType,
618    /// User-defined filter phrases (up to 3 phrases, each up to 9 characters).
619    pub user_phrases: [FilterPhrase; 3],
620}
621
622impl Default for PacketFilter {
623    fn default() -> Self {
624        Self {
625            position_limit: false,
626            filter_type: PacketFilterType::All,
627            user_phrases: Default::default(),
628        }
629    }
630}
631
632/// Packet filter type.
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
634pub enum PacketFilterType {
635    /// Accept all packet types.
636    All,
637    /// Position packets only.
638    Position,
639    /// Weather packets only.
640    Weather,
641    /// Message packets only (directed to this station).
642    Message,
643    /// Other packet types.
644    Other,
645}
646
647/// User-defined APRS filter phrase (up to 9 characters).
648#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
649pub struct FilterPhrase(String);
650
651impl FilterPhrase {
652    /// Maximum length of a filter phrase.
653    pub const MAX_LEN: usize = 9;
654
655    /// Creates a new filter phrase.
656    ///
657    /// # Errors
658    ///
659    /// Returns `None` if the phrase exceeds 9 characters.
660    #[must_use]
661    pub fn new(phrase: &str) -> Option<Self> {
662        if phrase.len() <= Self::MAX_LEN {
663            Some(Self(phrase.to_owned()))
664        } else {
665            None
666        }
667    }
668
669    /// Returns the filter phrase as a string slice.
670    #[must_use]
671    pub fn as_str(&self) -> &str {
672        &self.0
673    }
674}
675
676// ---------------------------------------------------------------------------
677// Auto-reply
678// ---------------------------------------------------------------------------
679
680/// APRS auto-reply message configuration.
681///
682/// When enabled, the radio automatically replies to incoming APRS
683/// messages with a configured response.
684#[derive(Debug, Clone, PartialEq, Eq)]
685pub struct AutoReplyConfig {
686    /// Enable auto-reply.
687    pub enabled: bool,
688    /// Auto-reply type.
689    pub reply_type: AutoReplyType,
690    /// Reply-to callsign filter (reply only to this callsign, or empty
691    /// for any station).
692    pub reply_to: AprsCallsign,
693    /// Delay time before sending the reply (seconds).
694    pub delay_time: AutoReplyDelay,
695    /// Reply message text (up to 45 characters).
696    pub message: ReplyMessage,
697}
698
699impl Default for AutoReplyConfig {
700    fn default() -> Self {
701        Self {
702            enabled: false,
703            reply_type: AutoReplyType::Reply,
704            reply_to: AprsCallsign::default(),
705            delay_time: AutoReplyDelay::Sec30,
706            message: ReplyMessage::default(),
707        }
708    }
709}
710
711/// Auto-reply type.
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
713pub enum AutoReplyType {
714    /// Reply with the configured message.
715    Reply,
716    /// Reply with the current position.
717    Position,
718}
719
720/// Auto-reply delay time.
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
722pub enum AutoReplyDelay {
723    /// No delay.
724    None,
725    /// 10 second delay.
726    Sec10,
727    /// 30 second delay.
728    Sec30,
729    /// 60 second delay.
730    Sec60,
731}
732
733/// APRS reply message text (up to 45 characters).
734#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
735pub struct ReplyMessage(String);
736
737impl ReplyMessage {
738    /// Maximum length of a reply message.
739    pub const MAX_LEN: usize = 45;
740
741    /// Creates a new reply message.
742    ///
743    /// # Errors
744    ///
745    /// Returns `None` if the text exceeds 45 characters.
746    #[must_use]
747    pub fn new(text: &str) -> Option<Self> {
748        if text.len() <= Self::MAX_LEN {
749            Some(Self(text.to_owned()))
750        } else {
751            None
752        }
753    }
754
755    /// Returns the reply message as a string slice.
756    #[must_use]
757    pub fn as_str(&self) -> &str {
758        &self.0
759    }
760}
761
762// ---------------------------------------------------------------------------
763// Notification
764// ---------------------------------------------------------------------------
765
766/// APRS notification sound and display configuration.
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
768pub struct NotificationConfig {
769    /// Beep on receiving an APRS packet.
770    pub rx_beep: bool,
771    /// Beep on transmitting an APRS beacon.
772    pub tx_beep: bool,
773    /// Special beep for directed messages (addressed to this station).
774    pub special_call: bool,
775}
776
777impl Default for NotificationConfig {
778    fn default() -> Self {
779        Self {
780            rx_beep: true,
781            tx_beep: false,
782            special_call: true,
783        }
784    }
785}
786
787/// Display area setting for incoming APRS data.
788#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
789pub enum DisplayArea {
790    /// Show APRS data on the entire display.
791    EntireDisplay,
792    /// Show APRS data in the lower portion only.
793    LowerOnly,
794}
795
796/// Interrupt time for APRS data display (how long the display shows
797/// incoming APRS data before returning to normal).
798#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
799pub enum InterruptTime {
800    /// 3 second interrupt.
801    Sec3,
802    /// 5 second interrupt.
803    Sec5,
804    /// 10 second interrupt.
805    Sec10,
806    /// 30 second interrupt.
807    Sec30,
808    /// Continuous (hold until dismissed).
809    Continuous,
810}
811
812// ---------------------------------------------------------------------------
813// Digipeater
814// ---------------------------------------------------------------------------
815
816/// APRS digipeater (digital repeater) configuration.
817///
818/// The TH-D75 can function as a fill-in digipeater, relaying packets
819/// from other APRS stations.
820///
821/// # Menu numbers (per Operating Tips §2.5)
822///
823/// - Menu No. 580: `UIdigipeat` on/off
824/// - Menu No. 581: `UIflood` alias
825/// - Menu No. 582: `UIflood` substitution
826/// - Menu No. 583: `UItrace` alias
827/// - Menu No. 584-587: My Alias 1-4
828/// - Menu No. 588: `UIcheck`
829///
830/// `UIdigipeat` enables relaying of received UI (Unnumbered Information)
831/// frames. `UIflood` handles the "flood" style of digipeating where
832/// the hop count is decremented but the alias is not changed (unless
833/// substitution is on). `UItrace` handles "trace" style digipeating
834/// where the digipeater inserts its own callsign into the path.
835#[derive(Debug, Clone, PartialEq, Eq, Default)]
836pub struct DigipeatConfig {
837    /// Enable `UIdigipeat` (relay UI frames).
838    pub ui_digipeat: bool,
839    /// Enable `UIcheck` (display frames before relaying).
840    pub ui_check: bool,
841    /// `UIflood` alias (e.g. "WIDE1") for New-N paradigm digipeating.
842    pub ui_flood: FloodAlias,
843    /// `UIflood` substitution (replace alias with own callsign).
844    pub ui_flood_substitute: bool,
845    /// `UItrace` alias (e.g. "WIDE2") for traced digipeating.
846    pub ui_trace: TraceAlias,
847    /// Digipeater MY alias slots (up to 4 additional aliases).
848    pub my_alias: [DigipeatAlias; 4],
849}
850
851/// `UIflood` alias (up to 5 characters, e.g. "WIDE1").
852#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
853pub struct FloodAlias(String);
854
855impl FloodAlias {
856    /// Maximum length of a flood alias.
857    pub const MAX_LEN: usize = 5;
858
859    /// Creates a new flood alias.
860    ///
861    /// # Errors
862    ///
863    /// Returns `None` if the alias exceeds 5 characters.
864    #[must_use]
865    pub fn new(alias: &str) -> Option<Self> {
866        if alias.len() <= Self::MAX_LEN {
867            Some(Self(alias.to_owned()))
868        } else {
869            None
870        }
871    }
872
873    /// Returns the flood alias as a string slice.
874    #[must_use]
875    pub fn as_str(&self) -> &str {
876        &self.0
877    }
878}
879
880/// `UItrace` alias (up to 5 characters, e.g. "WIDE2").
881#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
882pub struct TraceAlias(String);
883
884impl TraceAlias {
885    /// Maximum length of a trace alias.
886    pub const MAX_LEN: usize = 5;
887
888    /// Creates a new trace alias.
889    ///
890    /// # Errors
891    ///
892    /// Returns `None` if the alias exceeds 5 characters.
893    #[must_use]
894    pub fn new(alias: &str) -> Option<Self> {
895        if alias.len() <= Self::MAX_LEN {
896            Some(Self(alias.to_owned()))
897        } else {
898            None
899        }
900    }
901
902    /// Returns the trace alias as a string slice.
903    #[must_use]
904    pub fn as_str(&self) -> &str {
905        &self.0
906    }
907}
908
909/// Digipeater MY alias (up to 5 characters).
910#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
911pub struct DigipeatAlias(String);
912
913impl DigipeatAlias {
914    /// Maximum length of a digipeater alias.
915    pub const MAX_LEN: usize = 5;
916
917    /// Creates a new digipeater alias.
918    ///
919    /// # Errors
920    ///
921    /// Returns `None` if the alias exceeds 5 characters.
922    #[must_use]
923    pub fn new(alias: &str) -> Option<Self> {
924        if alias.len() <= Self::MAX_LEN {
925            Some(Self(alias.to_owned()))
926        } else {
927            None
928        }
929    }
930
931    /// Returns the digipeater alias as a string slice.
932    #[must_use]
933    pub fn as_str(&self) -> &str {
934        &self.0
935    }
936}
937
938// ---------------------------------------------------------------------------
939// QSY information
940// ---------------------------------------------------------------------------
941
942/// QSY (frequency change) information configuration.
943///
944/// QSY information allows APRS stations to advertise an alternate
945/// voice frequency so other operators can contact them directly.
946///
947/// Per Operating Tips §2.3.3: the voice frequency from Band A or B is
948/// embedded in the APRS beacon. In D-STAR DR mode, the beacon also
949/// includes the repeater callsign (§2.3.4); in DV mode, only the
950/// frequency is included.
951///
952/// Menu No. 523 controls the QSY distance restriction (§2.3.5):
953/// only QSY beacons from stations within the configured distance
954/// are displayed.
955#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
956pub struct QsyConfig {
957    /// Include QSY information in APRS status text.
958    pub info_in_status: bool,
959    /// Include tone and narrow FM settings in QSY information.
960    pub tone_narrow: bool,
961    /// Include repeater shift and offset in QSY information.
962    pub shift_offset: bool,
963    /// Limit distance for QSY display (0 = no limit, 1-2500 km).
964    pub limit_distance: u16,
965}
966
967// ---------------------------------------------------------------------------
968// Voice alert
969// ---------------------------------------------------------------------------
970
971/// Voice alert configuration.
972///
973/// Voice alert transmits a CTCSS tone with APRS packets. Stations
974/// monitoring the APRS frequency with matching tone squelch will hear
975/// the alert, enabling a quick voice QSO.
976///
977/// Per Operating Tips §5.3: `VoiceAlert` is CTCSS-based. The radio
978/// transmits a CTCSS tone on the APRS frequency; stations with
979/// matching tone squelch hear an audible alert. Menu No. 910
980/// controls the volume balance between `VoiceAlert` audio and normal
981/// receive audio.
982#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
983pub struct VoiceAlertConfig {
984    /// Enable voice alert.
985    pub enabled: bool,
986    /// Voice alert CTCSS tone code (index into the CTCSS frequency table).
987    /// Default is tone code 12 (100.0 Hz).
988    pub tone_code: ToneCode,
989}
990
991impl Default for VoiceAlertConfig {
992    fn default() -> Self {
993        Self {
994            enabled: false,
995            // SAFETY: 12 is within valid range 0-49.
996            tone_code: ToneCode::new(12).expect("default tone code 12 is valid"),
997        }
998    }
999}
1000
1001// ---------------------------------------------------------------------------
1002// Group codes
1003// ---------------------------------------------------------------------------
1004
1005/// Message or bulletin group code (up to 9 characters).
1006///
1007/// Group codes filter incoming APRS messages and bulletins so only
1008/// messages addressed to matching group identifiers are displayed.
1009#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
1010pub struct GroupCode(String);
1011
1012impl GroupCode {
1013    /// Maximum length of a group code.
1014    pub const MAX_LEN: usize = 9;
1015
1016    /// Creates a new group code.
1017    ///
1018    /// # Errors
1019    ///
1020    /// Returns `None` if the code exceeds 9 characters.
1021    #[must_use]
1022    pub fn new(code: &str) -> Option<Self> {
1023        if code.len() <= Self::MAX_LEN {
1024            Some(Self(code.to_owned()))
1025        } else {
1026            None
1027        }
1028    }
1029
1030    /// Returns the group code as a string slice.
1031    #[must_use]
1032    pub fn as_str(&self) -> &str {
1033        &self.0
1034    }
1035}
1036
1037// ---------------------------------------------------------------------------
1038// NAVITRA
1039// ---------------------------------------------------------------------------
1040
1041/// NAVITRA (navigation/tracking) configuration.
1042///
1043/// NAVITRA is a Japanese APRS-like system for position tracking.
1044/// The TH-D75 supports NAVITRA alongside standard APRS.
1045#[derive(Debug, Clone, PartialEq, Eq)]
1046pub struct NavitraConfig {
1047    /// NAVITRA group mode.
1048    pub group_mode: NavitraGroupMode,
1049    /// NAVITRA group code (up to 9 characters).
1050    pub group_code: GroupCode,
1051    /// NAVITRA message text (up to 20 characters).
1052    pub message: NavitraMessage,
1053}
1054
1055impl Default for NavitraConfig {
1056    fn default() -> Self {
1057        Self {
1058            group_mode: NavitraGroupMode::Off,
1059            group_code: GroupCode::default(),
1060            message: NavitraMessage::default(),
1061        }
1062    }
1063}
1064
1065/// NAVITRA group filtering mode.
1066#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1067pub enum NavitraGroupMode {
1068    /// NAVITRA group filtering disabled.
1069    Off,
1070    /// Show only stations in the matching group.
1071    GroupOnly,
1072}
1073
1074/// NAVITRA message text (up to 20 characters).
1075#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
1076pub struct NavitraMessage(String);
1077
1078impl NavitraMessage {
1079    /// Maximum length of a NAVITRA message.
1080    pub const MAX_LEN: usize = 20;
1081
1082    /// Creates a new NAVITRA message.
1083    ///
1084    /// # Errors
1085    ///
1086    /// Returns `None` if the text exceeds 20 characters.
1087    #[must_use]
1088    pub fn new(text: &str) -> Option<Self> {
1089        if text.len() <= Self::MAX_LEN {
1090            Some(Self(text.to_owned()))
1091        } else {
1092            None
1093        }
1094    }
1095
1096    /// Returns the NAVITRA message as a string slice.
1097    #[must_use]
1098    pub fn as_str(&self) -> &str {
1099        &self.0
1100    }
1101}
1102
1103// ---------------------------------------------------------------------------
1104// Network
1105// ---------------------------------------------------------------------------
1106
1107/// APRS network identifier.
1108///
1109/// Selects the APRS-IS network for internet gateway connections.
1110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1111pub enum AprsNetwork {
1112    /// APRS standard network (e.g. 144.390 MHz in North America).
1113    #[default]
1114    Aprs,
1115    /// NAVITRA network (Japanese navigation/tracking system).
1116    Navitra,
1117}
1118
1119// ---------------------------------------------------------------------------
1120// APRS message (received/transmitted)
1121// ---------------------------------------------------------------------------
1122
1123/// An APRS message (for RX history or TX queue).
1124///
1125/// APRS messaging supports point-to-point text messages between stations,
1126/// with acknowledgment. The TH-D75 stores a history of received and
1127/// transmitted messages.
1128#[derive(Debug, Clone, PartialEq, Eq)]
1129pub struct AprsMessage {
1130    /// Source callsign (who sent the message).
1131    pub from: AprsCallsign,
1132    /// Destination callsign (who the message is addressed to).
1133    pub to: AprsCallsign,
1134    /// Message text (up to 67 characters per the APRS spec).
1135    pub text: String,
1136    /// Message number for acknowledgment (1-99999, or 0 if no ack).
1137    pub message_number: u32,
1138    /// Whether this message has been acknowledged.
1139    pub acknowledged: bool,
1140}
1141
1142// ---------------------------------------------------------------------------
1143// APRS station (received position report)
1144// ---------------------------------------------------------------------------
1145
1146/// A received APRS station report from the station list.
1147///
1148/// The TH-D75 maintains a list of recently heard APRS stations with
1149/// their position, status, and other information.
1150#[derive(Debug, Clone, PartialEq)]
1151pub struct AprsStation {
1152    /// Station callsign with SSID.
1153    pub callsign: AprsCallsign,
1154    /// Station latitude in decimal degrees (positive = North).
1155    pub latitude: f64,
1156    /// Station longitude in decimal degrees (positive = East).
1157    pub longitude: f64,
1158    /// Station altitude in meters (if available).
1159    pub altitude: Option<f64>,
1160    /// Station course in degrees (0-360, if moving).
1161    pub course: Option<f64>,
1162    /// Station speed in km/h (if moving).
1163    pub speed: Option<f64>,
1164    /// Station comment text.
1165    pub comment: String,
1166    /// Station APRS icon.
1167    pub icon: AprsIcon,
1168    /// Distance from own position in km (calculated by radio).
1169    pub distance: Option<f64>,
1170    /// Bearing from own position in degrees (calculated by radio).
1171    pub bearing: Option<f64>,
1172}
1173
1174// ---------------------------------------------------------------------------
1175// Tests
1176// ---------------------------------------------------------------------------
1177
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181
1182    #[test]
1183    fn aprs_callsign_valid() {
1184        let cs = AprsCallsign::new("N0CALL-9").unwrap();
1185        assert_eq!(cs.as_str(), "N0CALL-9");
1186    }
1187
1188    #[test]
1189    fn aprs_callsign_max_length() {
1190        let cs = AprsCallsign::new("N0CALL-15").unwrap();
1191        assert_eq!(cs.as_str(), "N0CALL-15");
1192    }
1193
1194    #[test]
1195    fn aprs_callsign_too_long() {
1196        assert!(AprsCallsign::new("N0CALL-150").is_none());
1197    }
1198
1199    #[test]
1200    fn status_text_valid() {
1201        let st = StatusText::new("Testing 1 2 3").unwrap();
1202        assert_eq!(st.as_str(), "Testing 1 2 3");
1203    }
1204
1205    #[test]
1206    fn status_text_max_length() {
1207        let text = "a".repeat(62);
1208        assert!(StatusText::new(&text).is_some());
1209    }
1210
1211    #[test]
1212    fn status_text_too_long() {
1213        let text = "a".repeat(63);
1214        assert!(StatusText::new(&text).is_none());
1215    }
1216
1217    #[test]
1218    fn tx_delay_valid_range() {
1219        assert!(TxDelay::new(1).is_some());
1220        assert!(TxDelay::new(30).is_some());
1221        assert!(TxDelay::new(50).is_some());
1222    }
1223
1224    #[test]
1225    fn tx_delay_invalid() {
1226        assert!(TxDelay::new(0).is_none());
1227        assert!(TxDelay::new(51).is_none());
1228    }
1229
1230    #[test]
1231    fn tx_delay_default_300ms() {
1232        let d = TxDelay::default();
1233        assert_eq!(d.as_ms(), 300);
1234        assert_eq!(d.as_units(), 30);
1235    }
1236
1237    #[test]
1238    fn smart_beaconing_defaults() {
1239        let sb = McpSmartBeaconingConfig::default();
1240        assert_eq!(sb.low_speed, 5);
1241        assert_eq!(sb.high_speed, 60);
1242        assert_eq!(sb.fast_rate, 60);
1243        assert_eq!(sb.slow_rate, 1800);
1244        assert_eq!(sb.turn_angle, 28);
1245    }
1246
1247    #[test]
1248    fn filter_phrase_valid() {
1249        let fp = FilterPhrase::new("N0CALL").unwrap();
1250        assert_eq!(fp.as_str(), "N0CALL");
1251    }
1252
1253    #[test]
1254    fn filter_phrase_too_long() {
1255        assert!(FilterPhrase::new("0123456789").is_none());
1256    }
1257
1258    #[test]
1259    fn reply_message_valid() {
1260        let rm = ReplyMessage::new("I am away").unwrap();
1261        assert_eq!(rm.as_str(), "I am away");
1262    }
1263
1264    #[test]
1265    fn reply_message_too_long() {
1266        let text = "a".repeat(46);
1267        assert!(ReplyMessage::new(&text).is_none());
1268    }
1269
1270    #[test]
1271    fn aprs_config_default_compiles() {
1272        let cfg = AprsConfig::default();
1273        assert_eq!(cfg.data_speed, AprsDataSpeed::Bps1200);
1274        assert!(!cfg.aprs_lock);
1275    }
1276
1277    #[test]
1278    fn group_code_valid() {
1279        let gc = GroupCode::new("ARES").unwrap();
1280        assert_eq!(gc.as_str(), "ARES");
1281    }
1282
1283    #[test]
1284    fn group_code_too_long() {
1285        assert!(GroupCode::new("0123456789").is_none());
1286    }
1287
1288    #[test]
1289    fn qsy_config_defaults() {
1290        let qsy = QsyConfig::default();
1291        assert!(!qsy.info_in_status);
1292        assert_eq!(qsy.limit_distance, 0);
1293    }
1294}