Narrow the SNR quality bars: braille, 8 chars, 16-step resolution
The per-hop meters (shared by the trace and TX-optimize screens) drew 16 solid block characters. They now pack two fill steps into each of 8 braille characters, using only the middle two dot rows (2, 3, 5, 6) so the glyph sits mid-cell instead of hugging the top or bottom edge — half the width at the same 16-step resolution. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c819fdfd2f
commit
dacdb043f2
2 files changed
+50
-14
No files matched your search
@@ -83,14 +83,26 @@ if TYPE_CHECKING:
|
||||
#: Seconds between spinner frames while a trace or probe is in flight.
|
||||
_SPINNER_INTERVAL = 0.12
|
||||
|
||||
#: How wide the per-hop SNR quality bars draw, in cells.
|
||||
_BAR_WIDTH = 16
|
||||
#: How wide the per-hop SNR quality bars draw, in characters. Each character packs
|
||||
#: two fill steps (see :func:`snr_bar`), so the bar reads at 16-step resolution in
|
||||
#: half the columns a one-step-per-cell block bar would need.
|
||||
_BAR_WIDTH = 8
|
||||
|
||||
#: The SNR range the bars span, in dB: -15 (barely readable) to +10 (excellent). Values
|
||||
#: outside clamp to the ends, so the bar always shows *something* for a heard hop.
|
||||
_BAR_SNR_MIN = -15.0
|
||||
_BAR_SNR_MAX = 10.0
|
||||
|
||||
#: A half-filled bar step: a braille cell with only its left column of the middle
|
||||
#: two dot rows raised (dots 2, 3 — the top row, dots 1 and 4, and the bottom row,
|
||||
#: dots 7 and 8, both stay blank, so the glyph sits mid-cell instead of hugging
|
||||
#: either edge).
|
||||
_BAR_HALF = "⠆"
|
||||
|
||||
#: A full bar step: both columns of the same middle two rows raised (dots 2, 3,
|
||||
#: 5, 6).
|
||||
_BAR_FULL = "⠶"
|
||||
|
||||
#: The sample counts the Sample count dialog offers: how many traces one Trace action
|
||||
#: runs, paced between transmissions.
|
||||
SAMPLE_CHOICES = (1, 2, 3, 5, 8)
|
||||
@@ -110,21 +122,27 @@ PathFlow = Callable[[str], Awaitable[Optional[str]]]
|
||||
def snr_bar(snr: Optional[float], width: int = _BAR_WIDTH) -> Text:
|
||||
"""Render an SNR reading as a horizontal quality bar in the shared SNR colours.
|
||||
|
||||
Packs two fill steps into each character — a half step (:data:`_BAR_HALF`) then
|
||||
a full step (:data:`_BAR_FULL`) — for 16-step resolution in ``width`` characters
|
||||
instead of needing one cell per step.
|
||||
|
||||
Args:
|
||||
snr: The reading in dB, or ``None`` (renders as an empty, muted track).
|
||||
width: Total bar track width in cells.
|
||||
width: Bar track width in characters (each worth two fill steps).
|
||||
|
||||
Returns:
|
||||
A :class:`Text` of filled blocks over a faint track, coloured by
|
||||
:func:`~meshterm.ui.theme.snr_style`.
|
||||
A :class:`Text` of filled braille cells over a faint dotted track,
|
||||
coloured by :func:`~meshterm.ui.theme.snr_style`.
|
||||
"""
|
||||
if snr is None:
|
||||
return Text("·" * width, style="faint")
|
||||
span = _BAR_SNR_MAX - _BAR_SNR_MIN
|
||||
frac = min(1.0, max(0.0, (snr - _BAR_SNR_MIN) / span))
|
||||
filled = max(1, round(frac * width))
|
||||
bar = Text("▆" * filled, style=snr_style(snr))
|
||||
bar.append("·" * (width - filled), style="faint")
|
||||
steps = max(1, round(frac * width * 2))
|
||||
full, half = divmod(steps, 2)
|
||||
filled = _BAR_FULL * full + _BAR_HALF * half
|
||||
bar = Text(filled, style=snr_style(snr))
|
||||
bar.append("·" * (width - len(filled)), style="faint")
|
||||
return bar
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from datetime import timedelta
|
||||
import pytest
|
||||
|
||||
from meshterm.core.models import Hop, TraceResult, TraceStats, TxLevelResult, TxOptResult, utcnow
|
||||
from meshterm.ui.trace_screen import TraceScreen, snr_bar
|
||||
from meshterm.ui.trace_screen import _BAR_FULL, _BAR_HALF, _BAR_WIDTH, TraceScreen, snr_bar
|
||||
from meshterm.ui.tx_screen import TxSweepScreen
|
||||
|
||||
|
||||
@@ -53,19 +53,37 @@ def _plain(lines: list[str]) -> str:
|
||||
# --- snr_bar -------------------------------------------------------------------
|
||||
|
||||
|
||||
def _filled_steps(bar: str) -> int:
|
||||
"""Count a rendered bar's fill steps — two per full braille cell, one per half."""
|
||||
return bar.count(_BAR_FULL) * 2 + bar.count(_BAR_HALF)
|
||||
|
||||
|
||||
def test_snr_bar_scales_with_signal_quality() -> None:
|
||||
"""A stronger signal fills more of the track; None renders an empty muted track."""
|
||||
weak = snr_bar(-12.0).plain
|
||||
strong = snr_bar(8.0).plain
|
||||
assert weak.count("▆") < strong.count("▆")
|
||||
assert len(weak) == len(strong) # the track width is constant
|
||||
assert "▆" not in snr_bar(None).plain
|
||||
assert _filled_steps(weak) < _filled_steps(strong)
|
||||
assert len(weak) == len(strong) == _BAR_WIDTH # the track width is constant
|
||||
assert _filled_steps(snr_bar(None).plain) == 0
|
||||
|
||||
|
||||
def test_snr_bar_clamps_out_of_range_readings() -> None:
|
||||
"""Readings beyond the display range clamp to the ends instead of over/underflowing."""
|
||||
assert snr_bar(99.0).plain.count("▆") == snr_bar(10.0).plain.count("▆")
|
||||
assert snr_bar(-99.0).plain.count("▆") == 1 # a heard hop always shows something
|
||||
assert _filled_steps(snr_bar(99.0).plain) == _filled_steps(snr_bar(10.0).plain)
|
||||
assert _filled_steps(snr_bar(-99.0).plain) == 1 # a heard hop always shows something
|
||||
|
||||
|
||||
def test_snr_bar_packs_two_steps_per_character() -> None:
|
||||
"""16 steps of resolution pack into 8 characters: full cells, then one trailing half."""
|
||||
one_step = snr_bar(-13.4375).plain # frac = 1/16 of the -15..+10 span
|
||||
assert one_step[0] == _BAR_HALF
|
||||
assert one_step[1:] == "·" * (_BAR_WIDTH - 1)
|
||||
|
||||
three_steps = snr_bar(-10.3125).plain # frac = 3/16 → one full cell, one half
|
||||
assert three_steps[:2] == _BAR_FULL + _BAR_HALF
|
||||
assert three_steps[2:] == "·" * (_BAR_WIDTH - 2)
|
||||
|
||||
assert snr_bar(10.0).plain == _BAR_FULL * _BAR_WIDTH # top of range: every cell full
|
||||
|
||||
|
||||
# --- _previous_outbound -----------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user