thd75_tui/event.rs
1use std::time::Duration;
2
3use crossterm::event::{self, Event, KeyEventKind};
4use tokio::sync::mpsc;
5
6use crate::app::Message;
7
8/// Tick rate for the UI refresh loop.
9const TICK_RATE: Duration = Duration::from_millis(16); // ~60fps
10
11/// Commands sent from the app to the radio task.
12#[derive(Debug)]
13pub(crate) enum RadioCommand {
14 /// Trigger a full MCP memory read from the radio.
15 ReadMemory,
16 /// Trigger a full MCP memory write to the radio.
17 WriteMemory(Vec<u8>),
18 /// Tune the given band to a memory channel number.
19 TuneChannel {
20 /// The band to tune.
21 band: kenwood_thd75::types::Band,
22 /// The memory channel number (0–1199).
23 channel: u16,
24 },
25 /// Step frequency up by one increment on the given band.
26 FreqUp(kenwood_thd75::types::Band),
27 /// Step frequency down by one increment on the given band.
28 FreqDown(kenwood_thd75::types::Band),
29 /// Tune to a specific frequency on the given band.
30 TuneFreq {
31 /// The band to tune.
32 band: kenwood_thd75::types::Band,
33 /// The frequency in Hz.
34 freq: u32,
35 },
36 /// Set the squelch level for the given band (SQ write — verified working).
37 SetSquelch {
38 band: kenwood_thd75::types::Band,
39 level: kenwood_thd75::types::SquelchLevel,
40 },
41 /// Toggle the attenuator for the given band (RA write — verified working).
42 SetAttenuator {
43 band: kenwood_thd75::types::Band,
44 enabled: bool,
45 },
46 /// Set the operating mode for the given band (MD write — may return N in some modes).
47 SetMode {
48 band: kenwood_thd75::types::Band,
49 mode: kenwood_thd75::types::Mode,
50 },
51 /// Toggle lock on/off (LC write — verified working, value inverted on D75).
52 SetLock(bool),
53 /// Toggle dual band on/off (DL write — verified working, value inverted on D75).
54 SetDualBand(bool),
55 /// Toggle bluetooth on/off (BT write — verified working).
56 SetBluetooth(bool),
57 /// Toggle VOX on/off (VX write — verified working).
58 SetVox(bool),
59 /// Set VOX gain (VG write — verified working).
60 SetVoxGain(kenwood_thd75::types::VoxGain),
61 /// Set VOX delay (VD write — verified working).
62 SetVoxDelay(kenwood_thd75::types::VoxDelay),
63 /// Set TNC baud rate (AS write — verified working).
64 SetTncBaud(kenwood_thd75::types::TncBaud),
65 /// Set beacon type (PT write — verified working).
66 SetBeaconType(kenwood_thd75::types::BeaconMode),
67 /// Set GPS config (GP write — verified working).
68 SetGpsConfig(bool, bool),
69 /// Set FM radio on/off (FR write — verified working).
70 SetFmRadio(bool),
71 /// Set D-STAR callsign slot (CS write — verified working).
72 /// Not yet wired to `adjust_setting` (requires polling current slot first).
73 #[allow(dead_code)]
74 SetCallsignSlot(kenwood_thd75::types::CallsignSlot),
75 /// Set D-STAR slot (DS write — verified working).
76 /// Not yet wired to `adjust_setting` (requires polling current slot first).
77 #[allow(dead_code)]
78 SetDstarSlot(kenwood_thd75::types::DstarSlot),
79 /// Set the step size for the given band (SF write — verified working).
80 SetStepSize {
81 band: kenwood_thd75::types::Band,
82 step: kenwood_thd75::types::StepSize,
83 },
84 /// Set the scan resume method (SR write — write-only on D75).
85 SetScanResumeCat(kenwood_thd75::types::ScanResumeMethod),
86 /// Write a single byte to MCP memory via `modify_memory_page`.
87 /// Enters MCP mode, modifies one byte, exits. USB drops and reconnects.
88 /// Used for settings where CAT writes are rejected by D75 firmware.
89 McpWriteByte { offset: u16, value: u8 },
90 /// Set the transmit power level for the given band.
91 SetPower {
92 /// The band to adjust.
93 band: kenwood_thd75::types::Band,
94 /// The desired power level.
95 level: kenwood_thd75::types::PowerLevel,
96 },
97 /// Set D-STAR URCALL callsign via CAT (works in normal CAT mode).
98 SetUrcall {
99 /// Callsign (up to 8 chars).
100 callsign: String,
101 /// Suffix (up to 4 chars).
102 suffix: String,
103 },
104 /// Connect to a D-STAR reflector via CAT (sets URCALL to link command).
105 ConnectReflector {
106 /// Reflector callsign (e.g. "REF030").
107 name: String,
108 /// Reflector module letter (e.g. 'C').
109 module: char,
110 },
111 /// Disconnect from the current D-STAR reflector via CAT.
112 DisconnectReflector,
113 /// Set URCALL to CQCQCQ via CAT.
114 SetCQ,
115 /// Enter D-STAR gateway mode (MMDVM/DStarGateway).
116 EnterDStar {
117 /// D-STAR gateway configuration.
118 config: kenwood_thd75::DStarGatewayConfig,
119 },
120 /// Exit D-STAR gateway mode.
121 ExitDStar,
122 /// Enter APRS/KISS mode. The radio task enters KISS mode and starts
123 /// processing APRS packets instead of CAT polling.
124 EnterAprs {
125 /// APRS client configuration (callsign, SSID, etc.).
126 ///
127 /// Boxed because `AprsClientConfig` contains vector and option
128 /// fields that make it significantly larger than other variants.
129 config: Box<kenwood_thd75::AprsClientConfig>,
130 },
131 /// Exit APRS/KISS mode. Returns to CAT polling.
132 ExitAprs,
133 /// Send an APRS message to a station while in APRS mode.
134 SendAprsMessage {
135 /// Destination callsign.
136 addressee: String,
137 /// Message text.
138 text: String,
139 },
140 /// Transmit a manual position beacon while in APRS mode.
141 BeaconPosition {
142 /// Latitude in decimal degrees.
143 lat: f64,
144 /// Longitude in decimal degrees.
145 lon: f64,
146 /// Beacon comment text.
147 comment: String,
148 },
149}
150
151/// Merges terminal key events with messages from background tasks.
152pub(crate) struct EventHandler {
153 rx: mpsc::UnboundedReceiver<Message>,
154 tx: mpsc::UnboundedSender<Message>,
155 cmd_tx: mpsc::UnboundedSender<RadioCommand>,
156 cmd_rx: Option<mpsc::UnboundedReceiver<RadioCommand>>,
157}
158
159impl EventHandler {
160 /// Create a new event handler with internal message and command channels.
161 pub(crate) fn new() -> Self {
162 let (tx, rx) = mpsc::unbounded_channel();
163 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
164
165 // Spawn a dedicated thread for blocking crossterm event polling.
166 // This avoids blocking a tokio worker thread.
167 let input_tx = tx.clone();
168 let _handle = std::thread::spawn(move || {
169 loop {
170 if event::poll(TICK_RATE).expect("event poll failed")
171 && let Event::Key(key) = event::read().expect("event read failed")
172 && key.kind == KeyEventKind::Press
173 && input_tx.send(Message::Key(key)).is_err()
174 {
175 return;
176 }
177 }
178 });
179
180 Self {
181 rx,
182 tx,
183 cmd_tx,
184 cmd_rx: Some(cmd_rx),
185 }
186 }
187
188 /// Returns a sender that background tasks can use to push messages.
189 pub(crate) fn sender(&self) -> mpsc::UnboundedSender<Message> {
190 self.tx.clone()
191 }
192
193 /// Returns a sender the app can use to send commands to the radio task.
194 pub(crate) fn command_sender(&self) -> mpsc::UnboundedSender<RadioCommand> {
195 self.cmd_tx.clone()
196 }
197
198 /// Takes the command receiver (can only be called once).
199 ///
200 /// # Panics
201 ///
202 /// Panics if the command receiver has already been taken.
203 pub(crate) const fn take_command_receiver(&mut self) -> mpsc::UnboundedReceiver<RadioCommand> {
204 self.cmd_rx.take().expect("command receiver already taken")
205 }
206
207 /// Wait for the next message from any source (terminal input or background tasks).
208 pub(crate) async fn next(&mut self) -> Message {
209 self.rx.recv().await.unwrap_or(Message::Quit)
210 }
211}