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-<name>.csv (AP/Signal) + Ping-Verlust aus walk-ping-<name>.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 <noreply@anthropic.com>
This commit is contained in:
70
docs/handoff-20260904-tp25-konsole-n-clients.md
Normal file
70
docs/handoff-20260904-tp25-konsole-n-clients.md
Normal file
@@ -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-<name>.csv` (`sta_iface`, `sta_signal`) und den **juengsten Ping-Verlust** aus
|
||||
`walk-ping-<name>.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 → „<name> HAENGT NIRGENDS — nicht weitergehen" (Nachfolger des alten S22-Alarms).
|
||||
- **Pin-UI** (Karte „Pinnen (Multi-Pin)"): je Client ein `<select>` mit `frei` / `apX/5GHz` /
|
||||
`apX/2,4GHz` (Ziele aus `ap_map()` abgeleitet) plus drei Aktionen:
|
||||
- **„Pin setzen"** → baut `TP25_PINSPEC="name|mac|ap|band;…"` aus den Nicht-`frei`-Auswahlen und
|
||||
ruft `bash docs/tp25-pin-multi.sh set` im Thread.
|
||||
- **„Pin loesen"** → `bash docs/tp25-pin-multi.sh off`.
|
||||
- **„verify"** → `bash docs/tp25-pin-multi.sh verify`, Ausgabe als Statuszeile.
|
||||
Neuer Endpunkt `POST /pinmulti` (analog zum alten `/pin`).
|
||||
- **Entfernt:** `IPHONE_MAC`/`S22_MAC`, `_live`/`_auto`, `entscheide()`, `pin_lesen()`/
|
||||
`pin_setzen()` (via `tp25-pin-s22.sh`), die ap1/ap2/auto-Buttons und der `PIN_SH`-Aufruf.
|
||||
`tp25-pin-s22.sh` bleibt als Skript erhalten (liefert `map`), wird aber von der Konsole nicht
|
||||
mehr zum Pinnen aufgerufen.
|
||||
|
||||
`docs/tp25-runde.sh`:
|
||||
- `aufraeumen()` loest jetzt `tp25-pin-multi.sh off` statt `tp25-pin-s22.sh off` (Kommentar
|
||||
angepasst: die Konsole pinnt nicht mehr automatisch).
|
||||
|
||||
## Bedienung (Messablauf)
|
||||
|
||||
```
|
||||
bash docs/tp25-runde.sh [name] # Konsole (3031) + Messung, Adresse fuer das Telefon
|
||||
```
|
||||
|
||||
1. Am Telefon die Konsole oeffnen, einmal irgendwo tippen (schaltet den Signalton frei).
|
||||
2. In der Konsole **pinnen**: je Client AP/Band waehlen, „Pin setzen". Clients ohne Ziel bleiben
|
||||
frei (das Skript raeumt vorher alle `TP25-PINM-*`-Regeln ab).
|
||||
3. Losgehen. 🚶 beim Losgehen, 📍 beim Stehenbleiben, Raum antippen. Alarm = ein gepinnter Client
|
||||
haengt nirgends.
|
||||
4. „⏹ Aufzeichnung stoppen" (oder Ctrl-C im Terminal). Der Wrapper loest den Multi-Pin beim Beenden.
|
||||
|
||||
Auswertung: `python3 docs/tp25-gehweg-auswertung.py --dir docs/messdaten/tp25-runde-<name>`
|
||||
|
||||
## Was noch offen ist (braucht die Geraete vor Ort)
|
||||
|
||||
- **Live-Test mit N Geraeten im Netz** (Plan-Verifikation Schritt 3): `TP25_PHONES="…" bash
|
||||
docs/tp25-runde.sh test` → N Karten sichtbar, pinnen → `bash docs/tp25-pin-multi.sh verify` zeigt
|
||||
die Regeln, gehen/Raeume/stoppen → Auswertung liefert die Raumtabelle mit N Geraeten.
|
||||
- **Aufraeumen** (Schritt 4): nach dem Lauf `bash docs/tp25-pin-multi.sh off` — keine
|
||||
`TP25-PINM-*`-Regeln am Manager zurueck.
|
||||
|
||||
Statisch verifiziert (ohne Geraete): AST-Parse ok, keine stale Referenzen auf die alte Automatik,
|
||||
Smoke-Render mit 2 Test-Clients ok, `bash -n` auf allen drei Shell-Skripten ok.
|
||||
|
||||
## Gotchas (unveraendert, aus CLAUDE.md)
|
||||
|
||||
- **`cap-wifiN`-Karte wandert bei jedem Rejoin** — `tp25-walk-messung.sh` friert sie als
|
||||
`ap-map.txt` im Messlauf ein; die Konsole beschriftet daraus.
|
||||
- **Registration-Table nur am Manager** — die Sampler (`tp6-monitor-core.sh`) fragen den Core ab,
|
||||
nicht die CAPs.
|
||||
- **`sshpass -e`, nie `-p`** — gilt weiter fuer alle Skripte, die den Core ansprechen.
|
||||
@@ -27,10 +27,16 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
|
||||
|
||||
## Ist-Stand (Kurz)
|
||||
|
||||
**Stand 2026-09-02 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben.**
|
||||
**Stand 2026-09-04 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben.**
|
||||
**Compliance 140/140** (nach TP42, 2026-09-02).
|
||||
*(Die Fassung vom 2026-08-16 stand bis zum 18. hier und war an vier Stellen überholt. Wer diesen Block
|
||||
liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)*
|
||||
- ✅ **TP25 Multi-Pin-Konsole (2026-09-04):** die Klick-Konsole (`docs/tp6-marker/server.py`) ist auf
|
||||
**N Clients** generalisiert und hat eine **Pin-UI** bekommen. Sie liest die Client-Liste aus
|
||||
`geraete.txt` (schreibt `tp25-walk-messung.sh`), zeigt je Client AP/Signal/Ping-Verlust aus
|
||||
`sampler-<name>.csv`/`walk-ping-<name>.csv` (kein eigenes SSH mehr) und pinnt N Clients band-genau
|
||||
über `tp25-pin-multi.sh` direkt in der Konsole. Die alte 2-Client-Gegen-AP-Automatik ist entfernt.
|
||||
Handoff: [`docs/handoff-20260904-tp25-konsole-n-clients.md`](handoff-20260904-tp25-konsole-n-clients.md).
|
||||
- ✅ **TP25 Fund E GESCHLOSSEN (2026-09-02):** die vorgemerkte Abschlussmessung ist gelaufen —
|
||||
`reauthenticating` am 09-02 nur noch **4 in 8,6 h** (alle iPhone `F2:62`, isolierte Einzel, 0 Ausfälle)
|
||||
gegen Baseline 93 bzw. Hebel-1 **19/h**. Verlauf **218 → 40 → 4**; Vorbedingung geprüft (Core-Uptime
|
||||
@@ -506,7 +512,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
|
||||
> | TP20.1 | 🔜 | Vaultwarden → Platform-PG | — |
|
||||
> | TP23 | ✅ | DNS-Policy (Stufe 1+2 ✅) | 2026-08-26 |
|
||||
> | TP24 | 🔜 | DC-USV | — |
|
||||
> | TP25 | ⏳ | WLAN-Stabilisierung (Kern ✅) — **Fund E ✅ geschlossen 09-02: 7.24.1 war die Ursache, nicht `ft-over-ds` (4/8,6 h, 0 Ausfälle)**; offen nur ap2 unterperformt (2 m/−80, In-situ) | 2026-09-02 |
|
||||
> | TP25 | ⏳ | WLAN-Stabilisierung (Kern ✅) — **Fund E ✅ geschlossen 09-02: 7.24.1 war die Ursache, nicht `ft-over-ds` (4/8,6 h, 0 Ausfälle)**; **Konsole auf N Clients + Pin-UI ✅ 09-04**; offen nur ap2 unterperformt (2 m/−80, In-situ) | 2026-09-04 |
|
||||
> | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 |
|
||||
> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — |
|
||||
> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — |
|
||||
@@ -1858,16 +1864,20 @@ während eines Calls deployen, `/system backup` je AP vorher.
|
||||
- 🔴 **Ein vom Pin abgewiesener Verbindungsversuch ist in Ping-Daten nicht von einem Funkloch zu
|
||||
unterscheiden** — und der Manager protokolliert ihn auf `info`-Ebene **gar nicht**. Android quittiert
|
||||
die Abweisung mit „Verbindung fehlgeschlagen" und **wählt das Netz ab**; danach misst der Rest des
|
||||
Rundgangs Stille. Die Klick-Konsole schlägt deshalb Alarm, sobald das gepinnte Gerät nirgends hängt,
|
||||
und schreibt nach 45 s `UNERREICHBAR=apX` — denn „der Gegen-AP trägt hier nicht" **ist das Messergebnis**.
|
||||
- 📐 **Eine Runde, zwei Messpunkte** (Christophs Aufbau 2026-08-22): das frei roamende iPhone zeigt, was
|
||||
ein normaler Client wählt — interessant ist der **andere**. Das S22 wird per `access-list` auf den
|
||||
Gegen-AP gezwungen ([`tp25-pin-s22.sh`](tp25-pin-s22.sh)). Gute Referenz + schlechte Sonde = die
|
||||
Abdeckung steht, die Rückfallebene nicht. Memory `ref_gegen_ap_messung`.
|
||||
Rundgangs Stille. Die Klick-Konsole schlägt deshalb Alarm, sobald ein **gepinnter** Client nirgends
|
||||
hängt (`sta_iface=absent` in `sampler-<name>.csv`) — denn „der Gegen-AP trägt hier nicht" **ist das
|
||||
Messergebnis**.
|
||||
- 📐 **Eine Runde, N Messpunkte** (seit 2026-09-04): die frei roamenden Clients zeigen, was ein normaler
|
||||
Client wählt — interessant sind die **gepinnten**. Jeder Client wird **band-genau** auf einen AP
|
||||
gezwungen ([`tp25-pin-multi.sh`](tp25-pin-multi.sh), `TP25_PINSPEC="name|mac|ap|band;…"`), direkt aus
|
||||
der Klick-Konsole. Gute Referenz + schlechte Sonde = die Abdeckung steht, die Rückfallebene nicht.
|
||||
Memory `ref_gegen_ap_messung`. *(Die alte 2-Client-`access-list`-Automatik über `tp25-pin-s22.sh` ist
|
||||
mit dem Multi-Pin-Umbau entfallen; `tp25-pin-s22.sh` liefert nur noch die `map`.)*
|
||||
- ▶️ **Start ohne Werkzeugkette: `bash docs/tp25-runde.sh [name]`** — legt den Lauf unter
|
||||
`docs/messdaten/tp25-runde-<name>/` an, friert die AP-Karte ein, startet Konsole (Port 3031) und
|
||||
Messung, sagt die **vom Interface gelesene** Adresse fürs Telefon an und löst beim Beenden den Pin.
|
||||
Auswertung: `tp25-gehweg-auswertung.py --dir …`.
|
||||
Messung, sagt die **vom Interface gelesene** Adresse fürs Telefon an. In der Konsole: **pinnen**
|
||||
(N Clients je AP/Band) → **gehen** (🚶/📍) → **Räume antippen** → **stoppen**; beim Beenden löst der
|
||||
Wrapper den Multi-Pin. Auswertung: `tp25-gehweg-auswertung.py --dir …`.
|
||||
- ℹ️ **Und das alles passiert TROTZ aktivem 802.11k/v und FT** (`st-main` mit `rrm=yes`/`wnm=yes`,
|
||||
`ft-over-ds=yes`, live geprüft). Die Merkregel `ref_ft_is_not_roaming` („11k/v fehlen") ist
|
||||
**historisch**. Bei −48…−55 dBm kostet eine Übergabe 0–2,6 s, meist unter einer halben Sekunde —
|
||||
|
||||
@@ -19,9 +19,9 @@ cd "$HERE"
|
||||
|
||||
NAME="${1:-$(date +%Y%m%d-%H%M)}"
|
||||
export TP6_RUN_DIR="$HERE/docs/messdaten/tp25-runde-$NAME"
|
||||
export TP25_PHONE_IP="${TP25_PHONE_IP:-10.40.10.178}" # S22 -- wird gepingt
|
||||
export TP25_PHONE_MAC="${TP25_PHONE_MAC:-2A:0D:B0:5D:AD:A7}"
|
||||
export TP25_IPHONE_MAC="${TP25_IPHONE_MAC:-F2:62:4C:E1:AF:09}"
|
||||
# N-Geraete-Liste (name|ip|mac;...). Default = die bisherigen zwei, damit alte Aufrufe
|
||||
# unveraendert weiterlaufen. Wird an tp25-walk-messung.sh durchgereicht.
|
||||
export TP25_PHONES="${TP25_PHONES:-iphone|10.40.10.196|F2:62:4C:E1:AF:09;s22|10.40.10.178|2A:0D:B0:5D:AD:A7}"
|
||||
|
||||
mkdir -p "$TP6_RUN_DIR"
|
||||
|
||||
@@ -36,14 +36,6 @@ else
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 🔴 Gegen-AP-Automatik gleich einschalten. Sie war bisher ein Klick auf der Konsole -- und
|
||||
# wer ihn vergisst, geht die ganze Runde mit beiden Telefonen auf DEMSELBEN AP und merkt
|
||||
# es erst bei der Auswertung (im Test am 2026-08-22 genau so passiert). Abschalten geht
|
||||
# weiter jederzeit ueber den Knopf.
|
||||
curl -s -X POST --max-time 10 "http://127.0.0.1:3031/auto?an=1" >/dev/null 2>&1 \
|
||||
&& echo "Gegen-AP-Automatik: EIN" \
|
||||
|| echo "⚠️ Automatik konnte nicht eingeschaltet werden -- auf der Konsole nachholen!"
|
||||
|
||||
echo
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " Konsole am Telefon oeffnen:"
|
||||
@@ -53,8 +45,8 @@ ifconfig | awk '/^[a-z0-9]+:/{i=substr($1,1,length($1)-1)}
|
||||
/inet /{if ($2 !~ /^127\.|^169\.254\./) printf " http://%s:3031/ (%s)\n", $2, i}'
|
||||
echo
|
||||
echo " 1) Auf dem Telefon einmal irgendwo tippen -> schaltet den Signalton frei"
|
||||
echo " 2) Gegen-AP-Automatik laeuft bereits -> S22 folgt dem iPhone spiegelverkehrt"
|
||||
echo " 3) Losgehen. Doppelton = beide angemeldet -> Raum antippen, weiter"
|
||||
echo " 2) In der Konsole pinnen (Multi-Pin) -> je Client AP/Band waehlen, 'Pin setzen'"
|
||||
echo " 3) Losgehen. Raum antippen, weiter -> Alarm = gepinnter Client haengt nirgends"
|
||||
echo
|
||||
echo " Lauf: $TP6_RUN_DIR"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
@@ -63,18 +55,17 @@ echo
|
||||
aufraeumen() {
|
||||
echo
|
||||
echo "Runde beendet. Aufraeumen …"
|
||||
# 🔴 REIHENFOLGE: erst die Konsole toeten, dann den Pin loesen. Andersherum ist es ein
|
||||
# Wettlauf, den man verliert -- die Automatik der noch laufenden Konsole sieht den
|
||||
# geloesten Pin und setzt ihn binnen zwei Sekunden wieder. Am 2026-08-22 meldete der
|
||||
# Aufraeumer brav "AP-Pin geloest", und danach standen die Regeln trotzdem am Core:
|
||||
# das Telefon waere nach jeder Runde gepinnt geblieben.
|
||||
# 🔴 REIHENFOLGE: erst die Konsole toeten, dann den Pin loesen. Die Konsole pinnt nicht
|
||||
# mehr automatisch (die alte Gegen-AP-Automatik ist mit dem Multi-Pin-Umbau 2026-09-04
|
||||
# entfallen), aber sie haelt ggf. noch SSH-Sitzungen zum Manager offen -- die erst
|
||||
# beenden, bevor der Pin geloest wird.
|
||||
[[ -n "$KONSOLE" ]] && kill "$KONSOLE" 2>/dev/null && echo " Konsole beendet."
|
||||
# Muster bewusst ohne -e/-p: es gab beide Varianten, und die enge Fassung liess den
|
||||
# Sampler stehen.
|
||||
pkill -f 'sshpass.*10.40.99.1' 2>/dev/null
|
||||
pkill -f 'tp6-monitor-core.sh' 2>/dev/null
|
||||
sleep 1
|
||||
bash docs/tp25-pin-s22.sh off >/dev/null 2>&1 && echo " AP-Pin geloest."
|
||||
bash docs/tp25-pin-multi.sh off >/dev/null 2>&1 && echo " Multi-Pin geloest."
|
||||
echo
|
||||
echo "Auswertung:"
|
||||
echo " python3 docs/tp25-gehweg-auswertung.py --dir $TP6_RUN_DIR"
|
||||
|
||||
@@ -44,6 +44,14 @@ HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
mkdir -p "$RUN_DIR"
|
||||
[[ -f "$MARKERS" ]] || echo "ts_epoch,ts_local,marker" > "$MARKERS"
|
||||
|
||||
# Client-Liste fuer die Klick-Konsole (docs/tp6-marker/server.py). Die Konsole liest sie
|
||||
# ueber den .current-run-Zeiger -- so kennt sie die N Geraete auch, wenn sie als
|
||||
# Dauer-Preview schon vor dem Messlauf lief. Format: name|ip|mac je Zeile.
|
||||
: > "$RUN_DIR/geraete.txt"
|
||||
for i in "${!PH_NAME[@]}"; do
|
||||
printf '%s|%s|%s\n' "${PH_NAME[$i]}" "${PH_IP[$i]}" "${PH_MAC[$i]}" >> "$RUN_DIR/geraete.txt"
|
||||
done
|
||||
|
||||
# Zeiger fuer die Klick-Konsole (docs/tp6-marker/server.py). Die laeuft als Dauer-Preview
|
||||
# auf Port 3031 und kennt dieses Verzeichnis sonst nicht -- ihre Marker landeten dann im
|
||||
# Lauf von gestern, waehrend der Sampler hierher schreibt. Beides fiel erst bei der
|
||||
|
||||
@@ -16,12 +16,12 @@ Zwei Aufgaben:
|
||||
- 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
|
||||
🔴 SEIT 2026-08-22 AENDERT DIESE SEITE DOCH ETWAS AM NETZ: der Multi-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.
|
||||
Authentifizierung — wer in VLAN10 steht, kann pinnen. 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 MACs), aber die Regel ist es nicht, die als naechstes hier
|
||||
landen koennte.
|
||||
|
||||
Bindet auf 0.0.0.0 (Bedienung vom iPhone/iPad).
|
||||
"""
|
||||
@@ -164,6 +164,82 @@ def sampler_tail():
|
||||
return {}
|
||||
|
||||
|
||||
def geraete():
|
||||
"""Liste (name, ip, mac) aus geraete.txt im Laufverzeichnis.
|
||||
|
||||
🔴 Die Datei schreibt tp25-walk-messung.sh beim Start des Messlaufs (eine Zeile
|
||||
name|ip|mac je Client). Fehlt sie — Alt-Lauf oder Dauer-Preview, die schon vor dem
|
||||
ersten Lauf lief — fallen wir auf die zwei bisherigen Geraete zurueck, damit die
|
||||
Anzeige nicht leer ist. Die Namen muessen zu den sampler-<name>.csv /
|
||||
walk-ping-<name>.csv passen, die die Harness schreibt.
|
||||
"""
|
||||
try:
|
||||
zeilen = [z.strip() for z in open(_p("geraete.txt")) if z.strip()]
|
||||
gs = []
|
||||
for z in zeilen:
|
||||
teile = z.split("|")
|
||||
if len(teile) >= 3 and teile[0]:
|
||||
gs.append((teile[0], teile[1], teile[2]))
|
||||
if gs:
|
||||
return gs
|
||||
except Exception:
|
||||
pass
|
||||
return [("iphone", "", "F2:62:4C:E1:AF:09"), ("s22", "", "2A:0D:B0:5D:AD:A7")]
|
||||
|
||||
|
||||
def client_tail(name):
|
||||
"""Letzte Sampler-Zeile EINES Geraets (sampler-<name>.csv). Fehlt sie, ist das kein Fehler.
|
||||
|
||||
Gleiche Spalten wie sampler_tail() — die Harness schreibt pro Geraet einen eigenen
|
||||
Sampler mit demselben Header (tp6-monitor-core.sh). Die Konsole braucht deshalb KEINE
|
||||
eigene SSH-Sitzung mehr: sie liest nur, was die Sampler schon auf die Platte legen.
|
||||
"""
|
||||
try:
|
||||
pfad = _p("sampler-%s.csv" % name)
|
||||
rows = [r.strip() for r in open(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 ping_loss(name, n=150):
|
||||
"""Ping-Verlust der letzten ~n Zeilen von walk-ping-<name>.csv (5 Pings/s -> n=150 ~ 30 s).
|
||||
|
||||
Leeres ms-Feld = Verlust. Liefert (verloren, gesamt) oder None, wenn noch keine Daten da
|
||||
sind. Der Header (ts_local,seq,ms) wird ueber die nicht-numerische seq-Spalte uebersprungen.
|
||||
"""
|
||||
try:
|
||||
rows = [r.strip() for r in open(_p("walk-ping-%s.csv" % name)) if r.strip()]
|
||||
verloren = gesamt = 0
|
||||
for z in rows[-n:]:
|
||||
teile = z.split(",")
|
||||
if len(teile) >= 3 and teile[1].isdigit():
|
||||
gesamt += 1
|
||||
if not teile[2].strip():
|
||||
verloren += 1
|
||||
return (verloren, gesamt) if gesamt else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def wan_tail():
|
||||
"""WAN/CPU-Anzeige: aus sampler.csv (Alt-Lauf) oder dem ersten Geraet-Sampler."""
|
||||
w = sampler_tail()
|
||||
if w.get("cpu") or w.get("wan_rx"):
|
||||
return w
|
||||
gs = geraete()
|
||||
return client_tail(gs[0][0]) if gs else {}
|
||||
|
||||
|
||||
def kill_flows():
|
||||
"""Not-Aus: Saettigungs-Flows sofort beenden."""
|
||||
killed = 0
|
||||
@@ -214,153 +290,104 @@ 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")
|
||||
# 🔎 Die Messidee (Christoph, 2026-08-22, generalisiert 2026-09-04): die Clients roamen frei
|
||||
# und zeigen damit, welchen AP ein normaler Client an dieser Stelle waehlt. Fuer die
|
||||
# Abdeckungsfrage wird je Client per access-list GENAU EIN AP+Band erlaubt
|
||||
# (tp25-pin-multi.sh). Die alte Gegen-AP-Automatik (S22 folgt dem iPhone) ist damit
|
||||
# ueberholt: gepinnt wird vorab band-genau, nicht mehr automatisch waehrend des Gehens.
|
||||
|
||||
# Was der Beobachter sieht. "" = nicht angemeldet.
|
||||
_live = {"iphone": "", "iphone_sig": "", "s22": "", "s22_sig": "", "stand": 0.0}
|
||||
_auto = {"an": False, "seit_weg": 0.0, "aufgegeben": ""}
|
||||
PINM_SH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tp25-pin-multi.sh")
|
||||
|
||||
# Pin-Zustand. Das Setzen dauert ~8 s (Regeln + Kick), deshalb laeuft es in einem Thread und
|
||||
# die Seite zeigt solange "setze Pin …". pins = {name: "ap|band"} der zuletzt gesetzten Auswahl.
|
||||
_pinm = {"busy": False, "msg": "", "pins": {}, "stand": 0.0}
|
||||
|
||||
|
||||
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 pin_targets():
|
||||
"""Eindeutige (ap, band)-Paare aus der Live-Karte, band-normalisiert fuer tp25-pin-multi.sh.
|
||||
|
||||
|
||||
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.
|
||||
ap_map() liefert 'apX / 5 GHz' bzw. 'apX / 2,4 GHz'; das Skript erwartet Band '5' oder '24'.
|
||||
"""
|
||||
# 🔴 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)
|
||||
ziele, gesehen = [], set()
|
||||
for wert in ap_map().values():
|
||||
ap, _, band = wert.partition(" / ")
|
||||
band = band.replace(" GHz", "").replace(",", ".")
|
||||
if band.startswith("5"):
|
||||
band = "5"
|
||||
elif band.startswith("2"):
|
||||
band = "24"
|
||||
else:
|
||||
continue
|
||||
if (ap, band) not in gesehen:
|
||||
gesehen.add((ap, band))
|
||||
ziele.append((ap, band))
|
||||
return sorted(ziele)
|
||||
|
||||
|
||||
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 build_spec(form):
|
||||
"""Aus den Formular-Auswahlen (pin_<name>=ap|band oder frei) den TP25_PINSPEC-String bauen.
|
||||
|
||||
Clients ohne Ziel (frei) bleiben frei — tp25-pin-multi.sh set raeumt vorher alle
|
||||
TP25-PINM-*-Regeln ab und setzt nur die hier genannten.
|
||||
"""
|
||||
eintraege = []
|
||||
for name, _ip, mac in geraete():
|
||||
wert = (form.get("pin_%s" % name) or ["frei"])[0]
|
||||
if wert and wert != "frei" and "|" in wert:
|
||||
ap, band = wert.split("|", 1)
|
||||
eintraege.append("%s|%s|%s|%s" % (name, mac, ap, band))
|
||||
return ";".join(eintraege)
|
||||
|
||||
|
||||
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)
|
||||
def pinm_aktion(spec):
|
||||
"""Pin setzen (spec = 'name|mac|ap|band;...') oder loesen (spec = ''). Laeuft im Thread."""
|
||||
pins = {}
|
||||
for e in (spec.split(";") if spec else []):
|
||||
teile = e.split("|")
|
||||
if len(teile) >= 4:
|
||||
pins[teile[0]] = "%s|%s" % (teile[2], teile[3])
|
||||
|
||||
|
||||
# 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]
|
||||
umgebung = dict(os.environ)
|
||||
if spec:
|
||||
umgebung["TP25_PINSPEC"] = spec
|
||||
r = subprocess.run(["bash", PINM_SH, "set"], capture_output=True, text=True,
|
||||
timeout=90, env=umgebung)
|
||||
else:
|
||||
r = subprocess.run(["bash", PINM_SH, "off"], capture_output=True, text=True,
|
||||
timeout=90)
|
||||
_pinm["msg"] = r.stdout.strip() or r.stderr.strip() or "ok"
|
||||
except Exception as e:
|
||||
_pin["msg"] = "Fehler: %s" % e
|
||||
_pinm["msg"] = "Fehler: %s" % e
|
||||
finally:
|
||||
_pin["busy"] = False
|
||||
_pin["stand"] = 0.0
|
||||
pin_lesen()
|
||||
_pin["busy"] = True
|
||||
_pin["msg"] = "schaltet um …"
|
||||
_pinm["busy"] = False
|
||||
_pinm["stand"] = time.time()
|
||||
|
||||
_pinm["busy"] = True
|
||||
_pinm["msg"] = "setze Pin …" if spec else "löse Pin …"
|
||||
_pinm["pins"] = pins
|
||||
# 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)
|
||||
# nicht sagen, welche Messpunkte zu welchem Pin gehoeren.
|
||||
write_marker("PINM=%s" % (spec or "off"))
|
||||
threading.Thread(target=lauf, daemon=True).start()
|
||||
|
||||
|
||||
def pinm_verify():
|
||||
"""tp25-pin-multi.sh verify im Thread — zeigt Regeln + aktuelle Anmeldungen."""
|
||||
def lauf():
|
||||
try:
|
||||
r = subprocess.run(["bash", PINM_SH, "verify"], capture_output=True, text=True,
|
||||
timeout=60)
|
||||
_pinm["msg"] = r.stdout.strip() or r.stderr.strip() or "ok"
|
||||
except Exception as e:
|
||||
_pinm["msg"] = "Fehler: %s" % e
|
||||
finally:
|
||||
_pinm["busy"] = False
|
||||
_pinm["stand"] = time.time()
|
||||
|
||||
_pinm["busy"] = True
|
||||
_pinm["msg"] = "prüfe …"
|
||||
threading.Thread(target=lauf, daemon=True).start()
|
||||
|
||||
|
||||
@@ -391,27 +418,42 @@ def fmt_bps(v):
|
||||
|
||||
|
||||
def render(st):
|
||||
w = sampler_tail()
|
||||
w = wan_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>')
|
||||
haengende = []
|
||||
|
||||
# 🔴 Der wichtigste Alarm der Seite, seit es den Pin gibt (2026-08-22, generalisiert
|
||||
# 2026-09-04): 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 Client-Zeile gezeigt, sondern schreit.
|
||||
clients = []
|
||||
for name, ip, mac in geraete():
|
||||
t = client_tail(name)
|
||||
pl = ping_loss(name)
|
||||
iface = t.get("iface", "")
|
||||
ap = ap_map().get(iface, iface) if iface else ""
|
||||
signal = t.get("signal", "")
|
||||
frisch = t.get("age", 999) < 30
|
||||
gepinnt = name in _pinm["pins"]
|
||||
if gepinnt and iface == "absent" and frisch:
|
||||
alarm = True
|
||||
haengende.append(name)
|
||||
clients.append((name, ip, mac, iface, ap, signal, pl, frisch, gepinnt))
|
||||
|
||||
if haengende:
|
||||
banner = ('<div class="banner warn">📵 <b>%s</b> HÄNGT NIRGENDS — der Pin wirft das '
|
||||
'Geraet vom gesperrten AP; Android waehlt das Netz dann von selbst ab. '
|
||||
'<b>Nicht weitergehen</b>, sonst misst du Stille statt Abdeckung.</div>'
|
||||
% ", ".join(haengende))
|
||||
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>')
|
||||
'pinnen geht 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>')
|
||||
@@ -423,86 +465,65 @@ def render(st):
|
||||
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")))
|
||||
# Client-Karten: je Geraet Name, AP+Signal, Ping-Verlust der letzten ~30 s.
|
||||
karten = ""
|
||||
for name, ip, mac, iface, ap, signal, pl, frisch, gepinnt in clients:
|
||||
if iface and iface != "absent":
|
||||
status = ("%s · %s dBm" % (ap, signal)) if signal else ap
|
||||
elif iface == "absent":
|
||||
status = "<b>nirgends</b>"
|
||||
else:
|
||||
status = "—"
|
||||
if pl:
|
||||
verloren, gesamt = pl
|
||||
ping = "%d/%d verloren" % (verloren, gesamt)
|
||||
else:
|
||||
ping = "—"
|
||||
karten += ('<div class="card"><div class="phase">%s</div>'
|
||||
'<div class="inst"><b>%s</b></div>'
|
||||
'<div class="cval">%s</div>'
|
||||
'<div class="cval">Ping: %s</div></div>'
|
||||
% ("📌 gepinnt" if gepinnt else "Client", name, status, ping))
|
||||
|
||||
ziel = pin_lesen()
|
||||
ip_ap, s22_ap = _ap_von(_live["iphone"]), _ap_von(_live["s22"])
|
||||
frisch = time.time() - _live["stand"] < 15
|
||||
# Pin-UI: je Client ein Ziel-Dropdown, dazu setzen/loesen/verify.
|
||||
ziele = pin_targets()
|
||||
|
||||
# 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 optionen(aktuell):
|
||||
out = ['<option value="frei"%s>frei</option>' % (" selected" if aktuell == "frei" else "")]
|
||||
for ap, band in ziele:
|
||||
wert = "%s|%s" % (ap, band)
|
||||
label = "%s / %s GHz" % (ap, "5" if band == "5" else "2,4")
|
||||
out.append('<option value="%s"%s>%s</option>'
|
||||
% (wert, " selected" if wert == aktuell else "", label))
|
||||
return "".join(out)
|
||||
|
||||
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)
|
||||
zeilen = ""
|
||||
for name, ip, mac in geraete():
|
||||
zeilen += ('<div class="pinzeile"><label>%s</label>'
|
||||
'<select name="pin_%s">%s</select></div>'
|
||||
% (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<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
|
||||
if _pinm["busy"]:
|
||||
pinstatus = "⏳ %s" % _pinm["msg"].replace("\n", "<br>")
|
||||
elif _pinm["msg"]:
|
||||
pinstatus = _pinm["msg"].replace("\n", "<br>")
|
||||
elif not ziele:
|
||||
pinstatus = "Manager nicht erreichbar — keine Pin-Ziele."
|
||||
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",
|
||||
)
|
||||
)
|
||||
pinstatus = "Je Client AP/Band wählen, dann „Pin setzen“."
|
||||
|
||||
pinm_karte = (
|
||||
'<div class="card"><span id="sig" data-alarm="%d" data-busy="%d"></span>'
|
||||
'<div class="phase">Pinnen (Multi-Pin)</div>'
|
||||
'<div class="inst" id="pininst">%s</div>'
|
||||
'<form method="post" action="/pinmulti" onsubmit="return sendpinm(event)">'
|
||||
'%s'
|
||||
'<button class="ok" type="submit">📌 Pin setzen</button></form>'
|
||||
'<form method="post" action="/pinmulti?off=1" onsubmit="return sendpinmoff(event)">'
|
||||
'<button class="cam" type="submit">Pin lösen</button></form>'
|
||||
'<form method="post" action="/pinmulti?verify=1" onsubmit="return sendpinmoff(event)">'
|
||||
'<button class="cam" type="submit">verify</button></form>'
|
||||
'</div>' % (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}}
|
||||
</style>
|
||||
{banner}
|
||||
@@ -589,10 +616,10 @@ form{{margin:0}}
|
||||
{countdown}
|
||||
{prog}
|
||||
</div>
|
||||
{pinkarte}
|
||||
{clients}
|
||||
{pinm_karte}
|
||||
{raumzeile}
|
||||
<div class="live">WLAN {wlan}
|
||||
WAN {wan}
|
||||
<div class="live">WAN {wan}
|
||||
Marker {last}</div>
|
||||
{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 …<br>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 <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);
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user