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}