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}