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:
Jean-Pierre MartineauandClaude Sonnet 5 committed 2026-07-11 00:49:48 -04:00
1 parent c819fdfd2f
commit dacdb043f2
2 files changed
+50 -14

No files matched your search

+26 -8
View File
@@ -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
+24 -6
View File
@@ -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 -----------------------------------------------------------