fix: show live sparkline bar immediately on packet receipt

The rightmost graph bar now renders the current (uncommitted) time
slot, so incoming packets appear on the sparkline instantly instead
of waiting up to 1 second for the slot timer to commit them. When
the slot advances, the live bar shifts left into committed history
and a fresh live slot begins.
This commit is contained in:
swaits committed 2026-03-30 14:44:51 -06:00
1 parent b34ca99fd4
commit 466acfc0b6
2 files changed
+90 -35

No files matched your search

+88 -35
View File
@@ -40,6 +40,8 @@ pub fn dashboard(
rssi_history: &[i16; RSSI_HISTORY_LEN], rssi_history: &[i16; RSSI_HISTORY_LEN],
tx_history: &[bool; RSSI_HISTORY_LEN], tx_history: &[bool; RSSI_HISTORY_LEN],
rssi_count: usize, rssi_count: usize,
current_slot_rssi: i16,
current_slot_tx: bool,
board: &BoardInfo, board: &BoardInfo,
) { ) {
let _ = target.clear(BinaryColor::Off); let _ = target.clear(BinaryColor::Off);
@@ -147,7 +149,14 @@ pub fn dashboard(
.ok(); .ok();
// RSSI sparkline (TX slots shown as dotted bars) // RSSI sparkline (TX slots shown as dotted bars)
rssi_sparkline(target, rssi_history, tx_history, rssi_count); rssi_sparkline(
target,
rssi_history,
tx_history,
rssi_count,
current_slot_rssi,
current_slot_tx,
);
} }
None => { None => {
// Row 1: "DongLoRa" bold left, version small right // Row 1: "DongLoRa" bold left, version small right
@@ -188,63 +197,107 @@ pub fn dashboard(
/// regardless of packet rate. RX slots draw solid bars; TX slots draw /// regardless of packet rate. RX slots draw solid bars; TX slots draw
/// dotted bars (alternating pixel rows). TX takes precedence if both /// dotted bars (alternating pixel rows). TX takes precedence if both
/// occurred in the same slot. /// 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.
fn rssi_sparkline( fn rssi_sparkline(
target: &mut impl DrawTarget<Color = BinaryColor>, target: &mut impl DrawTarget<Color = BinaryColor>,
history: &[i16; RSSI_HISTORY_LEN], history: &[i16; RSSI_HISTORY_LEN],
tx_history: &[bool; RSSI_HISTORY_LEN], tx_history: &[bool; RSSI_HISTORY_LEN],
count: usize, count: usize,
current_rssi: i16,
current_tx: bool,
) { ) {
if count == 0 { 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)
} else {
committed
};
let total = hist_slots + if live { 1 } else { 0 };
if total == 0 {
return; return;
} }
let n = count.min(RSSI_HISTORY_LEN);
let fill = PrimitiveStyle::with_fill(BinaryColor::On); let fill = PrimitiveStyle::with_fill(BinaryColor::On);
for i in 0..n { // Draw committed history (shifted left by 1 when live bar is present)
for i in 0..hist_slots {
let idx = if count <= RSSI_HISTORY_LEN { let idx = if count <= RSSI_HISTORY_LEN {
i // 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
} else {
i
}
} else { } else {
(count - RSSI_HISTORY_LEN + i) % RSSI_HISTORY_LEN let start = count - RSSI_HISTORY_LEN;
let start = if live { start + 1 } else { start };
(start + i) % RSSI_HISTORY_LEN
}; };
let is_tx = tx_history[idx]; let is_tx = tx_history[idx];
let rssi = history[idx]; let rssi = history[idx];
// TX slots with no RSSI: show a short fixed-height dotted bar if let Some(bar_h) = bar_height(rssi, is_tx) {
let bar_h = if rssi <= RSSI_MIN { let x = (RSSI_HISTORY_LEN - total + i) as i32 * 2;
if is_tx { draw_bar(target, x, bar_h, is_tx, &fill);
SPARK_H / 3 // small marker for TX-only slots
} else {
continue; // empty slot
}
} else {
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
((clamped - RSSI_MIN) as i32 * SPARK_H) / (RSSI_MAX - RSSI_MIN) as i32
};
if bar_h == 0 {
continue;
} }
}
let x = (RSSI_HISTORY_LEN - n + i) as i32 * 2; // Draw live (current) bar at the rightmost position
let y = SPARK_TOP + SPARK_H - bar_h; 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);
}
}
}
/// Compute the pixel height for a sparkline bar, or None to skip.
fn bar_height(rssi: i16, is_tx: bool) -> Option<i32> {
let h = if rssi <= RSSI_MIN {
if is_tx { if is_tx {
// Dotted bar: draw every other pixel row SPARK_H / 3
for row in 0..bar_h {
if row % 2 == 0 {
Rectangle::new(Point::new(x, y + row), Size::new(2, 1))
.into_styled(fill)
.draw(target)
.ok();
}
}
} else { } else {
// Solid bar for RX return None;
Rectangle::new(Point::new(x, y), Size::new(2, bar_h as u32))
.into_styled(fill)
.draw(target)
.ok();
} }
} else {
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
((clamped - RSSI_MIN) as i32 * SPARK_H) / (RSSI_MAX - RSSI_MIN) as i32
};
if h == 0 { None } else { Some(h) }
}
/// Draw a single sparkline bar (solid for RX, dotted for TX).
fn draw_bar(
target: &mut impl DrawTarget<Color = BinaryColor>,
x: i32,
bar_h: i32,
is_tx: bool,
fill: &PrimitiveStyle<BinaryColor>,
) {
let y = SPARK_TOP + SPARK_H - bar_h;
if is_tx {
for row in 0..bar_h {
if row % 2 == 0 {
Rectangle::new(Point::new(x, y + row), Size::new(2, 1))
.into_styled(*fill)
.draw(target)
.ok();
}
}
} else {
Rectangle::new(Point::new(x, y), Size::new(2, bar_h as u32))
.into_styled(*fill)
.draw(target)
.ok();
} }
} }
+2
View File
@@ -193,6 +193,8 @@ where
&state.rssi_history, &state.rssi_history,
&state.tx_history, &state.tx_history,
state.rssi_count, state.rssi_count,
state.current_slot_rssi,
state.current_slot_tx,
board, board,
); );
let _ = display.flush().await; let _ = display.flush().await;