kenwood_thd75/radio/aprs.rs
1//! APRS (Automatic Packet Reporting System) subsystem methods.
2//!
3//! APRS is a digital communications protocol for real-time tactical information exchange. The
4//! TH-D75 has a built-in TNC (Terminal Node Controller) that handles AX.25 packet encoding and
5//! decoding, supporting both 1200 baud (VHF, standard APRS on 144.390 MHz in North America) and
6//! 9600 baud (UHF) operation.
7//!
8//! The TNC handles position beaconing, message exchange, and weather reporting. Beacon
9//! transmission is controlled by the beacon type setting (PT command), which determines whether
10//! beacons are sent manually, at fixed intervals, or based on `SmartBeaconing` rules.
11//!
12//! # Related commands
13//!
14//! - **AS**: TNC baud rate (1200/9600)
15//! - **PT**: Beacon TX control mode
16//! - **MS**: Position source / message send (overloaded mnemonic)
17//! - **AE**: Serial number info (not actually APRS-related, but shares the A prefix)
18//! - **BE**: Sends an APRS beacon (transmits on air — requires a valid
19//! amateur licence and appropriate authorisation; use deliberately)
20
21use crate::error::{Error, ProtocolError};
22use crate::protocol::{Command, Response};
23use crate::transport::Transport;
24use crate::types::{BeaconMode, TncBaud};
25
26use super::Radio;
27
28impl<T: Transport> Radio<T> {
29 /// Get the TNC baud rate (AS read).
30 ///
31 /// Returns 0 = 1200 baud, 1 = 9600 baud.
32 ///
33 /// # Errors
34 ///
35 /// Returns an error if the command fails or the response is unexpected.
36 pub async fn get_tnc_baud(&mut self) -> Result<TncBaud, Error> {
37 tracing::debug!("reading TNC baud rate");
38 let response = self.execute(Command::GetTncBaud).await?;
39 match response {
40 Response::TncBaud { rate } => Ok(rate),
41 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
42 expected: "TncBaud".into(),
43 actual: format!("{other:?}").into_bytes(),
44 })),
45 }
46 }
47
48 /// Get the beacon TX control mode (PT read).
49 ///
50 /// Returns the current beacon transmission mode:
51 ///
52 /// - `0` = Off (no automatic beaconing)
53 /// - `1` = Manual (beacon sent only when explicitly triggered)
54 /// - `2` = PTT (beacon sent after each PTT release)
55 /// - `3` = Auto (beacon sent at fixed intervals set by the beacon interval timer)
56 /// - `4` = `SmartBeaconing` (adaptive beaconing based on speed and direction changes)
57 ///
58 /// # Errors
59 ///
60 /// Returns an error if the command fails or the response is unexpected.
61 pub async fn get_beacon_type(&mut self) -> Result<BeaconMode, Error> {
62 tracing::debug!("reading beacon type");
63 let response = self.execute(Command::GetBeaconType).await?;
64 match response {
65 Response::BeaconType { mode } => Ok(mode),
66 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
67 expected: "BeaconType".into(),
68 actual: format!("{other:?}").into_bytes(),
69 })),
70 }
71 }
72
73 /// Get the APRS position source (MS read).
74 ///
75 /// # Errors
76 ///
77 /// Returns an error if the command fails or the response is unexpected.
78 pub async fn get_position_source(&mut self) -> Result<u8, Error> {
79 tracing::debug!("reading APRS position source");
80 let response = self.execute(Command::GetPositionSource).await?;
81 match response {
82 Response::PositionSource { source } => Ok(source),
83 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
84 expected: "PositionSource".into(),
85 actual: format!("{other:?}").into_bytes(),
86 })),
87 }
88 }
89
90 /// Set the TNC baud rate (AS write).
91 ///
92 /// Values: 0 = 1200 baud, 1 = 9600 baud.
93 ///
94 /// # Errors
95 ///
96 /// Returns an error if the command fails or the response is unexpected.
97 pub async fn set_tnc_baud(&mut self, rate: TncBaud) -> Result<(), Error> {
98 tracing::info!(?rate, "setting TNC baud rate");
99 let response = self.execute(Command::SetTncBaud { rate }).await?;
100 match response {
101 Response::TncBaud { .. } => Ok(()),
102 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
103 expected: "TncBaud".into(),
104 actual: format!("{other:?}").into_bytes(),
105 })),
106 }
107 }
108
109 /// Set the beacon TX control mode (PT write).
110 ///
111 /// See [`get_beacon_type`](Self::get_beacon_type) for valid mode values and their meanings.
112 ///
113 /// # Errors
114 ///
115 /// Returns an error if the command fails or the response is unexpected.
116 pub async fn set_beacon_type(&mut self, mode: BeaconMode) -> Result<(), Error> {
117 tracing::info!(?mode, "setting beacon type");
118 let response = self.execute(Command::SetBeaconType { mode }).await?;
119 match response {
120 Response::BeaconType { .. } => Ok(()),
121 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
122 expected: "BeaconType".into(),
123 actual: format!("{other:?}").into_bytes(),
124 })),
125 }
126 }
127
128 /// Send a message via the APRS/TNC interface (MS write).
129 ///
130 /// # RF emission warning
131 ///
132 /// **This command causes the radio to transmit on the air.** The TNC will key the
133 /// transmitter and send an AX.25 packet containing the message on the currently configured
134 /// APRS frequency. Ensure you are authorized to transmit on the current frequency before
135 /// calling this method.
136 ///
137 /// The transmission is a single packet burst (not continuous like [`transmit`](super::Radio::transmit)),
138 /// but it still constitutes an RF emission that must comply with radio regulations.
139 ///
140 /// # Errors
141 ///
142 /// Returns an error if the command fails or the response is unexpected.
143 pub async fn send_message(&mut self, text: &str) -> Result<(), Error> {
144 tracing::info!("sending APRS message");
145 let response = self
146 .execute(Command::SendMessage {
147 text: text.to_owned(),
148 })
149 .await?;
150 match response {
151 // MS write echoes back as an MS response, which the parser
152 // decodes as PositionSource (the MS read variant). Both use
153 // the same wire mnemonic.
154 Response::PositionSource { .. } => Ok(()),
155 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
156 expected: "MS (PositionSource echo from message send)".into(),
157 actual: format!("{other:?}").into_bytes(),
158 })),
159 }
160 }
161
162 /// Get the radio's serial number and model code (AE read).
163 ///
164 /// Despite the AE mnemonic, this returns serial info, not APRS data.
165 /// Returns `(serial, model_code)`.
166 ///
167 /// # Errors
168 ///
169 /// Returns an error if the command fails or the response is unexpected.
170 pub async fn get_serial_info(&mut self) -> Result<(String, String), Error> {
171 tracing::debug!("reading serial info");
172 let response = self.execute(Command::GetSerialInfo).await?;
173 match response {
174 Response::SerialInfo { serial, model_code } => Ok((serial, model_code)),
175 other => Err(Error::Protocol(ProtocolError::UnexpectedResponse {
176 expected: "SerialInfo".into(),
177 actual: format!("{other:?}").into_bytes(),
178 })),
179 }
180 }
181}