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}