feat: redesign active display layout with mode box and title box
New layout:
- Mode box (left): RX/TX indicator with vertical separator
- Title box (right): 'DongLoRa v{ver}' + radio settings (freq/bw/sf/cr)
- Centered info rows: packet counters + RSSI/SNR with labels
- Sparkline bar graph fills remaining height
- Idle state returns to splash screen (active screen only for RX/TX)
- Layout positions derived from display dimensions (h/w agnostic)
- RSSI history buffer doubled to 128 slots (~2 min at 1s/slot)
- Smart compact_count() formatting for large RX/TX counters (k/M)
This commit is contained in:
3 files changed
+273
-117
No files matched your search
@@ -0,0 +1,26 @@
|
||||
+---------------------+
|
||||
|RXTX 910.525MHz 22dBM|
|
||||
|62.5kHz SF7 CR4/5 |
|
||||
|RX:4 TX:0 |
|
||||
|0dBm SNR:14dB |
|
||||
|---------------------|
|
||||
| <BAR GRAPH HERE> |
|
||||
+---------------------+
|
||||
|
||||
|
||||
+---------------------+
|
||||
|RX|DongLoRa v99.99.99| <= title (centered in title box)
|
||||
|TX| 950.000/500/12/8 | <= radio settings (centered in title box)
|
||||
|---------------------| <= thin separator line
|
||||
|RX:99999999 TX:999999| <= rx/tx counts (centered, see note below)
|
||||
|RSSI:-120dBm SNR:20dB| <= last receive info (cenetered)
|
||||
|---------------------| <= thin separator line
|
||||
|RX/TX BAR GRAPH HERE | <= bar graph, can be shorter than current
|
||||
+---------------------+
|
||||
|
||||
* note the vertical bar to the right of the RX/TX indicators (which work the
|
||||
same as before, alternating based on which mode we're in, and being inverted) -- that vertical bar puts them in their own little box now which I'm calling the "mode box" (get rid of "IDLE" mode, we never need to show that since we just go back to the idle/splash screen), and that gives way to a new box to the right I'm calling the title box
|
||||
* note on RX/TX counts.. if these exceed what we have space for, I guess figure out something smart to do?
|
||||
* note on display size -- all of this has to be done in a h/w agnostic way because different boards might have different display sizes!
|
||||
* I believe I've used max values for all this stuff, check that and correct if needed
|
||||
* Also, I want things to stay laid out nicely,
|
||||
+198
-106
@@ -12,15 +12,15 @@ use heapless::String;
|
||||
use crate::channel::{RadioState, RadioStatus};
|
||||
use crate::protocol::Bandwidth;
|
||||
|
||||
/// RSSI ring buffer length — one bar per 2px across 128px.
|
||||
pub const RSSI_HISTORY_LEN: usize = 64;
|
||||
/// RSSI ring buffer length — supports displays up to 256px wide (2px per bar).
|
||||
pub const RSSI_HISTORY_LEN: usize = 128;
|
||||
|
||||
// Layout constants
|
||||
const W: i32 = 128;
|
||||
// Font metrics (FONT_6X10)
|
||||
const CHAR_W: i32 = 6;
|
||||
const FONT_H: i32 = 10;
|
||||
const SEP_Y: i32 = 43;
|
||||
const SPARK_TOP: i32 = 45;
|
||||
const SPARK_H: i32 = 19; // y=45..63
|
||||
|
||||
// Mode box: 2 chars + 1px padding each side
|
||||
const MODE_BOX_W: i32 = 2 * CHAR_W + 2; // 14px
|
||||
|
||||
// RSSI mapping range (dBm)
|
||||
const RSSI_MIN: i16 = -120;
|
||||
@@ -33,7 +33,7 @@ pub struct BoardInfo<'a> {
|
||||
pub mac: &'a str,
|
||||
}
|
||||
|
||||
/// Render the main status dashboard.
|
||||
/// Render the active dashboard (shown when radio is in RX or TX mode).
|
||||
pub fn dashboard(
|
||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
||||
status: &RadioStatus,
|
||||
@@ -44,111 +44,164 @@ pub fn dashboard(
|
||||
current_slot_tx: bool,
|
||||
board: &BoardInfo,
|
||||
) {
|
||||
let bb = target.bounding_box();
|
||||
let w = bb.size.width as i32;
|
||||
let h = bb.size.height as i32;
|
||||
let title_x = MODE_BOX_W + 1;
|
||||
let title_w = w - title_x;
|
||||
let header_h = 2 * FONT_H;
|
||||
let sep1_y = header_h + 3; // 3px gap below header text (clears descenders)
|
||||
let info_y = sep1_y + 1; // 1px line, info text tight below separator
|
||||
let sep2_y = info_y + 2 * FONT_H + 3; // 3px gap below info text (clears descenders)
|
||||
let spark_top = sep2_y + 2; // 1px line + 1px gap above graph
|
||||
let spark_h = h - spark_top;
|
||||
let visible_bars = ((w / 2) as usize).min(RSSI_HISTORY_LEN);
|
||||
|
||||
let _ = target.clear(BinaryColor::Off);
|
||||
let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
||||
let mut buf: String<32> = String::new();
|
||||
|
||||
match status.config {
|
||||
Some(cfg) => {
|
||||
let freq_mhz = cfg.freq_hz / 1_000_000;
|
||||
let freq_khz = (cfg.freq_hz % 1_000_000) / 1_000;
|
||||
let _ = write!(buf, " {}.{:03}MHz", freq_mhz, freq_khz);
|
||||
Text::new(&buf, Point::new(0, FONT_H - 1), style)
|
||||
.draw(target)
|
||||
.ok();
|
||||
buf.clear();
|
||||
let _ = write!(buf, "{}dBm", cfg.tx_power_dbm);
|
||||
Text::with_alignment(&buf, Point::new(W - 1, FONT_H - 1), style, Alignment::Right)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// State indicator: inverted "RX" or "TX", plain "IDLE"
|
||||
match status.state {
|
||||
RadioState::Idle => {
|
||||
Text::new("IDLE", Point::new(0, FONT_H - 1), style)
|
||||
.draw(target)
|
||||
.ok();
|
||||
}
|
||||
RadioState::Receiving => {
|
||||
let x = 0;
|
||||
let char_w = 6;
|
||||
let text_w = 2 * char_w; // "RX" = 2 chars
|
||||
// ── Mode box (left column, 2 rows tall) ─────────────────────────
|
||||
let fill = PrimitiveStyle::with_fill(BinaryColor::On);
|
||||
let mut inv_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::Off);
|
||||
inv_style.set_background_color(Some(BinaryColor::On));
|
||||
Rectangle::new(Point::new(x, 0), Size::new(text_w as u32, FONT_H as u32))
|
||||
.into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
|
||||
|
||||
match status.state {
|
||||
RadioState::Receiving => {
|
||||
// "RX" inverted on row 0
|
||||
Rectangle::new(Point::new(0, 0), Size::new(MODE_BOX_W as u32, FONT_H as u32))
|
||||
.into_styled(fill)
|
||||
.draw(target)
|
||||
.ok();
|
||||
Text::new("RX", Point::new(x, FONT_H - 1), inv_style)
|
||||
Text::new("RX", Point::new(1, FONT_H - 1), inv_style)
|
||||
.draw(target)
|
||||
.ok();
|
||||
}
|
||||
RadioState::Transmitting => {
|
||||
let char_w = 6;
|
||||
let x = 2 * char_w; // start at column 2 (third character position)
|
||||
let text_w = 2 * char_w; // "TX" = 2 chars
|
||||
let mut inv_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::Off);
|
||||
inv_style.set_background_color(Some(BinaryColor::On));
|
||||
Rectangle::new(Point::new(x, 0), Size::new(text_w as u32, FONT_H as u32))
|
||||
.into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
|
||||
// "TX" inverted on row 1
|
||||
Rectangle::new(
|
||||
Point::new(0, FONT_H),
|
||||
Size::new(MODE_BOX_W as u32, FONT_H as u32),
|
||||
)
|
||||
.into_styled(fill)
|
||||
.draw(target)
|
||||
.ok();
|
||||
Text::new("TX", Point::new(x, FONT_H - 1), inv_style)
|
||||
Text::new("TX", Point::new(1, 2 * FONT_H - 1), inv_style)
|
||||
.draw(target)
|
||||
.ok();
|
||||
}
|
||||
RadioState::Idle => {} // Not shown on active screen
|
||||
}
|
||||
|
||||
// Row 1: BW (full) SF CR
|
||||
buf.clear();
|
||||
let bw_str = match cfg.bw {
|
||||
Bandwidth::Khz7 => "7.8kHz",
|
||||
Bandwidth::Khz10 => "10.4kHz",
|
||||
Bandwidth::Khz15 => "15.6kHz",
|
||||
Bandwidth::Khz20 => "20.8kHz",
|
||||
Bandwidth::Khz31 => "31.2kHz",
|
||||
Bandwidth::Khz41 => "41.7kHz",
|
||||
Bandwidth::Khz62 => "62.5kHz",
|
||||
Bandwidth::Khz125 => "125kHz",
|
||||
Bandwidth::Khz250 => "250kHz",
|
||||
Bandwidth::Khz500 => "500kHz",
|
||||
};
|
||||
let _ = write!(buf, "{} SF{} CR4/{}", bw_str, cfg.sf, cfg.cr);
|
||||
Text::new(&buf, Point::new(0, FONT_H * 2 - 1), style)
|
||||
// Vertical separator between mode box and title box
|
||||
Line::new(Point::new(MODE_BOX_W, 0), Point::new(MODE_BOX_W, sep1_y))
|
||||
.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// ── Title box (right of mode box) ───────────────────────────────
|
||||
let title_center_x = title_x + title_w / 2;
|
||||
|
||||
// Row 0: "DongLoRa v{version}"
|
||||
buf.clear();
|
||||
let _ = write!(buf, "DongLoRa v{}", board.version);
|
||||
Text::with_alignment(
|
||||
&buf,
|
||||
Point::new(title_center_x, FONT_H - 1),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// Row 1: radio settings "{freq}/{bw}/{sf}/{cr}"
|
||||
if let Some(cfg) = status.config {
|
||||
buf.clear();
|
||||
let freq_mhz = cfg.freq_hz / 1_000_000;
|
||||
let freq_khz = (cfg.freq_hz % 1_000_000) / 1_000;
|
||||
let bw_str = match cfg.bw {
|
||||
Bandwidth::Khz7 => "7.8",
|
||||
Bandwidth::Khz10 => "10.4",
|
||||
Bandwidth::Khz15 => "15.6",
|
||||
Bandwidth::Khz20 => "20.8",
|
||||
Bandwidth::Khz31 => "31.2",
|
||||
Bandwidth::Khz41 => "41.7",
|
||||
Bandwidth::Khz62 => "62.5",
|
||||
Bandwidth::Khz125 => "125",
|
||||
Bandwidth::Khz250 => "250",
|
||||
Bandwidth::Khz500 => "500",
|
||||
};
|
||||
let _ = write!(
|
||||
buf,
|
||||
"{}.{:03}/{}/{}/{}",
|
||||
freq_mhz, freq_khz, bw_str, cfg.sf, cfg.cr
|
||||
);
|
||||
Text::with_alignment(
|
||||
&buf,
|
||||
Point::new(title_center_x, 2 * FONT_H - 1),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// ── First separator line ────────────────────────────────────────
|
||||
Line::new(Point::new(0, sep1_y), Point::new(w - 1, sep1_y))
|
||||
.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// ── Info rows (centered) ────────────────────────────────────────
|
||||
let center_x = w / 2;
|
||||
|
||||
// Row 2: packet counters
|
||||
buf.clear();
|
||||
let _ = write!(buf, "RX:{} TX:{}", status.rx_count, status.tx_count);
|
||||
Text::new(&buf, Point::new(0, FONT_H * 3 - 1), style)
|
||||
let _ = write!(
|
||||
buf,
|
||||
"RX:{} TX:{}",
|
||||
compact_count(status.rx_count),
|
||||
compact_count(status.tx_count)
|
||||
);
|
||||
Text::with_alignment(
|
||||
&buf,
|
||||
Point::new(center_x, info_y + FONT_H - 1),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// Row 3: RSSI + SNR (compact to fit 21 chars)
|
||||
// Row 3: RSSI + SNR
|
||||
buf.clear();
|
||||
match (status.last_rssi, status.last_snr) {
|
||||
(Some(rssi), Some(snr)) => {
|
||||
let _ = write!(buf, "{}dBm SNR:{}dB", rssi, snr);
|
||||
let _ = write!(buf, "RSSI:{}dBm SNR:{}dB", rssi, snr);
|
||||
}
|
||||
(Some(rssi), None) => {
|
||||
let _ = write!(buf, "{}dBm", rssi);
|
||||
let _ = write!(buf, "RSSI:{}dBm", rssi);
|
||||
}
|
||||
_ => {
|
||||
let _ = write!(buf, "No signal");
|
||||
}
|
||||
}
|
||||
Text::new(&buf, Point::new(0, FONT_H * 4 - 1), style)
|
||||
Text::with_alignment(
|
||||
&buf,
|
||||
Point::new(center_x, info_y + 2 * FONT_H - 1),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// Separator line (moved down to give RSSI/SNR breathing room)
|
||||
Line::new(Point::new(0, SEP_Y), Point::new(W - 1, SEP_Y))
|
||||
// ── Second separator line ───────────────────────────────────────
|
||||
Line::new(Point::new(0, sep2_y), Point::new(w - 1, sep2_y))
|
||||
.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// RSSI sparkline (TX slots shown as dotted bars)
|
||||
// ── Sparkline bar graph ─────────────────────────────────────────
|
||||
if spark_h > 0 {
|
||||
rssi_sparkline(
|
||||
target,
|
||||
rssi_history,
|
||||
@@ -156,51 +209,86 @@ pub fn dashboard(
|
||||
rssi_count,
|
||||
current_slot_rssi,
|
||||
current_slot_tx,
|
||||
spark_top,
|
||||
spark_h,
|
||||
visible_bars,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
// Row 1: "DongLoRa" bold left, version small right
|
||||
}
|
||||
|
||||
/// Render the splash/waiting screen (shown when idle or no config).
|
||||
pub fn splash(
|
||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
||||
board: &BoardInfo,
|
||||
) {
|
||||
let bb = target.bounding_box();
|
||||
let w = bb.size.width as i32;
|
||||
|
||||
let _ = target.clear(BinaryColor::Off);
|
||||
let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
||||
let title_style = MonoTextStyle::new(&FONT_9X15_BOLD, BinaryColor::On);
|
||||
let mut buf: String<32> = String::new();
|
||||
|
||||
let center_x = w / 2;
|
||||
|
||||
// Row 1: "DongLoRa" bold left, version small right
|
||||
Text::new("DongLoRa", Point::new(4, 15), title_style)
|
||||
.draw(target)
|
||||
.ok();
|
||||
buf.clear();
|
||||
let _ = write!(buf, "v{}", board.version);
|
||||
Text::with_alignment(&buf, Point::new(W - 2, 15), style, Alignment::Right)
|
||||
Text::with_alignment(&buf, Point::new(w - 2, 15), style, Alignment::Right)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// Row 2: board name
|
||||
Text::with_alignment(board.name, Point::new(W / 2, 28), style, Alignment::Center)
|
||||
Text::with_alignment(
|
||||
board.name,
|
||||
Point::new(center_x, 28),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// Row 3: MAC address
|
||||
Text::with_alignment(board.mac, Point::new(W / 2, 41), style, Alignment::Center)
|
||||
Text::with_alignment(
|
||||
board.mac,
|
||||
Point::new(center_x, 41),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
|
||||
// Row 4: status
|
||||
Text::with_alignment(
|
||||
"Waiting for host...",
|
||||
Point::new(W / 2, 54),
|
||||
Point::new(center_x, 54),
|
||||
style,
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(target)
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Format a u32 count compactly for display.
|
||||
fn compact_count(n: u32) -> String<10> {
|
||||
let mut s: String<10> = String::new();
|
||||
if n > 9_999_999 {
|
||||
let _ = write!(s, "{}M", n / 1_000_000);
|
||||
} else if n > 999_999 {
|
||||
let _ = write!(s, "{}k", n / 1_000);
|
||||
} else {
|
||||
let _ = write!(s, "{}", n);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Render the RSSI history as a bar-chart sparkline.
|
||||
///
|
||||
/// Each bar represents one time slot. The display task advances the
|
||||
/// slot index on a fixed timer, so each bar covers a constant duration
|
||||
/// regardless of packet rate. RX slots draw solid bars; TX slots draw
|
||||
/// dotted bars (alternating pixel rows). TX takes precedence if both
|
||||
/// occurred in the same slot.
|
||||
///
|
||||
/// The current (uncommitted) slot is rendered at the rightmost position
|
||||
/// so that incoming packets appear on the graph immediately, before the
|
||||
/// slot timer commits them to history.
|
||||
/// so that incoming packets appear on the graph immediately.
|
||||
fn rssi_sparkline(
|
||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
||||
history: &[i16; RSSI_HISTORY_LEN],
|
||||
@@ -208,15 +296,16 @@ fn rssi_sparkline(
|
||||
count: usize,
|
||||
current_rssi: i16,
|
||||
current_tx: bool,
|
||||
spark_top: i32,
|
||||
spark_h: i32,
|
||||
visible_bars: usize,
|
||||
) {
|
||||
let live = current_rssi > RSSI_MIN || current_tx;
|
||||
// Total bars: committed history + optional live bar
|
||||
let committed = count.min(RSSI_HISTORY_LEN);
|
||||
// If live bar is active, reserve the rightmost slot for it
|
||||
let hist_slots = if live {
|
||||
committed.min(RSSI_HISTORY_LEN - 1)
|
||||
committed.min(visible_bars.saturating_sub(1))
|
||||
} else {
|
||||
committed
|
||||
committed.min(visible_bars)
|
||||
};
|
||||
let total = hist_slots + if live { 1 } else { 0 };
|
||||
|
||||
@@ -226,51 +315,53 @@ fn rssi_sparkline(
|
||||
|
||||
let fill = PrimitiveStyle::with_fill(BinaryColor::On);
|
||||
|
||||
// Draw committed history (shifted left by 1 when live bar is present)
|
||||
// Draw committed history
|
||||
for i in 0..hist_slots {
|
||||
let idx = if count <= RSSI_HISTORY_LEN {
|
||||
// Buffer hasn't wrapped yet — if live bar is active, skip
|
||||
// the oldest slot if we've hit the display limit.
|
||||
if live && committed == RSSI_HISTORY_LEN {
|
||||
// Full buffer + live: drop oldest committed slot
|
||||
i + 1
|
||||
if live && committed >= visible_bars {
|
||||
let skip = committed - (visible_bars - 1);
|
||||
i + skip
|
||||
} else {
|
||||
i
|
||||
}
|
||||
} else {
|
||||
let start = count - RSSI_HISTORY_LEN;
|
||||
let start = if live { start + 1 } else { start };
|
||||
(start + i) % RSSI_HISTORY_LEN
|
||||
let skip = RSSI_HISTORY_LEN.saturating_sub(if live {
|
||||
visible_bars - 1
|
||||
} else {
|
||||
visible_bars
|
||||
});
|
||||
(start + skip + i) % RSSI_HISTORY_LEN
|
||||
};
|
||||
let is_tx = tx_history[idx];
|
||||
let rssi = history[idx];
|
||||
|
||||
if let Some(bar_h) = bar_height(rssi, is_tx) {
|
||||
let x = (RSSI_HISTORY_LEN - total + i) as i32 * 2;
|
||||
draw_bar(target, x, bar_h, is_tx, &fill);
|
||||
if let Some(bar_h) = bar_height(rssi, is_tx, spark_h) {
|
||||
let x = (visible_bars - total + i) as i32 * 2;
|
||||
draw_bar(target, x, bar_h, is_tx, spark_top, spark_h, &fill);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw live (current) bar at the rightmost position
|
||||
if live {
|
||||
if let Some(bar_h) = bar_height(current_rssi, current_tx) {
|
||||
let x = (RSSI_HISTORY_LEN - 1) as i32 * 2;
|
||||
draw_bar(target, x, bar_h, current_tx, &fill);
|
||||
if let Some(bar_h) = bar_height(current_rssi, current_tx, spark_h) {
|
||||
let x = (visible_bars - 1) as i32 * 2;
|
||||
draw_bar(target, x, bar_h, current_tx, spark_top, spark_h, &fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the pixel height for a sparkline bar, or None to skip.
|
||||
fn bar_height(rssi: i16, is_tx: bool) -> Option<i32> {
|
||||
fn bar_height(rssi: i16, is_tx: bool, spark_h: i32) -> Option<i32> {
|
||||
let h = if rssi <= RSSI_MIN {
|
||||
if is_tx {
|
||||
SPARK_H / 3
|
||||
spark_h / 3
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
|
||||
((clamped - RSSI_MIN) as i32 * SPARK_H) / (RSSI_MAX - RSSI_MIN) as i32
|
||||
((clamped - RSSI_MIN) as i32 * spark_h) / (RSSI_MAX - RSSI_MIN) as i32
|
||||
};
|
||||
if h == 0 { None } else { Some(h) }
|
||||
}
|
||||
@@ -281,9 +372,11 @@ fn draw_bar(
|
||||
x: i32,
|
||||
bar_h: i32,
|
||||
is_tx: bool,
|
||||
spark_top: i32,
|
||||
spark_h: i32,
|
||||
fill: &PrimitiveStyle<BinaryColor>,
|
||||
) {
|
||||
let y = SPARK_TOP + SPARK_H - bar_h;
|
||||
let y = spark_top + spark_h - bar_h;
|
||||
if is_tx {
|
||||
for row in 0..bar_h {
|
||||
if row % 2 == 0 {
|
||||
@@ -305,4 +398,3 @@ fn draw_bar(
|
||||
pub fn blank(target: &mut impl DrawTarget<Color = BinaryColor>) {
|
||||
let _ = target.clear(BinaryColor::Off);
|
||||
}
|
||||
|
||||
+49
-11
@@ -7,7 +7,9 @@ use ssd1306::size::DisplaySize128x64;
|
||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||
|
||||
use crate::board::DisplayParts;
|
||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioStatus, StatusWatch};
|
||||
use crate::channel::{
|
||||
DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch,
|
||||
};
|
||||
|
||||
use crate::board::{Board, LoRaBoard};
|
||||
|
||||
@@ -15,7 +17,7 @@ use super::render::{self, RSSI_HISTORY_LEN};
|
||||
|
||||
const BOARD_NAME: &str = Board::NAME;
|
||||
|
||||
/// Duration per sparkline slot. 64 slots * 1s = ~1 minute of history.
|
||||
/// Duration per sparkline slot. 128 slots * 1s = ~2 minutes of history.
|
||||
const SPARK_SLOT_MS: u64 = 1000;
|
||||
|
||||
/// Sentinel: no packet received in this slot. Below SX1262 sensitivity
|
||||
@@ -68,6 +70,14 @@ impl DisplayState {
|
||||
self.current_slot_rssi = NO_SIGNAL;
|
||||
self.current_slot_tx = false;
|
||||
}
|
||||
|
||||
/// Whether the active dashboard should be shown (RX or TX mode).
|
||||
fn is_active(&self) -> bool {
|
||||
matches!(
|
||||
self.last_status.state,
|
||||
RadioState::Receiving | RadioState::Transmitting
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
@@ -115,8 +125,8 @@ pub async fn display_task(
|
||||
return;
|
||||
};
|
||||
|
||||
// Show splash/waiting screen (dashboard with no config renders it)
|
||||
render_and_flush(&mut display, &state, &board_info).await;
|
||||
// Show splash/waiting screen
|
||||
render_splash(&mut display, &board_info).await;
|
||||
|
||||
loop {
|
||||
match select3(
|
||||
@@ -141,7 +151,7 @@ pub async fn display_task(
|
||||
state.last_status = radio_status;
|
||||
|
||||
if state.display_on {
|
||||
render_and_flush(&mut display, &state, &board_info).await;
|
||||
render_current(&mut display, &state, &board_info).await;
|
||||
}
|
||||
}
|
||||
Either3::Second(cmd) => match cmd {
|
||||
@@ -157,18 +167,18 @@ pub async fn display_task(
|
||||
if let Some(s) = receiver.try_get() {
|
||||
state.last_status = s;
|
||||
}
|
||||
render_and_flush(&mut display, &state, &board_info).await;
|
||||
render_current(&mut display, &state, &board_info).await;
|
||||
}
|
||||
DisplayCommand::Reset => {
|
||||
state = DisplayState::new();
|
||||
render_and_flush(&mut display, &state, &board_info).await;
|
||||
render_splash(&mut display, &board_info).await;
|
||||
}
|
||||
},
|
||||
Either3::Third(()) => {
|
||||
// Timer tick: advance sparkline slot
|
||||
state.advance_slot();
|
||||
if state.display_on {
|
||||
render_and_flush(&mut display, &state, &board_info).await;
|
||||
if state.display_on && state.is_active() {
|
||||
render_dashboard(&mut display, &state, &board_info).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,8 +193,27 @@ type Display<I> = ssd1306::Ssd1306Async<
|
||||
ssd1306::mode::BufferedGraphicsModeAsync<DisplaySize128x64>,
|
||||
>;
|
||||
|
||||
async fn render_and_flush<I>(display: &mut Display<I>, state: &DisplayState, board: &render::BoardInfo<'_>)
|
||||
where
|
||||
/// Render the appropriate screen (active dashboard or splash) and flush.
|
||||
async fn render_current<I>(
|
||||
display: &mut Display<I>,
|
||||
state: &DisplayState,
|
||||
board: &render::BoardInfo<'_>,
|
||||
) where
|
||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
||||
{
|
||||
if state.is_active() {
|
||||
render_dashboard(display, state, board).await;
|
||||
} else {
|
||||
render_splash(display, board).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the active dashboard and flush.
|
||||
async fn render_dashboard<I>(
|
||||
display: &mut Display<I>,
|
||||
state: &DisplayState,
|
||||
board: &render::BoardInfo<'_>,
|
||||
) where
|
||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
||||
{
|
||||
render::dashboard(
|
||||
@@ -199,3 +228,12 @@ where
|
||||
);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
|
||||
/// Render the splash/waiting screen and flush.
|
||||
async fn render_splash<I>(display: &mut Display<I>, board: &render::BoardInfo<'_>)
|
||||
where
|
||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
||||
{
|
||||
render::splash(display, board);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
Reference in new issue
Block a user