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:
1 parent
3fccfd7971
commit
97d4d231c7
4 files changed
+75
-64
No files matched your search
@@ -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}} ")
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user