Initial import from bf-network-r6.opencode — opencode migration
This commit is contained in:
843
docs/tp6-marker/server.py
Normal file
843
docs/tp6-marker/server.py
Normal file
@@ -0,0 +1,843 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TP6 — Klick-Konsole fuer den Call-Durchlauf.
|
||||
|
||||
Zwei Aufgaben:
|
||||
1. FUEHREN: die Seite sagt jederzeit, was gerade laeuft, wie lange es noch dauert und
|
||||
was der Mensch am Telefon jetzt tun (und lassen) soll. Am Ende sagt sie unmissver-
|
||||
staendlich, dass die Messung abgeschlossen ist.
|
||||
2. MITSCHREIBEN: jeder Klick landet mit Zeitstempel in derselben Zeitachse wie der
|
||||
Core-Sampler. Der Not-Aus killt zusaetzlich sofort die Saettigungs-Flows.
|
||||
|
||||
🔴 Zwei Dinge, die hier absichtlich so gebaut sind:
|
||||
- Die Buttons sind server-seitig gerenderte <form>-POSTs. Faellt das JavaScript aus
|
||||
(auf dem iPad am 2026-08-20 passiert: es blieb nur der eine statische Button uebrig),
|
||||
funktioniert die Konsole trotzdem — die Seite laedt dann nach jedem Klick neu.
|
||||
- Die Seite hat ein <meta refresh>. Ohne JS aktualisiert sich die Anleitung dadurch
|
||||
trotzdem; laeuft JS, entfernt es das Meta-Tag und pollt stattdessen leise.
|
||||
|
||||
🔴 SEIT 2026-08-22 AENDERT DIESE SEITE DOCH ETWAS AM NETZ: der AP-Pin (TP25) schreibt
|
||||
access-list-Regeln auf den WLAN-Manager. Die Seite bindet auf 0.0.0.0 und kennt keine
|
||||
Authentifizierung — wer in VLAN10 steht, kann den Pin umschalten. Das ist fuer die Dauer
|
||||
einer Messung vertretbar und sonst nicht: den Server nach dem Rundgang beenden. Der Pin
|
||||
selbst ist harmlos genug (er betrifft eine MAC), aber die Regel ist es nicht, die als
|
||||
naechstes hier landen koennte.
|
||||
|
||||
Bindet auf 0.0.0.0 (Bedienung vom iPhone/iPad).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import urllib.parse
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
# 🔴 Das Laufverzeichnis wird NICHT mehr beim Start eingefroren. Grund (2026-08-22): die
|
||||
# Konsole laeuft als Dauer-Preview auf Port 3031, der Messlauf startet spaeter und legt
|
||||
# ein neues Verzeichnis an. Vorher schrieb der Server dann brav Marker in den Lauf von
|
||||
# GESTERN, waehrend der Sampler ins neue Verzeichnis schrieb -- die beiden Zeitachsen
|
||||
# waren hinterher nicht mehr zusammenzubringen, und man sah es der Seite nicht an.
|
||||
# Jetzt hinterlaesst tp25-walk-messung.sh einen Zeiger, dem die Konsole folgt.
|
||||
FALLBACK_RUN_DIR = ("/private/tmp/claude-501/-Users-christophemsenhuber-dev-bf-network-r6/"
|
||||
"0d86ebd1-035f-4513-a231-5cca8111ecfd/scratchpad/tp6-run2")
|
||||
POINTER = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".current-run")
|
||||
|
||||
|
||||
def run_dir():
|
||||
d = os.environ.get("TP6_RUN_DIR")
|
||||
if d:
|
||||
return d
|
||||
try:
|
||||
z = open(POINTER).read().strip()
|
||||
if z and os.path.isdir(z):
|
||||
return z
|
||||
except Exception:
|
||||
pass
|
||||
return FALLBACK_RUN_DIR
|
||||
|
||||
|
||||
def _p(name):
|
||||
return os.path.join(run_dir(), name)
|
||||
|
||||
|
||||
def sampler_pfad():
|
||||
return os.environ.get("TP6_SAMPLER", _p("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,Diele OG,Arbeitszimmer,Bad,Klo,Technik OG,Terrasse"
|
||||
).split(",") if r.strip()]
|
||||
|
||||
def markers_pfad():
|
||||
d = run_dir()
|
||||
os.makedirs(d, exist_ok=True)
|
||||
pfad = os.path.join(d, "markers.csv")
|
||||
if not os.path.exists(pfad):
|
||||
with open(pfad, "w") as fh:
|
||||
fh.write("ts_epoch,ts_local,marker\n")
|
||||
return pfad
|
||||
|
||||
# name, label, css-Klasse, in welchen Modi sichtbar
|
||||
BUTTONS = [
|
||||
("CALL_START", "📞 Call verbunden — Messung starten", "start", ("wartet",)),
|
||||
("ME_BAD", "🔴 ICH höre/sehe schlecht", "bad-me", ("messung", "langzeit")),
|
||||
("GELI_BAD", "🟠 GELI meldet: hört/sieht mich schlecht", "bad-geli", ("messung", "langzeit")),
|
||||
("SYNC_OFF", "🎬 Lippen nicht synchron", "bad-me", ("messung", "langzeit")),
|
||||
("OK_AGAIN", "🟢 Wieder gut", "ok", ("messung", "langzeit")),
|
||||
("CAM_OFF", "📷 Kamera aus", "cam", ("messung", "langzeit")),
|
||||
("CAM_ON", "📷 Kamera wieder an", "cam", ("messung", "langzeit")),
|
||||
("WALK", "🚶 Ich gehe jetzt los", "walk", ("langzeit",)),
|
||||
("ARRIVED", "📍 Ich stehe still", "walk", ("langzeit",)),
|
||||
("CALL_END", "📴 Call beendet", "end", ("messung", "langzeit")),
|
||||
]
|
||||
|
||||
DEFAULT_STATE = {
|
||||
"mode": "wartet",
|
||||
"phase": "—",
|
||||
"title": "Kein Treiber",
|
||||
"instruction": "Es läuft kein Messtreiber. Ein Start-Klick würde nichts messen.",
|
||||
"step": 0, "steps": 0, "ends": 0,
|
||||
}
|
||||
|
||||
|
||||
def read_state():
|
||||
try:
|
||||
with open(_p("phase.json")) as fh:
|
||||
st = json.load(fh)
|
||||
st["driver"] = (time.time() - os.path.getmtime(_p("phase.json"))) < 20
|
||||
return st
|
||||
except Exception:
|
||||
st = dict(DEFAULT_STATE)
|
||||
st["driver"] = False
|
||||
return st
|
||||
|
||||
|
||||
def write_marker(name):
|
||||
with open(markers_pfad(), "a") as fh:
|
||||
fh.write("%d,%s,%s\n" % (time.time(), time.strftime("%H:%M:%S"), name))
|
||||
fh.flush()
|
||||
|
||||
|
||||
def last_marker():
|
||||
try:
|
||||
rows = [r.strip() for r in open(markers_pfad()) if r.strip()]
|
||||
if len(rows) < 2:
|
||||
return "—"
|
||||
p = rows[-1].split(",")
|
||||
return "%s (%s)" % (p[2], p[1])
|
||||
except Exception:
|
||||
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_pfad()) 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:
|
||||
rows = [r.strip() for r in open(sampler_pfad()) if r.strip()]
|
||||
if len(rows) < 2:
|
||||
return {}
|
||||
row = dict(zip(rows[0].split(","), rows[-1].split(",")))
|
||||
return {
|
||||
"age": int(time.time() - int(row.get("ts_epoch", "0"))),
|
||||
"cpu": row.get("cpu", ""), "wan_rx": row.get("wan_rx_bps", ""),
|
||||
"wan_tx": row.get("wan_tx_bps", ""), "iface": row.get("sta_iface", ""),
|
||||
"signal": row.get("sta_signal", ""), "tx_rate": row.get("sta_tx_rate", ""),
|
||||
"sta_tx_bps": row.get("sta_tx_bps", ""), "sta_rx_bps": row.get("sta_rx_bps", ""),
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def kill_flows():
|
||||
"""Not-Aus: Saettigungs-Flows sofort beenden."""
|
||||
killed = 0
|
||||
try:
|
||||
pids = [int(p) for p in open(_p("flows.pid")).read().split() if p.strip().isdigit()]
|
||||
except Exception:
|
||||
pids = []
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
killed += 1
|
||||
except Exception:
|
||||
pass
|
||||
open(_p("abort.flag"), "w").close()
|
||||
return killed
|
||||
|
||||
|
||||
# 🔴 Hier stand bis 2026-08-22 eine fest verdrahtete Tabelle — und zwar mit VERTAUSCHTEN
|
||||
# APs (cap-wifi1/2 als ap1 statt ap2). Die Baender stimmten, die Identitaet nicht. Die
|
||||
# cap-wifiN-Nummern vergibt der Manager dynamisch in der Anmeldereihenfolge der CAPs;
|
||||
# jede Konstante dazu ist ein Fehler, der auf den naechsten AP-Reboot wartet und dann
|
||||
# still falsch beschriftet. Deshalb kommt die Zuordnung jetzt vom Geraet.
|
||||
PIN_SH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tp25-pin-s22.sh")
|
||||
_apmap = {"stand": 0.0, "karte": {}}
|
||||
_aplock = threading.Lock()
|
||||
|
||||
|
||||
def ap_map():
|
||||
"""cap-wifiN -> 'ap1 / 5 GHz'. Live vom Manager, 60 s gecacht."""
|
||||
with _aplock:
|
||||
if time.time() - _apmap["stand"] < 60 and _apmap["karte"]:
|
||||
return _apmap["karte"]
|
||||
karte = {}
|
||||
try:
|
||||
out = subprocess.run(["bash", PIN_SH, "map"], capture_output=True, text=True,
|
||||
timeout=20).stdout
|
||||
for zeile in out.splitlines():
|
||||
if "=" not in zeile:
|
||||
continue
|
||||
iface, wert = zeile.strip().split("=", 1)
|
||||
ap, _, band = wert.partition("/")
|
||||
karte[iface] = "%s / %s" % (ap, band.replace("GHz", " GHz"))
|
||||
except Exception:
|
||||
pass
|
||||
with _aplock:
|
||||
if karte:
|
||||
_apmap.update(stand=time.time(), karte=karte)
|
||||
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())
|
||||
# 🔴 Bis 2026-08-22 blieben diese Zahlen NUR im Speicher, fuer die Anzeige.
|
||||
# Nach dem Rundgang fehlte deshalb die halbe Messung: das S22 sitzt per
|
||||
# Automatik immer auf dem Gegen-AP, die Alltagsabdeckung zeigt das iPhone
|
||||
# -- und genau dessen Zeitreihe war weg. Nachtraeglich liess sich nur das
|
||||
# Roaming-Log am Manager retten: Ereignisse, keine Kurve.
|
||||
mitschreiben(teile)
|
||||
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 mitschreiben(teile):
|
||||
"""Beide Telefone im 2-s-Takt in den Lauf schreiben: iface + Signal je Geraet."""
|
||||
try:
|
||||
pfad = _p("beide-telefone.csv")
|
||||
frisch = not os.path.exists(pfad) or os.path.getsize(pfad) == 0
|
||||
with open(pfad, "a") as fh:
|
||||
if frisch:
|
||||
fh.write("ts_epoch,ts_local,iphone_iface,iphone_sig,s22_iface,s22_sig\n")
|
||||
fh.write("%d,%s,%s\n" % (time.time(), time.strftime("%H:%M:%S"), ",".join(teile)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
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}
|
||||
|
||||
|
||||
def pin_lesen():
|
||||
if _pin["busy"] or time.time() - _pin["stand"] < 20:
|
||||
return _pin["ziel"]
|
||||
try:
|
||||
_pin["ziel"] = subprocess.run(["bash", PIN_SH, "which"], capture_output=True,
|
||||
text=True, timeout=20).stdout.strip() or None
|
||||
_pin["stand"] = time.time()
|
||||
except Exception:
|
||||
pass
|
||||
return _pin["ziel"]
|
||||
|
||||
|
||||
def pin_setzen(ziel):
|
||||
def lauf():
|
||||
try:
|
||||
r = subprocess.run(["bash", PIN_SH, ziel], capture_output=True, text=True, timeout=60)
|
||||
_pin["msg"] = (r.stdout.strip().splitlines() or [""])[0]
|
||||
except Exception as e:
|
||||
_pin["msg"] = "Fehler: %s" % e
|
||||
finally:
|
||||
_pin["busy"] = False
|
||||
_pin["stand"] = 0.0
|
||||
pin_lesen()
|
||||
_pin["busy"] = True
|
||||
_pin["msg"] = "schaltet um …"
|
||||
# Der Klick gehoert in dieselbe Zeitachse wie alles andere — sonst laesst sich hinterher
|
||||
# nicht sagen, welche Messpunkte zu welchem AP gehoeren.
|
||||
write_marker("PIN=%s" % ziel)
|
||||
threading.Thread(target=lauf, daemon=True).start()
|
||||
|
||||
|
||||
def lokale_adressen():
|
||||
"""Alle IPv4-Adressen dieses Rechners ausser Loopback/Link-Local, als (iface, ip)."""
|
||||
treffer = []
|
||||
try:
|
||||
out = subprocess.run(["ifconfig"], capture_output=True, text=True, timeout=10).stdout
|
||||
iface = "?"
|
||||
for zeile in out.splitlines():
|
||||
if zeile and not zeile[0].isspace():
|
||||
iface = zeile.split(":", 1)[0]
|
||||
elif zeile.strip().startswith("inet "):
|
||||
ip = zeile.split()[1]
|
||||
if not ip.startswith(("127.", "169.254.")):
|
||||
treffer.append((iface, ip))
|
||||
except Exception:
|
||||
pass
|
||||
return treffer
|
||||
|
||||
|
||||
def fmt_bps(v):
|
||||
try:
|
||||
n = int(v)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
return "%.1f Mbit" % (n / 1e6) if n >= 1e6 else "%d kbit" % (n // 1000)
|
||||
|
||||
|
||||
def render(st):
|
||||
w = sampler_tail()
|
||||
rest = max(0, int(st.get("ends", 0) - time.time())) if st.get("ends") else 0
|
||||
mode = st.get("mode", "wartet")
|
||||
|
||||
banner = ""
|
||||
alarm = False # nur DIESER Zustand loest Ton + Vibration aus, nicht jeder Banner
|
||||
# 🔴 Der wichtigste Alarm der Seite, seit es den AP-Pin gibt (2026-08-22): ein vom Pin
|
||||
# ABGEWIESENER Verbindungsversuch ist in den Ping-Daten nicht von einem Funkloch zu
|
||||
# unterscheiden -- und der Manager protokolliert die Abweisung auf info-Ebene nicht.
|
||||
# Android quittiert sie mit "Verbindung fehlgeschlagen" und WAEHLT DAS NETZ AB, danach
|
||||
# misst der restliche Rundgang nur noch Stille. Deshalb wird "haengt nirgends" hier
|
||||
# nicht klein in der WLAN-Zeile gezeigt, sondern schreit.
|
||||
if pin_lesen() in ("ap1", "ap2") and w.get("iface") == "absent" and w.get("age", 999) < 30:
|
||||
alarm = True
|
||||
banner = ('<div class="banner warn">📵 S22 HÄNGT NIRGENDS — Display an und wieder auf '
|
||||
'dirtyvader-main verbinden. Der Pin wirft es vom gesperrten AP; Android '
|
||||
'wählt das Netz dann von selbst ab. <b>Nicht weitergehen</b>, sonst misst '
|
||||
'du Stille statt Abdeckung.</div>')
|
||||
elif not st.get("driver"):
|
||||
banner = ('<div class="banner done">ℹ️ Keine Ping-Aufzeichnung — Räume markieren und '
|
||||
'AP-Pin gehen trotzdem. Für die Messkurve <b>tp25-walk-messung.sh</b> starten.</div>')
|
||||
elif mode == "langzeit":
|
||||
banner = ('<div class="banner done">✅ MESSUNG ABGESCHLOSSEN — die Zahlen liegen vor. '
|
||||
'Ab jetzt läuft nur noch die Langzeit-Aufzeichnung.</div>')
|
||||
|
||||
prog = ""
|
||||
if st.get("steps"):
|
||||
prog = '<div class="steps">Schritt %d von %d</div>' % (st.get("step", 0), st["steps"])
|
||||
|
||||
countdown = ('<div class="cd" id="cd" data-ends="%d">noch ~%d s</div>' % (st.get("ends", 0), rest)
|
||||
if rest else '<div class="cd" id="cd" data-ends="0"></div>')
|
||||
|
||||
wlan = "—"
|
||||
if w.get("iface"):
|
||||
wlan = "%s · %s dBm · tx %s / rx %s" % (
|
||||
ap_map().get(w["iface"], w["iface"]), w.get("signal", "?"),
|
||||
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.
|
||||
# 🔴 "beide angemeldet" ist NICHT das Kriterium -- der Messpunkt taugt nur, wenn die
|
||||
# beiden Telefone auf VERSCHIEDENEN APs sitzen. Am 2026-08-22 meldete die Seite
|
||||
# "✅ beide angemeldet, auf verschiedenen APs", waehrend beide auf ap2 hingen: ein
|
||||
# gruenes Licht fuer eine Messung, die es gar nicht gab. Wieder derselbe Fehler --
|
||||
# die Anzeige geprueft statt der Wirkung.
|
||||
getrennt = bool(ip_ap and s22_ap and ip_ap != s22_ap)
|
||||
bereit = bool(frisch and getrennt)
|
||||
gleich = bool(frisch and ip_ap and s22_ap and ip_ap == 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: <b>%s</b> %s dBm" % (mac_name, ap, sig)) if ap else ("%s: <b>nirgends</b>" % mac_name)
|
||||
|
||||
doppel = "%s · %s" % (z(ip_ap, _live["iphone_sig"], "iPhone"),
|
||||
z(s22_ap, _live["s22_sig"], "S22"))
|
||||
|
||||
if _pin["busy"]:
|
||||
pinstatus = "⏳ %s<br>%s" % (_pin["msg"], doppel)
|
||||
elif _auto["aufgegeben"]:
|
||||
pinstatus = ("🚫 S22 bekommt <b>%s</b> hier nicht — <b>das ist das Messergebnis</b>, "
|
||||
"Raum markieren und weitergehen.<br>%s" % (_auto["aufgegeben"], doppel))
|
||||
elif bereit:
|
||||
pinstatus = "✅ beide angemeldet, <b>verschiedene APs</b> — Messpunkt zählt<br>%s" % doppel
|
||||
elif gleich and _auto["an"]:
|
||||
pinstatus = ("⏳ beide auf <b>%s</b> — Gegen-AP wird gerade nachgezogen, "
|
||||
"noch nicht weitergehen<br>%s" % (ip_ap, doppel))
|
||||
elif gleich:
|
||||
pinstatus = ("⚠️ beide auf <b>%s</b> — <b>kein Gegen-AP-Messpunkt.</b> "
|
||||
"Automatik einschalten oder S22 von Hand pinnen.<br>%s" % (ip_ap, doppel))
|
||||
elif wartet:
|
||||
pinstatus = "⏳ S22 sucht <b>%s</b> …<br>%s" % (ziel or "?", doppel)
|
||||
elif ziel in ("ap1", "ap2"):
|
||||
pinstatus = "S22 gepinnt auf <b>%s</b><br>%s" % (ziel, doppel)
|
||||
elif ziel == "off":
|
||||
pinstatus = "kein Pin — S22 roamt frei<br>%s" % doppel
|
||||
else:
|
||||
pinstatus = "Zustand unbekannt (Manager nicht erreichbar?)"
|
||||
pinkarte = (
|
||||
'<div class="card"><span id="sig" data-alarm="%d" data-busy="%d" data-pin="%s" '
|
||||
'data-bereit="%d"></span>'
|
||||
'<div class="phase">AP-Pin S22</div>'
|
||||
'<div class="inst" id="pininst">%s</div><div class="raster">'
|
||||
'<form method="post" action="/pin?ap=ap1" onsubmit="return sendpin(event,\'ap1\')">'
|
||||
'<button class="raum%s" type="submit">ap1</button></form>'
|
||||
'<form method="post" action="/pin?ap=ap2" onsubmit="return sendpin(event,\'ap2\')">'
|
||||
'<button class="raum%s" type="submit">ap2</button></form>'
|
||||
'</div>'
|
||||
'<form method="post" action="/auto" onsubmit="return sendauto(event)">'
|
||||
'<button class="%s" style="margin-top:8px" type="submit">%s</button></form>'
|
||||
'<form method="post" action="/pin?ap=off" onsubmit="return sendpin(event,\'off\')">'
|
||||
'<button class="cam" style="margin-top:8px" type="submit">Pin lösen</button></form>'
|
||||
'</div>' % (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",
|
||||
)
|
||||
)
|
||||
|
||||
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 True:
|
||||
aktuell = ("<b>%s</b> · seit %d:%02d" % (raum, seit // 60, seit % 60)) if raum else "— noch kein Raum gewählt —"
|
||||
kacheln = "".join(
|
||||
'<form method="post" action="/m?type=RAUM%%3D%s" onsubmit="return send(event,\'RAUM=%s\')">'
|
||||
'<button class="raum%s" type="submit">%s</button></form>'
|
||||
% (r.replace(" ", "%20"), r, " hier" if r == raum else "", r) for r in ROOMS)
|
||||
raumzeile = ('<div class="card"><div class="phase">Wo bist du gerade?</div>'
|
||||
'<div class="inst">%s</div><div class="raster">%s</div></div>' % (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(
|
||||
'<form method="post" action="/m?type=%s" onsubmit="return send(event,\'%s\')">'
|
||||
'<button class="%s" type="submit">%s</button></form>' % (n, n, c, l)
|
||||
for n, l, c, modes in BUTTONS if mode in modes
|
||||
) if st.get("driver") else ""
|
||||
notaus = ('<form method="post" action="/m?type=ABORT" onsubmit="return send(event,\'ABORT\')">'
|
||||
'<button class="abort" type="submit">⛔ LAST SOFORT STOPPEN</button></form>'
|
||||
) if st.get("driver") else ""
|
||||
# 🔴 Der Stopp-Knopf. Er beendet die AUFZEICHNUNG (stop.flag -> tp25-walk-messung.sh),
|
||||
# nicht nur eine Marke. Vorher gab es ihn nicht, obwohl der Anleitungstext ihn nannte:
|
||||
# am 2026-08-22 drueckte Christoph "Call beendet" und den Not-Aus, beide schrieben nur
|
||||
# Marken, und der Core pingte danach noch minutenlang weiter.
|
||||
if st.get("driver"):
|
||||
notaus = ('<form method="post" action="/stop" onsubmit="return sendstop(event)">'
|
||||
'<button class="end" type="submit">⏹ Aufzeichnung stoppen</button></form>'
|
||||
) + notaus
|
||||
|
||||
# 🚶 Gehen/Stehen braucht keinen Treiber -- das sind reine Zeitmarken fuer die Auswertung.
|
||||
btns = ('<form method="post" action="/m?type=WALK" onsubmit="return send(event,\'WALK\')">'
|
||||
'<button class="walk" type="submit">🚶 Ich gehe jetzt los</button></form>'
|
||||
'<form method="post" action="/m?type=ARRIVED" onsubmit="return send(event,\'ARRIVED\')">'
|
||||
'<button class="walk" type="submit">📍 Ich stehe still</button></form>') + 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, notaus=notaus,
|
||||
)
|
||||
|
||||
|
||||
PAGE = """<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv="refresh" content="8" id="metaref">
|
||||
<title>TP6 — Call-Konsole</title><style>
|
||||
:root{{color-scheme:dark}}
|
||||
body{{margin:0;padding:14px;background:#14161a;color:#e8eaed;
|
||||
font:16px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}}
|
||||
.banner{{padding:12px 14px;border-radius:10px;font-weight:700;margin-bottom:12px}}
|
||||
.warn{{background:#7f1d1d;color:#fff}} .done{{background:#14532d;color:#d1fae5}}
|
||||
.card{{background:#1c1f26;border:1px solid #2a2f3a;border-radius:12px;padding:14px;margin-bottom:12px}}
|
||||
.phase{{font-size:13px;color:#7dd3fc;text-transform:uppercase;letter-spacing:.06em}}
|
||||
h2{{margin:4px 0 8px;font-size:21px}}
|
||||
.inst{{font-size:17px;color:#e8eaed}}
|
||||
.cd{{margin-top:10px;font:600 15px ui-monospace,Menlo,monospace;color:#fbbf24}}
|
||||
.steps{{margin-top:6px;font-size:13px;color:#9aa4b2}}
|
||||
.live{{background:#1c1f26;border:1px solid #2a2f3a;border-radius:10px;padding:10px 12px;
|
||||
margin-bottom:14px;font:13px/1.7 ui-monospace,Menlo,monospace;white-space:pre-wrap;color:#9aa4b2}}
|
||||
button{{width:100%;margin:0 0 10px;padding:20px 16px;font-size:19px;font-weight:600;border:none;
|
||||
border-radius:12px;background:#262b34;color:#e8eaed;-webkit-tap-highlight-color:transparent}}
|
||||
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}}
|
||||
</style>
|
||||
{banner}
|
||||
<div class="card">
|
||||
<div class="phase">{phase}</div>
|
||||
<h2>{title}</h2>
|
||||
<div class="inst">{instruction}</div>
|
||||
{countdown}
|
||||
{prog}
|
||||
</div>
|
||||
{pinkarte}
|
||||
{raumzeile}
|
||||
<div class="live">WLAN {wlan}
|
||||
WAN {wan}
|
||||
Marker {last}</div>
|
||||
{buttons}
|
||||
{notaus}
|
||||
<script>
|
||||
// JS ist Komfort, keine Voraussetzung: die Buttons sind Formulare, die Anleitung kommt
|
||||
// notfalls per <meta refresh>. Laeuft JS, ersetzen wir das durch leises Nachladen.
|
||||
window.onerror = function(m, s, l, c){{
|
||||
try{{ fetch('/jserr?msg=' + encodeURIComponent(m + ' @' + l + ':' + c), {{method:'POST'}}); }}catch(e){{}}
|
||||
}};
|
||||
function send(ev, name){{
|
||||
try{{
|
||||
ev.preventDefault(); unlock();
|
||||
fetch('/m?type=' + encodeURIComponent(name), {{method:'POST'}}).then(refresh);
|
||||
var b = ev.target.querySelector('button');
|
||||
if(b){{ b.style.filter='brightness(1.6)'; setTimeout(function(){{b.style.filter='';}},400); }}
|
||||
return false;
|
||||
}}catch(e){{ return true; }} // im Zweifel den normalen Form-POST durchlassen
|
||||
}}
|
||||
// 🔊 Signalton. Browser lassen Audio erst nach einer echten Nutzergeste zu -- der erste
|
||||
// Button-Druck schaltet ihn frei, deshalb haengt unlock() an jedem Klick. Auf dem S22
|
||||
// kommt Vibration dazu; iOS kennt navigator.vibrate nicht, das faellt still aus.
|
||||
var AC = null;
|
||||
function unlock(){{
|
||||
try{{
|
||||
if(!AC) AC = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if(AC.state === 'suspended') AC.resume();
|
||||
}}catch(e){{}}
|
||||
}}
|
||||
function ton(hz, ms, wann){{
|
||||
try{{
|
||||
if(!AC) return;
|
||||
var o = AC.createOscillator(), g = AC.createGain();
|
||||
o.type = 'sine'; o.frequency.value = hz;
|
||||
g.gain.setValueAtTime(0.001, AC.currentTime + wann);
|
||||
g.gain.exponentialRampToValueAtTime(0.35, AC.currentTime + wann + 0.02);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, AC.currentTime + wann + ms/1000);
|
||||
o.connect(g); g.connect(AC.destination);
|
||||
o.start(AC.currentTime + wann); o.stop(AC.currentTime + wann + ms/1000 + 0.05);
|
||||
}}catch(e){{}}
|
||||
}}
|
||||
function alarmton(){{ ton(880,180,0); ton(660,180,0.24); ton(880,260,0.48);
|
||||
try{{ navigator.vibrate && navigator.vibrate([200,120,200,120,320]); }}catch(e){{}} }}
|
||||
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;
|
||||
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
|
||||
// waehrend des Gehens ist kein Alarm.
|
||||
if(!alarmTimer) alarmTimer = setInterval(alarmton, 12000);
|
||||
}}
|
||||
if(!alarm && alarmTimer){{ clearInterval(alarmTimer); alarmTimer = null; }}
|
||||
if(!busy && letzterBusy) okton(); // Umschalten fertig
|
||||
letzterAlarm = alarm ? 1 : 0; letzterBusy = busy ? 1 : 0;
|
||||
}}
|
||||
|
||||
function sendstop(ev){{
|
||||
try{{
|
||||
ev.preventDefault(); unlock(); okton();
|
||||
var inst = document.getElementById('pininst');
|
||||
fetch('/stop', {{method:'POST'}}).catch(function(){{}});
|
||||
var b = ev.target.querySelector('button');
|
||||
if(b){{ b.textContent = '⏹ gestoppt — Aufzeichnung endet'; b.disabled = true; }}
|
||||
setTimeout(refresh, 1500);
|
||||
return false;
|
||||
}}catch(e){{ return true; }}
|
||||
}}
|
||||
function sendauto(ev){{
|
||||
try{{
|
||||
ev.preventDefault(); unlock(); okton();
|
||||
fetch('/auto', {{method:'POST'}}).catch(function(){{}});
|
||||
setTimeout(refresh, 400); setTimeout(refresh, 3000);
|
||||
return false;
|
||||
}}catch(e){{ return true; }}
|
||||
}}
|
||||
function sendpin(ev, ap){{
|
||||
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 <b>' + ap + '</b> …<br>' +
|
||||
'<b>Dieses Telefon fliegt jetzt aus dem WLAN.</b> Wenn die Seite haengt: in den ' +
|
||||
'WLAN-Einstellungen wieder auf <b>dirtyvader-main</b> 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);
|
||||
return false;
|
||||
}}catch(e){{ return true; }}
|
||||
}}
|
||||
function refresh(){{
|
||||
fetch('/').then(function(r){{return r.text();}}).then(function(html){{
|
||||
var d = document.createElement('html'); d.innerHTML = html;
|
||||
var karten = d.querySelectorAll('.card'), alte = document.querySelectorAll('.card');
|
||||
for(var i = 0; i < karten.length && i < alte.length; i++) alte[i].replaceWith(karten[i]);
|
||||
var nb = d.querySelector('.live'), ob = document.querySelector('.live');
|
||||
if(nb && ob) ob.replaceWith(nb);
|
||||
var nban = d.querySelector('.banner'), oban = document.querySelector('.banner');
|
||||
if(nban && oban) oban.replaceWith(nban); else if(nban && !oban) document.body.prepend(nban);
|
||||
if(!nban && oban) oban.remove();
|
||||
tick(); signale();
|
||||
}}).catch(function(){{}});
|
||||
}}
|
||||
function tick(){{
|
||||
var el = document.getElementById('cd'); if(!el) return;
|
||||
var ends = parseInt(el.dataset.ends || '0', 10); if(!ends) {{ el.textContent=''; return; }}
|
||||
var r = Math.max(0, ends - Math.floor(Date.now()/1000));
|
||||
el.textContent = r ? 'noch ~' + r + ' s' : 'gleich weiter …';
|
||||
}}
|
||||
var mr = document.getElementById('metaref'); if(mr) mr.remove(); // JS lebt -> kein Vollreload
|
||||
setInterval(tick, 1000); setInterval(refresh, 3000);
|
||||
document.addEventListener('click', unlock, {{once:false}});
|
||||
signale();
|
||||
// Bildschirm wachhalten: gesperrtes iPad = kein Not-Aus.
|
||||
async function wake(){{ try{{ if('wakeLock' in navigator) await navigator.wakeLock.request('screen'); }}catch(e){{}} }}
|
||||
document.addEventListener('visibilitychange', function(){{ if(!document.hidden) wake(); }});
|
||||
wake();
|
||||
</script>"""
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def _send(self, code, body, ctype="application/json"):
|
||||
raw = body.encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", ctype + "; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def _back(self):
|
||||
self.send_response(303)
|
||||
self.send_header("Location", "/")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.startswith("/status"):
|
||||
st = read_state()
|
||||
st["sampler"] = sampler_tail()
|
||||
st["last"] = last_marker()
|
||||
self._send(200, json.dumps(st))
|
||||
elif self.path in ("/", "/index.html"):
|
||||
self._send(200, render(read_state()), "text/html")
|
||||
else:
|
||||
self._send(404, "{}")
|
||||
|
||||
def do_POST(self):
|
||||
if self.path.startswith("/jserr"):
|
||||
with open(_p("jserrors.log"), "a") as fh:
|
||||
fh.write("%s %s\n" % (time.strftime("%H:%M:%S"), self.path.split("msg=", 1)[-1]))
|
||||
self._send(200, "{}")
|
||||
return
|
||||
if self.path.startswith("/stop"):
|
||||
# Der Knopf, den der Anleitungstext seit jeher versprach und den es nie gab.
|
||||
open(_p("stop.flag"), "w").close()
|
||||
write_marker("STOP")
|
||||
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")
|
||||
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()
|
||||
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}))
|
||||
return
|
||||
if self.path.startswith("/m?"):
|
||||
# 🔴 Erst dekodieren, dann NICHT hochschreiben: "RAUM%3DDiele%20OG" wurde sonst zu
|
||||
# "RAUM%3DDIELE%20OG" — der Marker war weder lesbar noch als Raum erkennbar.
|
||||
name = urllib.parse.unquote(self.path.split("type=", 1)[1].split("&")[0])
|
||||
if not name.startswith("RAUM="):
|
||||
name = name.upper()
|
||||
if name == "ABORT":
|
||||
write_marker("ABORT_killed=%d" % kill_flows())
|
||||
else:
|
||||
if name == "CALL_START":
|
||||
open(_p("go.flag"), "w").close()
|
||||
write_marker(name)
|
||||
if "text/html" in (self.headers.get("Accept") or ""):
|
||||
self._back()
|
||||
else:
|
||||
self._send(200, json.dumps({"ok": True}))
|
||||
else:
|
||||
self._send(404, "{}")
|
||||
|
||||
def log_message(self, *_a):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 🔴 Hier stand eine HARTKODIERTE IP (10.40.10.183) -- die des MacBook Air. Auf dem Mac
|
||||
# mini ist es 10.40.10.82, und am 2026-08-22 wurde die falsche Adresse ans Telefon
|
||||
# weitergegeben: "geht nicht" bei einem Server, der einwandfrei auf 0.0.0.0 lauschte.
|
||||
# Eine Adresse, die man ansagt, ohne sie gelesen zu haben, ist keine Adresse.
|
||||
# Deshalb kommt sie jetzt vom Interface -- und zwar ALLE, mit Kennzeichnung.
|
||||
print("Konsole lokal: http://localhost:%d/" % PORT)
|
||||
for name, ip in lokale_adressen():
|
||||
print(" vom Telefon: http://%s:%d/ (%s)" % (ip, PORT, name))
|
||||
if not lokale_adressen():
|
||||
print(" ⚠️ keine LAN-Adresse gefunden -- vom Telefon nicht erreichbar")
|
||||
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
|
||||
# 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()
|
||||
Reference in New Issue
Block a user