Added RSSI Indicator
This commit is contained in:
+21
@@ -1,7 +1,9 @@
|
|||||||
//! Application state management.
|
//! Application state management.
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::mpsc::{self, Receiver, Sender};
|
use std::sync::mpsc::{self, Receiver, Sender};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::capture::{ButtonCommand, Capture};
|
use crate::capture::{ButtonCommand, Capture};
|
||||||
use crate::protocols::ProtocolRegistry;
|
use crate::protocols::ProtocolRegistry;
|
||||||
@@ -85,6 +87,14 @@ impl RadioDevice {
|
|||||||
RadioDevice::RtlSdr(_) => "RTL-SDR (RX only)",
|
RadioDevice::RtlSdr(_) => "RTL-SDR (RX only)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared atomic for RSSI (UI reads so RX never blocks on channel). None if no radio.
|
||||||
|
pub fn rssi_source(&self) -> Option<Arc<AtomicU32>> {
|
||||||
|
match self {
|
||||||
|
RadioDevice::HackRf(h) => Some(h.rssi_source()),
|
||||||
|
RadioDevice::RtlSdr(r) => Some(r.rssi_source()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Input mode for the application
|
/// Input mode for the application
|
||||||
@@ -279,6 +289,8 @@ pub struct App {
|
|||||||
pub last_error: Option<String>,
|
pub last_error: Option<String>,
|
||||||
/// Last status message
|
/// Last status message
|
||||||
pub status_message: Option<String>,
|
pub status_message: Option<String>,
|
||||||
|
/// Latest RSSI (average magnitude, 0..~1) from receiver
|
||||||
|
pub rssi: f32,
|
||||||
|
|
||||||
// -- Signal action menu state --
|
// -- Signal action menu state --
|
||||||
/// Currently selected signal menu item index
|
/// Currently selected signal menu item index
|
||||||
@@ -307,6 +319,8 @@ pub struct App {
|
|||||||
/// Sender for radio events (cloned to radio thread)
|
/// Sender for radio events (cloned to radio thread)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
radio_event_tx: Sender<RadioEvent>,
|
radio_event_tx: Sender<RadioEvent>,
|
||||||
|
/// RSSI read from radio atomic (no channel traffic, avoids RX blocking)
|
||||||
|
pub rssi_source: Option<Arc<AtomicU32>>,
|
||||||
/// Set by :q / :quit so the main loop can exit cleanly (terminal cleanup)
|
/// Set by :q / :quit so the main loop can exit cleanly (terminal cleanup)
|
||||||
pub quit_requested: bool,
|
pub quit_requested: bool,
|
||||||
|
|
||||||
@@ -400,6 +414,8 @@ impl App {
|
|||||||
InputMode::Normal
|
InputMode::Normal
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rssi_source = radio.as_ref().and_then(|r| r.rssi_source());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
input_mode: initial_mode,
|
input_mode: initial_mode,
|
||||||
command_input: String::new(),
|
command_input: String::new(),
|
||||||
@@ -413,6 +429,7 @@ impl App {
|
|||||||
radio_state,
|
radio_state,
|
||||||
last_error: None,
|
last_error: None,
|
||||||
status_message: None,
|
status_message: None,
|
||||||
|
rssi: 0.0,
|
||||||
signal_menu_index: 0,
|
signal_menu_index: 0,
|
||||||
overlay_scroll: 0,
|
overlay_scroll: 0,
|
||||||
settings_field_index: 0,
|
settings_field_index: 0,
|
||||||
@@ -423,6 +440,7 @@ impl App {
|
|||||||
radio,
|
radio,
|
||||||
radio_event_rx,
|
radio_event_rx,
|
||||||
radio_event_tx,
|
radio_event_tx,
|
||||||
|
rssi_source,
|
||||||
quit_requested: false,
|
quit_requested: false,
|
||||||
pending_fob_files,
|
pending_fob_files,
|
||||||
export_capture_id: None,
|
export_capture_id: None,
|
||||||
@@ -899,6 +917,9 @@ impl App {
|
|||||||
|
|
||||||
/// Process pending radio events
|
/// Process pending radio events
|
||||||
pub fn process_radio_events(&mut self) -> Result<()> {
|
pub fn process_radio_events(&mut self) -> Result<()> {
|
||||||
|
if let Some(ref rssi_arc) = self.rssi_source {
|
||||||
|
self.rssi = f32::from_bits(rssi_arc.load(Ordering::Relaxed));
|
||||||
|
}
|
||||||
while let Ok(event) = self.radio_event_rx.try_recv() {
|
while let Ok(event) = self.radio_event_rx.try_recv() {
|
||||||
match event {
|
match event {
|
||||||
RadioEvent::SignalCaptured(mut capture) => {
|
RadioEvent::SignalCaptured(mut capture) => {
|
||||||
|
|||||||
+33
-1
@@ -7,7 +7,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||||
Arc, Mutex,
|
Arc, Mutex,
|
||||||
};
|
};
|
||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
@@ -58,6 +58,8 @@ pub struct HackRfController {
|
|||||||
hackrf_available: bool,
|
hackrf_available: bool,
|
||||||
/// Shared gain settings (read by receiver thread)
|
/// Shared gain settings (read by receiver thread)
|
||||||
gain_settings: Arc<Mutex<GainSettings>>,
|
gain_settings: Arc<Mutex<GainSettings>>,
|
||||||
|
/// RSSI (f32 bits) written by RX callback, read by UI - never blocks
|
||||||
|
rssi_value: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HackRfController {
|
impl HackRfController {
|
||||||
@@ -84,9 +86,15 @@ impl HackRfController {
|
|||||||
demodulator_fm: Arc::new(Mutex::new(demodulator_fm)),
|
demodulator_fm: Arc::new(Mutex::new(demodulator_fm)),
|
||||||
hackrf_available,
|
hackrf_available,
|
||||||
gain_settings: Arc::new(Mutex::new(GainSettings::default())),
|
gain_settings: Arc::new(Mutex::new(GainSettings::default())),
|
||||||
|
rssi_value: Arc::new(AtomicU32::new(0)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared atomic for RSSI (f32::to_bits); UI reads so callback never blocks on channel.
|
||||||
|
pub fn rssi_source(&self) -> Arc<AtomicU32> {
|
||||||
|
self.rssi_value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if HackRF is available
|
/// Check if HackRF is available
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn is_available(&self) -> bool {
|
pub fn is_available(&self) -> bool {
|
||||||
@@ -114,6 +122,7 @@ impl HackRfController {
|
|||||||
let demodulator_fm = self.demodulator_fm.clone();
|
let demodulator_fm = self.demodulator_fm.clone();
|
||||||
let hackrf_available = self.hackrf_available;
|
let hackrf_available = self.hackrf_available;
|
||||||
let gain_settings = self.gain_settings.clone();
|
let gain_settings = self.gain_settings.clone();
|
||||||
|
let rssi_value = self.rssi_value.clone();
|
||||||
|
|
||||||
self.rx_thread = Some(thread::spawn(move || {
|
self.rx_thread = Some(thread::spawn(move || {
|
||||||
if hackrf_available {
|
if hackrf_available {
|
||||||
@@ -124,6 +133,7 @@ impl HackRfController {
|
|||||||
demodulator_am,
|
demodulator_am,
|
||||||
demodulator_fm,
|
demodulator_fm,
|
||||||
gain_settings,
|
gain_settings,
|
||||||
|
rssi_value,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
let _ = event_tx.send(RadioEvent::Error(format!("Receiver error: {}", e)));
|
let _ = event_tx.send(RadioEvent::Error(format!("Receiver error: {}", e)));
|
||||||
@@ -269,6 +279,8 @@ struct RxState {
|
|||||||
demodulator_am: Arc<Mutex<Demodulator>>,
|
demodulator_am: Arc<Mutex<Demodulator>>,
|
||||||
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
||||||
capture_id: std::sync::atomic::AtomicU32,
|
capture_id: std::sync::atomic::AtomicU32,
|
||||||
|
/// RSSI (f32 bits) written here so callback never blocks on channel
|
||||||
|
rssi_value: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pairs_to_stored(pairs: &[LevelDuration]) -> Vec<StoredLevelDuration> {
|
fn pairs_to_stored(pairs: &[LevelDuration]) -> Vec<StoredLevelDuration> {
|
||||||
@@ -281,6 +293,22 @@ fn pairs_to_stored(pairs: &[LevelDuration]) -> Vec<StoredLevelDuration> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute average magnitude of IQ buffer (0..~1 for i8)
|
||||||
|
fn compute_rssi(buffer: &[num_complex::Complex<i8>]) -> f32 {
|
||||||
|
if buffer.is_empty() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let sum_mag: f32 = buffer
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
let i = c.re as f32 / 128.0;
|
||||||
|
let q = c.im as f32 / 128.0;
|
||||||
|
(i * i + q * q).sqrt()
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
sum_mag / buffer.len() as f32
|
||||||
|
}
|
||||||
|
|
||||||
/// RX callback: feed same IQ to AM and FM demodulators; emit a capture per path when signal complete.
|
/// RX callback: feed same IQ to AM and FM demodulators; emit a capture per path when signal complete.
|
||||||
fn rx_callback(
|
fn rx_callback(
|
||||||
_hackrf: &libhackrf::HackRf,
|
_hackrf: &libhackrf::HackRf,
|
||||||
@@ -297,6 +325,8 @@ fn rx_callback(
|
|||||||
let current_freq = *state.frequency.lock().unwrap();
|
let current_freq = *state.frequency.lock().unwrap();
|
||||||
let samples: Vec<i8> = buffer.iter().flat_map(|c| [c.re, c.im]).collect();
|
let samples: Vec<i8> = buffer.iter().flat_map(|c| [c.re, c.im]).collect();
|
||||||
|
|
||||||
|
state.rssi_value.store(compute_rssi(buffer).to_bits(), Ordering::Relaxed);
|
||||||
|
|
||||||
if let Ok(mut demod) = state.demodulator_am.lock() {
|
if let Ok(mut demod) = state.demodulator_am.lock() {
|
||||||
if let Some(pairs) = demod.process_samples(&samples) {
|
if let Some(pairs) = demod.process_samples(&samples) {
|
||||||
let id = state.capture_id.fetch_add(1, Ordering::SeqCst);
|
let id = state.capture_id.fetch_add(1, Ordering::SeqCst);
|
||||||
@@ -331,6 +361,7 @@ fn run_receiver_hackrf(
|
|||||||
demodulator_am: Arc<Mutex<Demodulator>>,
|
demodulator_am: Arc<Mutex<Demodulator>>,
|
||||||
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
||||||
gain_settings: Arc<Mutex<GainSettings>>,
|
gain_settings: Arc<Mutex<GainSettings>>,
|
||||||
|
rssi_value: Arc<AtomicU32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
@@ -366,6 +397,7 @@ fn run_receiver_hackrf(
|
|||||||
demodulator_am,
|
demodulator_am,
|
||||||
demodulator_fm,
|
demodulator_fm,
|
||||||
capture_id: std::sync::atomic::AtomicU32::new(0),
|
capture_id: std::sync::atomic::AtomicU32::new(0),
|
||||||
|
rssi_value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+37
-1
@@ -6,7 +6,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||||
Arc, Mutex,
|
Arc, Mutex,
|
||||||
};
|
};
|
||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
@@ -48,6 +48,8 @@ pub struct RtlSdrController {
|
|||||||
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
||||||
rtlsdr_available: bool,
|
rtlsdr_available: bool,
|
||||||
gain_settings: Arc<Mutex<TunerGainSetting>>,
|
gain_settings: Arc<Mutex<TunerGainSetting>>,
|
||||||
|
/// RSSI (f32 bits) written by RX thread, read by UI - never blocks
|
||||||
|
rssi_value: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtlSdrController {
|
impl RtlSdrController {
|
||||||
@@ -73,9 +75,15 @@ impl RtlSdrController {
|
|||||||
demodulator_fm: Arc::new(Mutex::new(demodulator_fm)),
|
demodulator_fm: Arc::new(Mutex::new(demodulator_fm)),
|
||||||
rtlsdr_available,
|
rtlsdr_available,
|
||||||
gain_settings: Arc::new(Mutex::new(TunerGainSetting::default())),
|
gain_settings: Arc::new(Mutex::new(TunerGainSetting::default())),
|
||||||
|
rssi_value: Arc::new(AtomicU32::new(0)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared atomic for RSSI (f32::to_bits); UI reads so RX thread never blocks on channel.
|
||||||
|
pub fn rssi_source(&self) -> Arc<AtomicU32> {
|
||||||
|
self.rssi_value.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if an RTL-SDR device was found.
|
/// Returns true if an RTL-SDR device was found.
|
||||||
pub fn is_available(&self) -> bool {
|
pub fn is_available(&self) -> bool {
|
||||||
self.rtlsdr_available
|
self.rtlsdr_available
|
||||||
@@ -102,6 +110,7 @@ impl RtlSdrController {
|
|||||||
let demodulator_fm = self.demodulator_fm.clone();
|
let demodulator_fm = self.demodulator_fm.clone();
|
||||||
let rtlsdr_available = self.rtlsdr_available;
|
let rtlsdr_available = self.rtlsdr_available;
|
||||||
let gain_settings = self.gain_settings.clone();
|
let gain_settings = self.gain_settings.clone();
|
||||||
|
let rssi_value = self.rssi_value.clone();
|
||||||
|
|
||||||
self.rx_thread = Some(thread::spawn(move || {
|
self.rx_thread = Some(thread::spawn(move || {
|
||||||
if rtlsdr_available {
|
if rtlsdr_available {
|
||||||
@@ -112,6 +121,7 @@ impl RtlSdrController {
|
|||||||
demodulator_am,
|
demodulator_am,
|
||||||
demodulator_fm,
|
demodulator_fm,
|
||||||
gain_settings,
|
gain_settings,
|
||||||
|
rssi_value,
|
||||||
) {
|
) {
|
||||||
let _ = event_tx.send(RadioEvent::Error(format!("RTL-SDR receiver error: {}", e)));
|
let _ = event_tx.send(RadioEvent::Error(format!("RTL-SDR receiver error: {}", e)));
|
||||||
}
|
}
|
||||||
@@ -235,6 +245,29 @@ fn u8_iq_to_i8(buf: &[u8]) -> Vec<i8> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Average magnitude of interleaved I/Q i8 samples (0..~1).
|
||||||
|
fn compute_rssi_i8(samples: &[i8]) -> f32 {
|
||||||
|
if samples.len() < 2 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let mut sum = 0.0f32;
|
||||||
|
let mut count = 0usize;
|
||||||
|
for chunk in samples.chunks(2) {
|
||||||
|
if chunk.len() < 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let i = chunk[0] as f32 / 128.0;
|
||||||
|
let q = chunk[1] as f32 / 128.0;
|
||||||
|
sum += (i * i + q * q).sqrt();
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
sum / count as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the receiver loop with an RTL-SDR device.
|
/// Run the receiver loop with an RTL-SDR device.
|
||||||
fn run_receiver_rtlsdr(
|
fn run_receiver_rtlsdr(
|
||||||
receiving: Arc<AtomicBool>,
|
receiving: Arc<AtomicBool>,
|
||||||
@@ -243,6 +276,7 @@ fn run_receiver_rtlsdr(
|
|||||||
demodulator_am: Arc<Mutex<Demodulator>>,
|
demodulator_am: Arc<Mutex<Demodulator>>,
|
||||||
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
demodulator_fm: Arc<Mutex<FmDemodulator>>,
|
||||||
gain_settings: Arc<Mutex<TunerGainSetting>>,
|
gain_settings: Arc<Mutex<TunerGainSetting>>,
|
||||||
|
rssi_value: Arc<AtomicU32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
@@ -278,6 +312,8 @@ fn run_receiver_rtlsdr(
|
|||||||
let current_freq = *frequency.lock().unwrap();
|
let current_freq = *frequency.lock().unwrap();
|
||||||
let samples = u8_iq_to_i8(&buf[..n]);
|
let samples = u8_iq_to_i8(&buf[..n]);
|
||||||
|
|
||||||
|
rssi_value.store(compute_rssi_i8(&samples).to_bits(), Ordering::Relaxed);
|
||||||
|
|
||||||
if let Ok(mut demod) = demodulator_am.lock() {
|
if let Ok(mut demod) = demodulator_am.lock() {
|
||||||
if let Some(pairs) = demod.process_samples(&samples) {
|
if let Some(pairs) = demod.process_samples(&samples) {
|
||||||
let id = capture_id.fetch_add(1, Ordering::SeqCst);
|
let id = capture_id.fetch_add(1, Ordering::SeqCst);
|
||||||
|
|||||||
+67
-20
@@ -3,7 +3,7 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
@@ -21,52 +21,65 @@ use crate::app::InputMode as IM;
|
|||||||
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// RSSI bar width (right side)
|
||||||
|
const RSSI_BAR_WIDTH: u16 = 5;
|
||||||
|
|
||||||
/// Draw the entire UI
|
/// Draw the entire UI
|
||||||
pub fn draw_ui(frame: &mut Frame, app: &App) {
|
pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||||
let show_settings = matches!(app.input_mode, IM::SettingsSelect | IM::SettingsEdit);
|
let show_settings = matches!(app.input_mode, IM::SettingsSelect | IM::SettingsEdit);
|
||||||
let show_command = app.input_mode == IM::Command;
|
let show_command = app.input_mode == IM::Command;
|
||||||
|
|
||||||
let mut constraints = vec![Constraint::Length(3)]; // Header
|
// Full-width rows: header, settings (optional), then middle row split into [captures | RX bar], status, command (optional), help
|
||||||
|
let main_area = frame.area();
|
||||||
|
let mut v_constraints = vec![
|
||||||
|
Constraint::Length(3), // Header (full width)
|
||||||
|
Constraint::Min(8), // Middle: captures + RX bar
|
||||||
|
Constraint::Length(3), // Status bar (full width)
|
||||||
|
Constraint::Length(1), // Help bar (full width)
|
||||||
|
];
|
||||||
if show_settings {
|
if show_settings {
|
||||||
constraints.push(Constraint::Length(3)); // Settings tabs
|
v_constraints.insert(1, Constraint::Length(3)); // Settings tabs (full width)
|
||||||
}
|
}
|
||||||
|
|
||||||
constraints.push(Constraint::Min(8)); // Captures list
|
|
||||||
constraints.push(Constraint::Length(3)); // Status bar
|
|
||||||
|
|
||||||
if show_command {
|
if show_command {
|
||||||
constraints.push(Constraint::Length(3)); // Command input
|
v_constraints.insert(v_constraints.len() - 1, Constraint::Length(3)); // Command (full width)
|
||||||
}
|
}
|
||||||
|
|
||||||
constraints.push(Constraint::Length(1)); // Help bar
|
let rows = Layout::default()
|
||||||
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(constraints)
|
.constraints(v_constraints)
|
||||||
.split(frame.area());
|
.split(main_area);
|
||||||
|
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
render_header(frame, chunks[idx], app);
|
render_header(frame, rows[idx], app);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
if show_settings {
|
if show_settings {
|
||||||
render_settings_tabs(frame, chunks[idx], app);
|
render_settings_tabs(frame, rows[idx], app);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_captures_list(frame, chunks[idx], app);
|
// Only the middle row is split: captures (left) | RX bar (right)
|
||||||
|
let middle_row = rows[idx];
|
||||||
|
let h_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Min(0), Constraint::Length(RSSI_BAR_WIDTH)])
|
||||||
|
.split(middle_row);
|
||||||
|
let captures_area = h_chunks[0];
|
||||||
|
let rssi_bar_rect = h_chunks[1];
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
render_status_bar(frame, chunks[idx], app);
|
render_captures_list(frame, captures_area, app);
|
||||||
|
render_rssi_bar(frame, rssi_bar_rect, app);
|
||||||
|
|
||||||
|
render_status_bar(frame, rows[idx], app);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
if show_command {
|
if show_command {
|
||||||
render_command_line(frame, chunks[idx], app);
|
render_command_line(frame, rows[idx], app);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_help_bar(frame, chunks[idx], app);
|
render_help_bar(frame, rows[idx], app);
|
||||||
|
|
||||||
// Overlay widgets (rendered on top of everything else)
|
// Overlay widgets (rendered on top of everything else)
|
||||||
if app.input_mode == InputMode::SignalMenu {
|
if app.input_mode == InputMode::SignalMenu {
|
||||||
@@ -111,6 +124,40 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the RSSI bar on the right (vertical bar, bottom = strong).
|
||||||
|
fn render_rssi_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(" RX ");
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
if inner.width == 0 || inner.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize RSSI (0..~1) to fill ratio; scale so ~0.5 magnitude ≈ full bar
|
||||||
|
let fill_ratio = (app.rssi / 0.6).min(1.0);
|
||||||
|
let filled_rows = (inner.height as f32 * fill_ratio).round() as u16;
|
||||||
|
|
||||||
|
let filled_style = Style::default().fg(Color::Green);
|
||||||
|
let empty_style = Style::default().fg(Color::DarkGray);
|
||||||
|
|
||||||
|
let mut lines = Vec::with_capacity(inner.height as usize);
|
||||||
|
for r in 0..inner.height {
|
||||||
|
let fill = r >= inner.height.saturating_sub(filled_rows);
|
||||||
|
let (style, ch) = if fill {
|
||||||
|
(filled_style, "█")
|
||||||
|
} else {
|
||||||
|
(empty_style, " ")
|
||||||
|
};
|
||||||
|
let s = ch.repeat(inner.width as usize);
|
||||||
|
lines.push(Line::from(Span::styled(s, style)));
|
||||||
|
}
|
||||||
|
let paragraph = Paragraph::new(Text::from(lines));
|
||||||
|
frame.render_widget(paragraph, inner);
|
||||||
|
}
|
||||||
|
|
||||||
/// Render the header with title and radio status
|
/// Render the header with title and radio status
|
||||||
fn render_header(frame: &mut Frame, area: Rect, app: &App) {
|
fn render_header(frame: &mut Frame, area: Rect, app: &App) {
|
||||||
let (status_symbol, status_style) = match app.radio_state {
|
let (status_symbol, status_style) = match app.radio_state {
|
||||||
|
|||||||
Reference in New Issue
Block a user