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}