Zweiter Gehweg-Durchgang, diesmal methodisch sauber: Ping vom Core statt vom Laptop, Konsole auf dem iPhone (Wake Lock haelt es wach, sonst misst man Ruhezustand), Raumwechsel per Klick markiert. Das Ergebnis ist eindeutig und macht die Roaming-Geschichte klein. Im ganzen Haus null Paketverlust -- Technik UG und OG, Diele, Kinder-, Schlaf- und Arbeitszimmer, Treppe, alle 0 %, und die Uebergaben ap1<->ap2 auf der Treppe kosten 0,2 s. Auf der Terrasse dagegen 43 % Verlust ueber 162 Sekunden, am Stueck fehlen 20,4 Sekunden, bei -72 dBm. Dort ist ein Call nicht gestoert, sondern weg. Die Topologie erklaert es vollstaendig: ap2 steht in Technik OG, das nur ueber das Arbeitszimmer zugaenglich ist -- zur Terrasse liegen Technik OG, Arbeitszimmer, Diele OG und die Aussenwand dazwischen. Das dreht die Massnahmen um. Ein signal-range-Boden (Fund E) wuerde die Terrasse VERSCHLECHTERN, weil es dort keinen besseren AP gibt, auf den er steuern koennte -- er wuerde nur rauswerfen. Stattdessen: 5 GHz auf 20 MHz (+3 dB, ein Wort im Template), dann ap2 von Technik OG in die Diele OG umhaengen, die an Terrasse, Arbeitszimmer und Treppe grenzt. Kein neuer AP noetig, damit ist auch die User-Randbedingung erfuellt. Konsole nachgezogen: Marker werden URL-dekodiert und behalten ihre Gross- und Kleinschreibung (aus "RAUM=Diele OG" wurde "RAUM%3DDIELE%20OG"), Diele OG ist ergaenzt, und das Raumraster steht jetzt oben -- ohne JavaScript laedt die Seite nach jedem Klick neu, und dann muss der naechste Raum ohne Scrollen erreichbar sein. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
4.3 KiB
Python
109 lines
4.3 KiB
Python
#!/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 <run-dir>
|
||
"""
|
||
import argparse
|
||
import csv
|
||
import os
|
||
import re
|
||
import urllib.parse
|
||
|
||
|
||
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 ros_ms(v):
|
||
"""RouterOS-Dauer in Millisekunden. Schreibweisen: "12ms", "3ms441us", "529us", "1s200ms"."""
|
||
total = 0.0
|
||
for zahl, einheit in re.findall(r"(\d+)(us|ms|s)", v):
|
||
total += int(zahl) * {"us": 0.001, "ms": 1.0, "s": 1000.0}[einheit]
|
||
return total
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--dir", required=True)
|
||
a = ap.parse_args()
|
||
|
||
pings = []
|
||
csv_path = os.path.join(a.dir, "walk-ping.csv")
|
||
if os.path.exists(csv_path):
|
||
# Neues Format: Ping vom Core, "ts_local,seq,ms" — leeres ms = Verlust.
|
||
for r in csv.DictReader(open(csv_path)):
|
||
if not (r.get("seq") or "").isdigit():
|
||
continue # RECONNECT-Zeilen
|
||
ms = (r.get("ms") or "").strip()
|
||
if ms:
|
||
pings.append((sec(r["ts_local"]), int(r["seq"]), ros_ms(ms)))
|
||
else:
|
||
# Altes Format: BSD-ping vom Laptop.
|
||
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))))
|
||
if not pings:
|
||
raise SystemExit("Keine Ping-Antworten gefunden.")
|
||
|
||
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"))):
|
||
# 🔴 Marker kommen URL-kodiert aus dem Formular ("RAUM%3DDIELE%20UG") — dekodieren,
|
||
# sonst heisst der Raum "%3DDIELE%20UG" und die Tabelle ist unlesbar.
|
||
mk = urllib.parse.unquote(m["marker"])
|
||
if mk.upper().startswith("RAUM="):
|
||
rooms.append((int(m["ts_epoch"]), sec(m["ts_local"]), mk[5:].title()))
|
||
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()
|