From 70afb165b7a6527f3ace15be5c7edcd2ca5f715e Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Fri, 4 Sep 2026 14:18:07 +0200 Subject: [PATCH] TP25: Klick-Konsole auf N Clients + Pin-UI Die Konsole (docs/tp6-marker/server.py) war beim Multi-Pin-Umbau nicht mitgezogen worden: hartkodiert auf 2 Clients, pinnte ueber die alte Gegen-AP-Automatik. Jetzt: - Client-Liste aus geraete.txt (schreibt tp25-walk-messung.sh), Fallback auf die zwei alten MACs fuer Alt-Laeufe. - Kein eigenes SSH mehr: je Client letzte Zeile von sampler-.csv (AP/Signal) + Ping-Verlust aus walk-ping-.csv. - N Client-Karten; Alarm, wenn ein gepinnter Client nirgends haengt. - Pin-UI: je Client AP/Band waehlen, set/off/verify via tp25-pin-multi.sh (POST /pinmulti). Alte 2-Client-Automatik (entscheide/pin_setzen/auto) entfernt; tp25-pin-s22.sh liefert nur noch die map. - tp25-runde.sh loest beim Aufraeumen den Multi-Pin statt des S22-Pins. Doku: Ist-Stand-Block + Quick-Index + Messwerkzeug-Bullets nachgezogen, neuer Handoff 2026-09-04. Co-Authored-By: Claude Code --- ...handoff-20260904-tp25-konsole-n-clients.md | 70 +++ docs/teilprojekte.md | 30 +- docs/tp25-runde.sh | 29 +- docs/tp25-walk-messung.sh | 8 + docs/tp6-marker/server.py | 574 +++++++++--------- 5 files changed, 396 insertions(+), 315 deletions(-) create mode 100644 docs/handoff-20260904-tp25-konsole-n-clients.md diff --git a/docs/handoff-20260904-tp25-konsole-n-clients.md b/docs/handoff-20260904-tp25-konsole-n-clients.md new file mode 100644 index 0000000..6fedf1d --- /dev/null +++ b/docs/handoff-20260904-tp25-konsole-n-clients.md @@ -0,0 +1,70 @@ +# Handoff 2026-09-04 — TP25: Klick-Konsole auf N Clients + Pin-UI + +**Kurz:** Die Klick-Konsole (`docs/tp6-marker/server.py`, Port 3031) war beim Multi-Pin-Umbau +(2026-09-04, `tp25-pin-multi.sh` + `TP25_PHONES`) **nicht** mitgezogen worden — sie zeigte weiter +hartkodiert zwei Clients (iPhone/S22) und pinnte ueber die alte Gegen-AP-Automatik. Jetzt ist sie +auf **N Clients** generalisiert und hat eine **Pin-UI**: Signal/AP/Ping-Verlust je Client live, +Pinnen band-genau direkt in der Konsole. Die alte 2-Client-Automatik ist entfernt. + +## Was umgebaut wurde + +`docs/tp6-marker/server.py` (Hauptumbau): +- **Client-Liste aus Datei statt hartkodiert:** `geraete()` liest `geraete.txt` aus dem + Laufverzeichnis (schreibt `tp25-walk-messung.sh`, eine Zeile `name|ip|mac` je Client). Fallback + auf die zwei alten MACs fuer Alt-Laeufe. +- **Kein eigenes SSH mehr in der Konsole:** der `beobachter()`-Thread (2 MACs per SSH, + `beide-telefone.csv`) ist raus. Stattdessen liest die Konsole je Client die **letzte Zeile** von + `sampler-.csv` (`sta_iface`, `sta_signal`) und den **juengsten Ping-Verlust** aus + `walk-ping-.csv` (leeres `ms` = Verlust). Eine SSH-Session weniger gegen den Core-SPOF. +- **N Client-Karten** statt der festen iPhone/S22-Zeile: Name, AP (via `ap_map()`), Signal dBm, + Ping-Verlust der letzten ~30 s. **Alarm**, wenn ein **gepinnter** Client `sta_iface == "absent"` + ist → „ HAENGT NIRGENDS — nicht weitergehen" (Nachfolger des alten S22-Alarms). +- **Pin-UI** (Karte „Pinnen (Multi-Pin)"): je Client ein `%s' + % (name, name, optionen(_pinm["pins"].get(name, "frei")))) - doppel = "%s  ·  %s" % (z(ip_ap, _live["iphone_sig"], "iPhone"), - z(s22_ap, _live["s22_sig"], "S22")) - - if _pin["busy"]: - 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, verschiedene APs — Messpunkt zählt
%s" % doppel - elif gleich and _auto["an"]: - pinstatus = ("⏳ beide auf %s — Gegen-AP wird gerade nachgezogen, " - "noch nicht weitergehen
%s" % (ip_ap, doppel)) - elif gleich: - pinstatus = ("⚠️ beide auf %s — kein Gegen-AP-Messpunkt. " - "Automatik einschalten oder S22 von Hand pinnen.
%s" % (ip_ap, doppel)) - elif wartet: - pinstatus = "⏳ S22 sucht %s …
%s" % (ziel or "?", doppel) - elif ziel in ("ap1", "ap2"): - pinstatus = "S22 gepinnt auf %s
%s" % (ziel, doppel) - elif ziel == "off": - pinstatus = "kein Pin — S22 roamt frei
%s" % doppel + if _pinm["busy"]: + pinstatus = "⏳ %s" % _pinm["msg"].replace("\n", "
") + elif _pinm["msg"]: + pinstatus = _pinm["msg"].replace("\n", "
") + elif not ziele: + pinstatus = "Manager nicht erreichbar — keine Pin-Ziele." else: - pinstatus = "Zustand unbekannt (Manager nicht erreichbar?)" - pinkarte = ( - '
' - '
AP-Pin S22
' - '
%s
' - '
' - '
' - '
' - '
' - '
' - '
' - '
' - '
' - '
' - '
' % (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 "", - "ok" if _auto["an"] else "cam", - "🔁 Gegen-AP: AN (folgt dem iPhone)" if _auto["an"] - else "🔁 Gegen-AP automatisch", - ) - ) + pinstatus = "Je Client AP/Band wählen, dann „Pin setzen“." + + pinm_karte = ( + '
' + '
Pinnen (Multi-Pin)
' + '
%s
' + '
' + '%s' + '
' + '
' + '
' + '
' + '
' + '
' % (1 if alarm else 0, 1 if _pinm["busy"] else 0, pinstatus, zeilen)) raum, seit = current_room() # 🔴 Die Kacheln hingen bis 2026-08-22 am Treiber-Zustand -- ohne laufenden TP6-Treiber @@ -547,8 +568,9 @@ def render(st): 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", "?")), + raumzeile=raumzeile, clients=karten, pinm_karte=pinm_karte, + 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, notaus=notaus, ) @@ -579,6 +601,11 @@ button:active{{filter:brightness(1.3)}} .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}} +.cval{{font:14px/1.6 ui-monospace,Menlo,monospace;color:#9aa4b2;margin-top:2px}} +.pinzeile{{display:flex;align-items:center;gap:10px;margin:8px 0}} +.pinzeile label{{flex:0 0 90px;font-weight:600}} +.pinzeile select{{flex:1;padding:10px;font-size:16px;background:#262b34;color:#e8eaed; + border:1px solid #2a2f3a;border-radius:8px}} form{{margin:0}} {banner} @@ -589,10 +616,10 @@ form{{margin:0}} {countdown} {prog} -{pinkarte} +{clients} +{pinm_karte} {raumzeile} -
WLAN {wlan} -WAN {wan} +
WAN {wan} Marker {last}
{buttons} {notaus} @@ -638,18 +665,10 @@ function alarmton(){{ ton(880,180,0); ton(660,180,0.24); ton(880,260,0.48); function okton(){{ ton(1320,90,0); ton(1760,120,0.11); try{{ navigator.vibrate && navigator.vibrate(90); }}catch(e){{}} }} -function bereitton(){{ ton(1180,110,0); ton(1580,150,0.13); - try{{ navigator.vibrate && navigator.vibrate([60,60,60]); }}catch(e){{}} }} - -var letzterAlarm = 0, letzterBusy = 0, letzterBereit = 0, alarmTimer = null; +var letzterAlarm = 0, letzterBusy = 0, alarmTimer = null; function signale(){{ var sig = document.getElementById('sig'); if(!sig) return; var alarm = sig.dataset.alarm === '1', busy = sig.dataset.busy === '1'; - var bereit = sig.dataset.bereit === '1'; - // Der Piep, auf den Christoph wartet: beide Telefone haengen, auf verschiedenen APs -- - // der Messpunkt ist vollstaendig, weitergehen. Nur bei ZUSTANDSWECHSEL, nicht dauernd. - if(bereit && !letzterBereit) bereitton(); - letzterBereit = bereit ? 1 : 0; if(alarm && !letzterAlarm){{ alarmton(); // Solange nichts geschieht, alle 12 s erinnern -- ein einmaliger Piep in der Hosentasche @@ -657,7 +676,7 @@ function signale(){{ if(!alarmTimer) alarmTimer = setInterval(alarmton, 12000); }} if(!alarm && alarmTimer){{ clearInterval(alarmTimer); alarmTimer = null; }} - if(!busy && letzterBusy) okton(); // Umschalten fertig + if(!busy && letzterBusy) okton(); // Pin-Aktion fertig letzterAlarm = alarm ? 1 : 0; letzterBusy = busy ? 1 : 0; }} @@ -672,28 +691,22 @@ function sendstop(ev){{ return false; }}catch(e){{ return true; }} }} -function sendauto(ev){{ +function sendpinm(ev){{ try{{ ev.preventDefault(); unlock(); okton(); - fetch('/auto', {{method:'POST'}}).catch(function(){{}}); - setTimeout(refresh, 400); setTimeout(refresh, 3000); + var inst = document.getElementById('pininst'); + if(inst) inst.innerHTML = '⏳ setze Pin …
Gepinnte Geräte fliegen kurz aus dem WLAN und verbinden sich neu.'; + var fd = new FormData(ev.target); + fetch('/pinmulti', {{method:'POST', body: new URLSearchParams(fd)}}).catch(function(){{}}); + setTimeout(refresh, 3000); setTimeout(refresh, 9000); setTimeout(refresh, 20000); return false; }}catch(e){{ return true; }} }} -function sendpin(ev, ap){{ +function sendpinmoff(ev){{ try{{ - ev.preventDefault(); unlock(); - // 🔴 Der Klick kommt vom S22 SELBST -- also von genau dem Geraet, das der Pin gleich vom - // WLAN wirft. Die Antwort auf dieses fetch() kommt deshalb womoeglich nie an, und die - // Seite kann sich nicht mehr nachladen. Die Anweisung muss JETZT auf dem Schirm - // stehen, solange die Verbindung noch steht. - var inst = document.getElementById('pininst'); - if(inst) inst.innerHTML = '⏳ schalte auf ' + ap + ' …
' + - 'Dieses Telefon fliegt jetzt aus dem WLAN. Wenn die Seite haengt: in den ' + - 'WLAN-Einstellungen wieder auf dirtyvader-main verbinden, dann laedt sie von selbst.'; - okton(); - fetch('/pin?ap=' + encodeURIComponent(ap), {{method:'POST'}}).catch(function(){{}}); - setTimeout(refresh, 3000); setTimeout(refresh, 9000); setTimeout(refresh, 20000); + ev.preventDefault(); unlock(); okton(); + fetch(ev.target.action, {{method:'POST'}}).catch(function(){{}}); + setTimeout(refresh, 3000); setTimeout(refresh, 9000); return false; }}catch(e){{ return true; }} }} @@ -767,37 +780,27 @@ class Handler(BaseHTTPRequestHandler): self._send(200, json.dumps({"ok": True})) if "text/html" not in ( self.headers.get("Accept") or "") else self._back() return - if self.path.startswith("/auto"): - # Ohne Parameter umschalten (das macht der Knopf), mit ?an=0|1 gezielt setzen -- - # tp25-runde.sh schaltet damit beim Start scharf. Ein Toggle waere dort falsch: - # laeuft die Konsole schon mit Automatik, haette der Start sie AUSgeschaltet. - if "an=" in self.path: - _auto["an"] = self.path.split("an=", 1)[1].split("&")[0] in ("1", "true", "an") + if self.path.startswith("/pinmulti"): + if _pinm["busy"]: + if "text/html" in (self.headers.get("Accept") or ""): + self._back() + else: + self._send(409, json.dumps({"error": "laeuft schon"})) + return + qs = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) + if qs.get("off"): + pinm_aktion("") + elif qs.get("verify"): + pinm_verify() else: - _auto["an"] = not _auto["an"] - _auto["aufgegeben"] = "" - _auto["seit_weg"] = time.time() - write_marker("AUTO=%s" % ("an" if _auto["an"] else "aus")) - if _auto["an"]: - entscheide() + laenge = int(self.headers.get("Content-Length") or 0) + body = self.rfile.read(laenge).decode("utf-8") if laenge else "" + form = urllib.parse.parse_qs(body) + pinm_aktion(build_spec(form)) if "text/html" in (self.headers.get("Accept") or ""): self._back() else: - self._send(200, json.dumps({"ok": True, "auto": _auto["an"]})) - return - if self.path.startswith("/pin?"): - ap = urllib.parse.unquote(self.path.split("ap=", 1)[1].split("&")[0]) - if ap not in ("ap1", "ap2", "off"): - self._send(400, json.dumps({"error": "ap muss ap1, ap2 oder off sein"})) - return - if _pin["busy"]: - self._send(409, json.dumps({"error": "laeuft schon"})) - return - pin_setzen(ap) - if "text/html" in (self.headers.get("Accept") or ""): - self._back() - else: - self._send(200, json.dumps({"ok": True, "ziel": ap})) + self._send(200, json.dumps({"ok": True})) return if self.path.startswith("/m?"): # 🔴 Erst dekodieren, dann NICHT hochschreiben: "RAUM%3DDiele%20OG" wurde sonst zu @@ -836,8 +839,7 @@ if __name__ == "__main__": print("Marker -> %s" % markers_pfad()) for iface, wo in sorted(ap_map().items()): print(" %-14s %s" % (iface, wo)) - threading.Thread(target=beobachter, daemon=True).start() - # 🔴 Threading ist Pflicht, seit der Pin dazugekommen ist: das Umschalten redet ueber SSH + # 🔴 Threading ist Pflicht, seit der Pin dazugekommen ist: das Setzen redet ueber SSH # mit dem Manager und braucht ~8 s. Mit dem alten HTTPServer (single-threaded) haette # das die ganze Konsole blockiert -- inklusive Not-Aus. ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()