The dashboard's traffic counts every frame once, under the live feed's names

Traffic listed what the device reported (advert, telemetry, message, ack)
beside the frames those reports arrived in, so one advert was counted twice
and its frame row had to be renamed "raw advert" to tell the two apart. It
now counts frames heard on the air only, named by the same function as the
live feed's class lane (payload_marks), and every bar takes the colour the
feed draws a frame's class in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-28 00:22:51 -04:00
1 parent 3fccfd7971
commit 97d4d231c7
4 files changed
+75 -64

No files matched your search

+32 -43
View File
@@ -12,10 +12,10 @@ stacks three reads of the mesh, coarsest first:
drawn from the same :meth:`~meshterm.services.monitor_service.MonitorService`
buckets. A pulse line beneath it reads the rate, who's been heard, and the busiest
node of the window.
* **Traffic** — the session's tallies by packet class, each with its icon and a
proportional bar, from the monitor's kind counters — the raw ``packet`` bucket
broken out by payload class (channel text, trace, path, …), so every known frame
class shows once heard.
* **Traffic** — the frames heard on the air, tallied by class (chan text, trace,
path, …) under the names and icons the live feed gives them, each with a
proportional bar. Every frame counts once: what the device *reported* about a frame
(its advert event, a decoded message) is not counted a second time beside it.
* **RF health** — the trailing window's reception quality: median SNR (on the trace
tool's quality bar) and RSSI from stored observations, plus the radio's own live
numbers — noise floor, last RSSI/SNR, airtime, battery — polled from the device the
@@ -45,14 +45,8 @@ from ..core.events import MeshEvent
from ..core.models import NODE_TYPE_REPEATER, Observation, utcnow
from ..persistence.repository import OBSERVATION_WINDOW
from .braillechart import axis_chart, axis_chrome, axis_label_w, meter, timeline_rows
from .packet_viewer import (
_PAYLOAD_GLOSS,
DEFAULT_ICON,
KIND_ICONS,
KIND_STYLES,
PAYLOAD_ICONS,
)
from .theme import glyph, snr_style
from .packet_viewer import KIND_STYLES, payload_marks
from .theme import snr_style
from .trace_screen import snr_bar
from .tui.render import render_lines
from .tui.screen import Screen
@@ -69,13 +63,10 @@ _STATS_POLL_S = 10.0
#: How many braille rows tall the activity chart draws (each row is four dot rows).
_CHART_ROWS = 3
#: The order the traffic panel lists its buckets in (heard ones not listed sort last,
#: alphabetically): the decoded families first, then the raw ``packet:<TYPENAME>``
#: classes expanding in the old lone-``packet`` slot — chat-ish frames before
#: protocol-ish ones — with the class-less ``packet`` closing the raw block.
#: The order the traffic panel lists its classes in (heard ones not listed sort last,
#: alphabetically): chat-ish frames before protocol-ish ones, the class-less ``packet``
#: closing the list.
_TRAFFIC_ORDER = (
"advert",
"telemetry",
"packet:GRP_TXT",
"packet:GRP_DATA",
"packet:TEXT_MSG",
@@ -89,31 +80,27 @@ _TRAFFIC_ORDER = (
"packet:MULTIPART",
"packet:CONTROL",
"packet",
"message",
"ack",
)
#: Raw payload glosses that would read identically to a decoded family's row, retold
#: so two meters never share a label (an overheard advert frame vs. the advert event).
_RAW_GLOSS_CLASH = {"advert": "raw advert", "ack": "raw ack"}
def _is_frame_bucket(bucket: str) -> bool:
"""Whether a tally bucket counts frames heard on the air (``packet[:<TYPENAME>]``).
The monitor also tallies what the *device* reported — ``advert``, ``telemetry``,
``message``, ``ack`` — but each of those is a frame already counted under its own
class (the advert event *and* its RX-logged ``ADVERT`` frame), so the traffic panel
counts frames only: every packet once, under the name the live feed gives it.
"""
return bucket == "packet" or bucket.startswith("packet:")
def _traffic_chrome(bucket: str) -> tuple[str, str, str]:
"""One traffic bucket's ``(icon, label, meter style)``.
def _traffic_chrome(bucket: str) -> tuple[str, str]:
"""One traffic bucket's ``(icon, label)`` — the live feed's, from the same function.
A decoded family keeps its kind icon, name, and colour; a ``packet:<TYPENAME>``
bucket takes the payload class's icon and gloss over the calm muted ``packet``
meter — colour keeps marking the decoded families, raw overheard stays quiet.
Icons are mapped to single glyphs on PICOCALC via :func:`~meshterm.ui.theme.glyph`.
"""
if bucket.startswith("packet:"):
typename = bucket.split(":", 1)[1]
gloss = _PAYLOAD_GLOSS.get(typename, typename.lower())
label = _RAW_GLOSS_CLASH.get(gloss, gloss)
emoji = PAYLOAD_ICONS.get(typename, DEFAULT_ICON)
return glyph(emoji), label, KIND_STYLES["packet"]
emoji = KIND_ICONS.get(bucket, DEFAULT_ICON)
return glyph(emoji), bucket, KIND_STYLES.get(bucket, "brand")
typename = bucket.split(":", 1)[1] if ":" in bucket else None
return payload_marks(typename)
#: How many character cells a traffic lane's meter spans (48 half-step levels).
@@ -439,26 +426,28 @@ class DashboardScreen(Screen):
# -- traffic --
def _traffic_section(self) -> list[RenderableType]:
"""Tallies by packet class, each with an icon and a proportional braille meter.
"""Frames heard on the air, by class, each with an icon and a proportional meter.
Every known class shows once heard — the raw ``packet`` bucket is broken out
by payload class (channel text, trace, path, …) instead of lumping every
overheard frame together; classes never heard stay hidden.
Every class shows once heard, named as the live feed names it; classes never
heard stay hidden. Every bar is the one colour the feed draws a frame's class in
(``packet``'s): the icon and the label say which class a row is, so colour has
nothing left to say here.
"""
heading = Text("Traffic", style="accent")
heading.append(" · by packet class · stored history + live", style="muted")
counts = self._kind_counts()
counts = {k: n for k, n in self._kind_counts().items() if _is_frame_bucket(k)}
if not counts:
return [heading, Text("nothing heard yet", style="muted")]
order = {k: i for i, k in enumerate(_TRAFFIC_ORDER)}
peak = max(counts.values())
chrome = {bucket: _traffic_chrome(bucket) for bucket in counts}
label_w = max(len(label) for _, label, _ in chrome.values())
label_w = max(len(label) for _, label in chrome.values())
count_w = len(str(peak))
style = KIND_STYLES["packet"]
rows: list[RenderableType] = [heading]
for bucket in sorted(counts, key=lambda k: (order.get(k, len(order)), k)):
count = counts[bucket]
icon, label, style = chrome[bucket]
icon, label = chrome[bucket]
row = Text(f"{icon} ")
row.append(f"{label.ljust(label_w)} ", style="muted")
row.append(f"{count:>{count_w}} ")
+14 -5
View File
@@ -281,14 +281,23 @@ def class_marks(entry: PacketEntry) -> tuple[str, str]:
"""
if entry.kind == "packet":
raw = entry.raw if isinstance(entry.raw, dict) else {}
typename = raw.get("payload_typename")
if typename:
emoji = PAYLOAD_ICONS.get(typename, DEFAULT_ICON)
return glyph(emoji), _PAYLOAD_GLOSS.get(typename, typename.lower())
return glyph(KIND_ICONS["packet"]), "packet"
return payload_marks(raw.get("payload_typename"))
return kind_icon(entry.kind), entry.kind
def payload_marks(typename: str | None) -> tuple[str, str]:
"""A raw frame's icon and class label from its payload typename alone.
:func:`class_marks`'s half for a ``packet`` frame, for a surface that holds a tally by
typename rather than a packet (the dashboard's traffic rows), so a class reads the same
there as it does in the live feed's class lane. ``None`` is the class-less frame.
"""
if typename:
emoji = PAYLOAD_ICONS.get(typename, DEFAULT_ICON)
return glyph(emoji), _PAYLOAD_GLOSS.get(typename, typename.lower())
return glyph(KIND_ICONS["packet"]), "packet"
def class_chrome(entry: PacketEntry) -> tuple[str, str]:
"""The class headline the viewer's card leads with — :func:`class_marks`, shouted.
+28 -15
View File
@@ -80,7 +80,7 @@ def test_dashboard_renders_all_three_sections() -> None:
screen = _screen(
window=[_obs(), _obs(node="3d63", node_type=2, snr=-2.0)],
histogram=[3] + [0] * (ACTIVITY_BUCKETS - 1),
kinds={"advert": 5, "ack": 1},
kinds={"packet:ADVERT": 5, "packet:ACK": 1},
)
body = _plain(screen.render_body(100))
assert "Activity" in body and "Traffic" in body and "RF health" in body
@@ -140,28 +140,41 @@ def test_dashboard_pulse_drops_heard_only_when_it_wont_fit() -> None:
def test_dashboard_traffic_breaks_packets_out_by_payload_class() -> None:
"""The raw ``packet`` bucket meters per payload class, glossed and iconed."""
"""Each frame class meters on its own row, glossed and iconed as the live feed does."""
screen = _screen(
window=[_obs()],
kinds={"advert": 4, "packet:GRP_TXT": 3, "packet:TRACE": 2, "packet": 1},
kinds={"packet:GRP_TXT": 3, "packet:TRACE": 2, "packet": 1},
)
body = _plain(screen.render_body(100))
assert "chan text" in body and "trace" in body # glossed, not raw typenames
assert "GRP_TXT" not in body
# The class-less remainder keeps a plain "packet" row alongside the classed ones.
assert "packet" in body
# Decoded families before the raw classes, class-less packet closing the block.
assert body.index("advert") < body.index("chan text") < body.index("trace")
# The class-less remainder keeps a plain "packet" row after the classed ones.
assert body.index("chan text") < body.index("trace") < body.rindex("packet")
def test_dashboard_traffic_disambiguates_raw_advert_and_ack_rows() -> None:
"""An overheard ADVERT/ACK frame's row reads "raw …" so no two meters match."""
screen = _screen(
window=[_obs()],
kinds={"advert": 4, "packet:ADVERT": 2, "ack": 3, "packet:ACK": 1},
)
body = _plain(screen.render_body(100))
assert "raw advert" in body and "raw ack" in body
def test_dashboard_traffic_counts_each_frame_once_under_the_feeds_names() -> None:
"""The device's own reports stay out: an advert is its ADVERT frame, counted once.
The monitor tallies the advert *event* and the RX-logged frame it arrived in, and a
decoded message beside its ``chan text`` frame; counting both lists one packet twice.
The frame's row takes the live feed's name — ``advert``, never a "raw" variant.
"""
from meshterm.ui.packet_viewer import payload_marks
kinds = {
"advert": 40,
"telemetry": 9,
"message": 7,
"ack": 30,
"packet:ADVERT": 2,
"packet:ACK": 1,
}
lines = _plain(_screen(window=[_obs()], kinds=kinds).render_body(100)).splitlines()
start = next(i for i, ln in enumerate(lines) if ln.startswith("Traffic"))
rows = lines[start + 1 : lines.index("", start)]
assert [r.split()[1:3] for r in rows] == [["advert", "2"], ["ack", "1"]]
assert not any("raw" in r or "telemetry" in r or "message" in r for r in rows)
assert payload_marks("ADVERT")[1] == "advert" and payload_marks(None)[1] == "packet"
def test_dashboard_live_observations_land_in_the_window() -> None:
+1 -1
View File
@@ -177,7 +177,7 @@ def _dashboard(cols: int, rows: int) -> Screen:
],
activity=lambda: (2.0,) * ACTIVITY_BUCKETS,
activity_flags=lambda: (True,) * ACTIVITY_BUCKETS,
kind_counts=lambda: {"advert": 5, "ack": 2, "packet": 3},
kind_counts=lambda: {"packet:ADVERT": 5, "packet:ACK": 2, "packet": 3},
)