diff --git a/docs/tp6-marker/server.py b/docs/tp6-marker/server.py index 4816e88..ba30171 100644 --- a/docs/tp6-marker/server.py +++ b/docs/tp6-marker/server.py @@ -214,6 +214,101 @@ def ap_map(): return _apmap["karte"] +# πŸ”Ž Die Messidee (Christoph, 2026-08-22): das iPhone roamt frei und zeigt damit, welchen AP +# ein normaler Client an dieser Stelle waehlt -- das wissen wir also ohnehin. Interessant +# ist der ANDERE. Deshalb wird das S22 automatisch auf den Gegen-AP gezwungen: pro Standort +# gibt es dann zwei Messpunkte statt einem, ohne zweimal zu gehen. +IPHONE_MAC = os.environ.get("TP25_IPHONE_MAC", "F2:62:4C:E1:AF:09") +S22_MAC = os.environ.get("TP25_PIN_MAC", "2A:0D:B0:5D:AD:A7") + +# Was der Beobachter sieht. "" = nicht angemeldet. +_live = {"iphone": "", "iphone_sig": "", "s22": "", "s22_sig": "", "stand": 0.0} +_auto = {"an": False, "seit_weg": 0.0, "aufgegeben": ""} + + +def core_passwort(): + """Aus dem Credential-Store. Wird nie geloggt und nie in die Seite gerendert.""" + with open(os.path.expanduser("~/.secrets/credentials.env")) as fh: + for zeile in fh: + if zeile.startswith("CRS326_CORE_PASS="): + return zeile.split("=", 1)[1].strip().strip("\"'") + raise RuntimeError("CRS326_CORE_PASS nicht gefunden") + + +def _ap_von(iface): + return ap_map().get(iface, "").split(" /")[0] + + +def beobachter(): + """Haelt EINE SSH-Sitzung offen und liest beide Telefone im 2-s-Takt. + + πŸ”΄ Bewusst kein Poll mit je einem ssh-Aufruf: das waeren ueber einen Rundgang mehrere + hundert Verbindungsaufbauten, jeder ~1 s. Dasselbe Muster wie tp6-monitor-core.sh -- + inklusive Reconnect-Schleife, weil die Sitzung am 2026-08-21 mitten in einer Messung + weggebrochen ist und die Daten still endeten. + """ + # πŸ”΄ KEINE Klammern um die :while-Schleife -- "(:while ...)" laesst RouterOS die + # Sitzung wortlos beenden, und mit stderr auf DEVNULL sieht man nur, dass alle 3 s + # ein neuer ssh startet. Genau so lag der Beobachter am 2026-08-22 tot. + ros = (':while (true) do={' + ':local a [/interface/wifi/registration-table find where mac-address="%s"];' + ':local b [/interface/wifi/registration-table find where mac-address="%s"];' + ':local ai ""; :local as2 ""; :local bi ""; :local bs "";' + ':if ([:len $a] > 0) do={:set ai [/interface/wifi/registration-table get [:pick $a 0] interface];' + ':set as2 [/interface/wifi/registration-table get [:pick $a 0] signal]};' + ':if ([:len $b] > 0) do={:set bi [/interface/wifi/registration-table get [:pick $b 0] interface];' + ':set bs [/interface/wifi/registration-table get [:pick $b 0] signal]};' + ':put ($ai . "," . $as2 . "," . $bi . "," . $bs); :delay 2s;}' + % (IPHONE_MAC, S22_MAC)) + while True: + try: + pw = core_passwort() + # πŸ”΄ sshpass -e statt -p: bei -p steht das Router-Passwort in der KOMMANDOZEILE + # und damit in `ps`, in jedem Prozess-Trace und in jeder Debug-Ausgabe, die + # argv anfasst. Am 2026-08-22 ist es genau so im Klartext in einem Trace + # gelandet. Mit -e liegt es nur in der Umgebung dieses einen Kindprozesses. + umgebung = dict(os.environ, SSHPASS=pw) + proc = subprocess.Popen( + ["sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", + "-o", "ConnectTimeout=8", "-o", "ServerAliveInterval=15", + "admin@10.40.99.1", ros], + stdout=subprocess.PIPE, stderr=open(_p("beobachter.log"), "a"), + text=True, bufsize=1, env=umgebung) + # πŸ”΄ NICHT `for zeile in proc.stdout`: die Iteration eines Pipes liest blockweise + # vor, die Zeilen kommen also erst an, wenn ~4 KB beisammen sind -- bei einer + # Zeile alle 2 s waere das eine Stunde. readline() liefert sofort. + for zeile in iter(proc.stdout.readline, ""): + teile = zeile.strip().split(",") + if len(teile) != 4: + continue + _live.update(iphone=teile[0], iphone_sig=teile[1], + s22=teile[2], s22_sig=teile[3], stand=time.time()) + entscheide() + except Exception as e: + # Ein Beobachter, der still stirbt, ist schlimmer als keiner (Lehre aus dem + # Sampler-Abbruch am 2026-08-21). Also mitschreiben statt schlucken. + try: + with open(_p("beobachter.log"), "a") as fh: + fh.write("%s %s: %s\n" % (time.strftime("%H:%M:%S"), type(e).__name__, e)) + except Exception: + pass + time.sleep(3) + + +def entscheide(): + """Gegen-AP setzen, sobald das iPhone den AP wechselt.""" + if not _auto["an"] or _pin["busy"]: + return + ip_ap = _ap_von(_live["iphone"]) + if ip_ap not in ("ap1", "ap2"): + return # iPhone selbst nicht angemeldet -> nichts zu spiegeln + gegen = "ap1" if ip_ap == "ap2" else "ap2" + if pin_lesen() != gegen: + _auto["aufgegeben"] = "" + _auto["seit_weg"] = time.time() + pin_setzen(gegen) + + # Pin-Zustand. Das Umschalten dauert ~8 s (Regeln + Kick + Nachlesen), deshalb laeuft es # in einem Thread und die Seite zeigt solange "schaltet um". _pin = {"ziel": None, "busy": False, "msg": "", "stand": 0.0} @@ -316,16 +411,45 @@ def render(st): fmt_bps(w.get("sta_tx_bps")), fmt_bps(w.get("sta_rx_bps"))) ziel = pin_lesen() + ip_ap, s22_ap = _ap_von(_live["iphone"]), _ap_von(_live["s22"]) + frisch = time.time() - _live["stand"] < 15 + + # Drei Zustaende, nicht zwei. "S22 kommt nicht rein" ist ein ERGEBNIS (der Gegen-AP + # traegt hier nicht), kein Defekt -- es darf deshalb nicht wie ein Fehler aussehen und + # nicht ewig Alarm schlagen. + bereit = bool(frisch and ip_ap and s22_ap) + wartet = bool(frisch and ip_ap and not s22_ap and not _auto["aufgegeben"]) + if wartet and _auto["seit_weg"] and time.time() - _auto["seit_weg"] > 45: + _auto["aufgegeben"] = ziel or "?" + write_marker("UNERREICHBAR=%s" % (ziel or "?")) + wartet = False + + def z(ap, sig, mac_name): + if not frisch: + return "%s: β€”" % mac_name + return ("%s: %s %s dBm" % (mac_name, ap, sig)) if ap else ("%s: nirgends" % mac_name) + + doppel = "%s  Β·  %s" % (z(ip_ap, _live["iphone_sig"], "iPhone"), + z(s22_ap, _live["s22_sig"], "S22")) + if _pin["busy"]: - pinstatus = "⏳ %s" % _pin["msg"] + pinstatus = "⏳ %s
%s" % (_pin["msg"], doppel) + elif _auto["aufgegeben"]: + pinstatus = ("🚫 S22 bekommt %s hier nicht β€” das ist das Messergebnis, " + "Raum markieren und weitergehen.
%s" % (_auto["aufgegeben"], doppel)) + elif bereit: + pinstatus = "βœ… beide angemeldet, auf verschiedenen APs
%s" % doppel + elif wartet: + pinstatus = "⏳ S22 sucht %s …
%s" % (ziel or "?", doppel) elif ziel in ("ap1", "ap2"): - pinstatus = "S22 haengt fest auf %s" % ziel + pinstatus = "S22 gepinnt auf %s
%s" % (ziel, doppel) elif ziel == "off": - pinstatus = "kein Pin β€” S22 roamt frei" + pinstatus = "kein Pin β€” S22 roamt frei
%s" % doppel else: pinstatus = "Zustand unbekannt (Manager nicht erreichbar?)" pinkarte = ( - '
' + '
' '
AP-Pin S22
' '
%s
' '
' @@ -333,17 +457,27 @@ def render(st): '' '
' '
' + '
' + '
' '
' '
' '
' % (1 if alarm else 0, 1 if _pin["busy"] else 0, ziel or "?", + 1 if bereit else 0, pinstatus, " hier" if ziel == "ap1" else "", - " hier" if ziel == "ap2" else "") + " hier" if ziel == "ap2" else "", + "ok" if _auto["an"] else "cam", + "πŸ” Gegen-AP: AN (folgt dem iPhone)" if _auto["an"] + else "πŸ” Gegen-AP automatisch", + ) ) raum, seit = current_room() + # πŸ”΄ Die Kacheln hingen bis 2026-08-22 am Treiber-Zustand -- ohne laufenden TP6-Treiber + # waren sie unsichtbar, obwohl Raumkartierung genau das ist, was hier gebraucht wird. + # Kartieren geht immer; der Treiber ist dafuer irrelevant. raumzeile = "" - if mode in ("langzeit", "messung"): + if True: aktuell = ("%s Β· seit %d:%02d" % (raum, seit // 60, seit % 60)) if raum else "β€” noch kein Raum gewΓ€hlt β€”" kacheln = "".join( '
' @@ -352,18 +486,30 @@ def render(st): raumzeile = ('
Wo bist du gerade?
' '
%s
%s
' % (aktuell, kacheln)) + # Die Call-Buttons (Start / Not-Aus fuer die Saettigungs-Flows) gehoeren zu TP6 und haben + # ohne Treiber keine Wirkung -- ein Start-Klick haette dort NICHTS gemessen. Sichtbar + # also nur, wenn wirklich ein Treiber laeuft. btns = "\n".join( '' '
' % (n, n, c, l) for n, l, c, modes in BUTTONS if mode in modes - ) + ) if st.get("driver") else "" + notaus = ('
' + '
' + ) if st.get("driver") else "" + + # 🚢 Gehen/Stehen braucht keinen Treiber -- das sind reine Zeitmarken fuer die Auswertung. + btns = ('
' + '
' + '
' + '
') + btns return PAGE.format( banner=banner, title=st.get("title", "β€”"), phase=st.get("phase", "β€”"), instruction=st.get("instruction", ""), prog=prog, countdown=countdown, raumzeile=raumzeile, pinkarte=pinkarte, wlan=wlan, wan="rx %s / tx %s Β· CPU %s%%" % (fmt_bps(w.get("wan_rx")), fmt_bps(w.get("wan_tx")), w.get("cpu", "?")), - last=last_marker(), buttons=btns, + last=last_marker(), buttons=btns, notaus=notaus, ) @@ -409,8 +555,7 @@ form{{margin:0}} WAN {wan} Marker {last}
{buttons} -
-
+{notaus}