From 41809fe43b4a7c53fa7e0f995496d10041c7d8b2 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Fri, 21 Aug 2026 15:23:43 +0200 Subject: [PATCH] Gehweg-Konsole: Raum-Buttons und Auswertung pro Raum Die Gehweg-Messung erzeugte bisher eine Zeitachse, aus der niemand ablesen konnte, WO es schlecht war -- der teure Abschnitt von gestern liegt bei "15:07 bis 15:09", und das ist kein Satz, aus dem eine Massnahme folgt. Die Konsole bekommt deshalb ein Raster mit einem Button je Raum; der aktive Raum wird hervorgehoben und die Verweildauer mitgezaehlt. Die Liste steht per TP25_ROOMS in der Umgebung, damit ein fehlender Raum kein Codeeingriff ist. tp25-gehweg-auswertung.py verbindet die drei Mitschnitte -- Ping aufs Telefon, Funkwerte vom Manager, Raum-Marken -- zu einer Tabelle mit Signal, Paketverlust und laengster Luecke je Raum, samt AP, auf dem der Client dort haengt. Dazu die Lehre aus dem ersten Durchgang, diesmal als Anleitung statt als Fussnote: die Konsole gehoert waehrend der Messung auf DAS TELEFON. Der Wake Lock haelt das Display an und das Polling das Geraet wach -- ohne das beantwortet ein iPhone die Pings verzoegert oder gar nicht, und 44 % Verlust sind Ruhezustand statt Funk. Co-Authored-By: Claude Opus 5 --- docs/tp25-gehweg-auswertung.py | 83 ++++++++++++++++++++++++++++++++++ docs/tp6-marker/server.py | 36 ++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 docs/tp25-gehweg-auswertung.py diff --git a/docs/tp25-gehweg-auswertung.py b/docs/tp25-gehweg-auswertung.py new file mode 100644 index 0000000..0759682 --- /dev/null +++ b/docs/tp25-gehweg-auswertung.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +TP25 — Gehweg-Auswertung: Signal und Erreichbarkeit pro RAUM. + +Verbindet die drei Mitschnitte der Gehweg-Messung zu einer Raumtabelle: + walk-ping.log (5 Pings/s auf das Telefon) -> Verlust und laengste Luecke + sampler.csv (Funkwerte am Manager) -> AP/Band und Signal + markers.csv (RAUM=-Klicks) -> die Raumgrenzen auf der Zeitachse + +Damit wird aus "irgendwo zwischen 15:07 und 15:09 war es schlecht" ein Satz mit Raumnamen. + + python3 docs/tp25-gehweg-auswertung.py --dir +""" +import argparse +import csv +import os +import re + + +def sec(t): + h, m, s = map(int, t.split(":")) + return h * 3600 + m * 60 + s + + +AP = {"cap-wifi1": "ap1/5GHz", "cap-wifi2": "ap1/2,4GHz", + "cap-wifi3": "ap2/5GHz", "cap-wifi4": "ap2/2,4GHz"} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--dir", required=True) + a = ap.parse_args() + + pings = [] + for line in open(os.path.join(a.dir, "walk-ping.log")): + m = re.match(r"(\d\d:\d\d:\d\d) 64 bytes.*icmp_seq=(\d+).*time=([\d.]+)", line) + if m: + pings.append((sec(m.group(1)), int(m.group(2)), float(m.group(3)))) + + rows = [r for r in csv.DictReader(open(os.path.join(a.dir, "sampler.csv"))) + if r.get("sta_signal")] + + rooms = [] + for m in csv.DictReader(open(os.path.join(a.dir, "markers.csv"))): + if m["marker"].startswith("RAUM="): + rooms.append((int(m["ts_epoch"]), sec(m["ts_local"]), m["marker"][5:])) + if not rooms: + raise SystemExit("Keine RAUM=-Marken in markers.csv — nichts zuzuordnen.") + + ende = max(pings[-1][0], sec(rows[-1]["ts_local"]) if rows else 0) + 1 + fenster = [(r[1], rooms[i + 1][1] if i + 1 < len(rooms) else ende, r[2]) + for i, r in enumerate(rooms)] + + print("%-16s %6s %-24s %-18s %s" % ("Raum", "Dauer", "Signal min/avg/max", "Verlust", "längste Lücke")) + print("-" * 92) + for start, stop, name in fenster: + p = [x for x in pings if start <= x[0] < stop] + r = [x for x in rows if start <= sec(x["ts_local"]) < stop] + if not p and not r: + continue + # Verlust und laengste Luecke aus den Sequenznummern + luecke, verloren = 0.0, 0 + for i in range(1, len(p)): + miss = p[i][1] - p[i - 1][1] - 1 + if miss > 0: + verloren += miss + luecke = max(luecke, miss * 0.2) + gesamt = (p[-1][1] - p[0][1] + 1) if len(p) > 1 else 0 + sig = [int(x["sta_signal"]) for x in r] + ifaces = sorted({AP.get(x["sta_iface"], x["sta_iface"]) for x in r if x["sta_iface"]}) + print("%-16s %5ds %-24s %-18s %s" % ( + name, stop - start, + "%d / %.1f / %d dBm" % (min(sig), sum(sig) / len(sig), max(sig)) if sig else "—", + "%d von %d (%.0f %%)" % (verloren, gesamt, 100 * verloren / gesamt) if gesamt else "—", + ("%.1f s" % luecke) if luecke else "keine")) + if ifaces: + print("%-16s auf %s" % ("", ", ".join(ifaces))) + print("\nLesehilfe: eine längste Lücke über ~2 s ist für einen Videocall eine hörbare Unterbrechung,") + print("über 10 s ist der Call praktisch tot. Signal unter −70 dBm ist die Zone, in der das passiert.") + + +if __name__ == "__main__": + main() diff --git a/docs/tp6-marker/server.py b/docs/tp6-marker/server.py index 70ea0b6..044df2c 100644 --- a/docs/tp6-marker/server.py +++ b/docs/tp6-marker/server.py @@ -32,6 +32,13 @@ RUN_DIR = os.environ.get( SAMPLER = os.environ.get("TP6_SAMPLER", os.path.join(RUN_DIR, "sampler.csv")) PORT = int(os.environ.get("TP6_PORT", "3031")) +# Raeume fuer die Gehweg-/Dead-Spot-Kartierung. Reihenfolge = Reihenfolge auf der Seite. +# Aenderbar ohne Codeeingriff: TP25_ROOMS="Kueche,Bad,..." python3 docs/tp6-marker/server.py +ROOMS = [r.strip() for r in os.environ.get( + "TP25_ROOMS", + "Küche,Wohnzimmer,Diele UG,Technik UG,Treppe,Kinderzimmer,Schlafzimmer,Arbeitszimmer,Bad,Klo,Technik OG,Terrasse" +).split(",") if r.strip()] + MARKERS = os.path.join(RUN_DIR, "markers.csv") PIDFILE = os.path.join(RUN_DIR, "flows.pid") STATE = os.path.join(RUN_DIR, "phase.json") @@ -95,6 +102,18 @@ def last_marker(): return "—" +def current_room(): + """Letzte Raum-Marke plus Verweildauer — damit auf der Seite steht, was mitgeschrieben wird.""" + try: + for row in reversed([r.strip() for r in open(MARKERS) if r.strip()]): + p = row.split(",") + if len(p) >= 3 and p[2].startswith("RAUM="): + return p[2][5:], int(time.time() - int(p[0])) + except Exception: + pass + return None, 0 + + def sampler_tail(): """Letzte Sampler-Zeile fuer die Live-Anzeige. Fehlt sie, ist das kein Fehler.""" try: @@ -168,7 +187,18 @@ def render(st): AP.get(w["iface"], w["iface"]), w.get("signal", "?"), fmt_bps(w.get("sta_tx_bps")), fmt_bps(w.get("sta_rx_bps"))) - btns = "\n".join( + raum, seit = current_room() + raumzeile = "" + if mode in ("langzeit", "messung"): + aktuell = ("%s · seit %d:%02d" % (raum, seit // 60, seit % 60)) if raum else "— noch kein Raum gewählt —" + kacheln = "".join( + '
' + '
' + % (r.replace(" ", "%20"), r, " hier" if r == raum else "", r) for r in ROOMS) + raumzeile = ('
Wo bist du gerade?
' + '
%s
%s
' % (aktuell, kacheln)) + + btns = raumzeile + "\n".join( '
' '
' % (n, n, c, l) for n, l, c, modes in BUTTONS if mode in modes @@ -205,6 +235,10 @@ button:active{{filter:brightness(1.3)}} .start{{background:#14532d}} .bad-me{{background:#7f1d1d}} .bad-geli{{background:#7c3f12}} .ok{{background:#166534}} .cam{{background:#2b3648}} .walk{{background:#1e3a5f}} .end{{background:#3a2b4a}} .abort{{background:#b91c1c;font-size:23px;padding:30px 16px;box-shadow:0 0 0 3px #7f1d1d inset}} +.raster{{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}} +.raster form{{margin:0}} +.raum{{margin:0;padding:16px 8px;font-size:16px;background:#1e3a5f}} +.raum.hier{{background:#0ea5e9;color:#06202e;box-shadow:0 0 0 3px #7dd3fc inset}} form{{margin:0}} {banner}