Der Stopp-Knopf, den der Anleitungstext seit jeher versprach

Zwei Maengel aus dem Rundgang vom 22.8. behoben.

1. STOPP-KNOPF. Die Konsole nannte "⏹ Aufzeichnung stoppen", es gab ihn nicht.
   Christoph drueckte "Call beendet" und den Not-Aus -- beide schreiben nur
   Marken -- und die Messung lief weiter, waehrend der Core sein Telefon mit
   5 Pings/s bearbeitete. Jetzt legt der Knopf stop.flag ab, und die
   Warteschleife des Messskripts bricht darauf ab.

2. ZWEITES TELEFON. Der Beobachter las immer schon beide Geraete, hielt die
   Zahlen aber nur im Speicher fuer die Anzeige. Da das S22 per Automatik
   immer auf dem GEGEN-AP sitzt, fehlte damit ausgerechnet die Strecke, die
   die Alltagsabdeckung zeigt -- der Terrassen-Vergleich war nicht zu fuehren.
   Jetzt: zweiter Ping (walk-ping-iphone.csv) und beide-telefone.csv im
   2-s-Takt mit iface + Signal je Geraet.

Beim Testen kamen zwei Fehler ans Licht, die den naechsten Lauf gekostet
haetten:

- `PINGER=$(ping_lauf ...)` blockiert. Die Kommandosubstitution wartet, bis die
  stdout-Pipe schliesst, und der Hintergrundjob darin haelt sie offen. Das
  Skript stand in dieser Zeile, der zweite Ping wurde nie gestartet -- und es
  sah von aussen aus wie ein normal laufender Lauf. Die Funktion laeuft in der
  aktuellen Shell, also liefert $! danach die richtige PID.
- `kill $SAMPLER` traf nur den Wrapper. Dessen Subshell samt SSH-Sitzung wurde
  dabei verwaist und lief weiter: nach dem Stopp hielt der Core stumm eine
  Sampler-Sitzung offen. Wer den Vater zuerst killt, verliert den Zugriff auf
  die Kinder -- jetzt killbaum(), Kinder zuerst.

Mitgenommen von der Rotationsliste: tp6-monitor-core.sh und der Ping in
tp25-walk-messung.sh laufen jetzt ueber `sshpass -e` statt `-p`. Damit steht
das Router-Passwort in keiner Kommandozeile mehr. `pgrep -fl "sshpass -p"`
ist leer. Offen bleibt tp25-pin-s22.sh.

Abnahme: Lauf gestartet, beide Ziele gepingt (10.40.10.178 + .196), ein Klick
auf den Stopp-Knopf, danach kein Prozess und keine SSH-Sitzung mehr uebrig,
Pin geloest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 23:11:07 +02:00
parent ecc44b5af4
commit 6cd9db61d5
5 changed files with 152 additions and 35 deletions

View File

@@ -129,7 +129,7 @@ Entscheidung — und sie steht unter dem Vorbehalt, dass 20 MHz vorher gemessen
--- ---
## Zwei Werkzeugmängel, die dieser Lauf aufgedeckt hat ## Zwei Werkzeugmängel, die dieser Lauf aufgedeckt hat — ✅ beide behoben (2026-08-22)
1. 🔴 **Es gibt keinen Stopp-Knopf.** Christoph drückte 22:53:26 „📴 Call beendet" und 1. 🔴 **Es gibt keinen Stopp-Knopf.** Christoph drückte 22:53:26 „📴 Call beendet" und
„⛔ Last sofort stoppen" — beides schreibt nur Marken (`ABORT_killed=0`, es gab keine „⛔ Last sofort stoppen" — beides schreibt nur Marken (`ABORT_killed=0`, es gab keine
@@ -140,3 +140,14 @@ Entscheidung — und sie steht unter dem Vorbehalt, dass 20 MHz vorher gemessen
Telefone, hält die Zahlen aber nur im Speicher für die Anzeige. Nachträglich ließ sich nur Telefone, hält die Zahlen aber nur im Speicher für die Anzeige. Nachträglich ließ sich nur
das *Roaming-Log* am Manager retten (`manager-log.txt`) — Ereignisse, keine Zeitreihe. das *Roaming-Log* am Manager retten (`manager-log.txt`) — Ereignisse, keine Zeitreihe.
Deshalb fehlt oben der direkte Terrassen-Vergleich. Deshalb fehlt oben der direkte Terrassen-Vergleich.
**Behoben.** Die Konsole hat jetzt einen echten `⏹ Aufzeichnung stoppen`, der die Messung
beendet (`stop.flag`), und es werden **beide** Telefone gepingt (`walk-ping.csv` +
`walk-ping-iphone.csv`) sowie beide im 2-s-Takt mitgeschrieben (`beide-telefone.csv`).
Beim Testen fielen dabei zwei weitere Fehler auf, die beide diesen Lauf betroffen hätten:
`PINGER=$(ping_lauf …)` blockierte (Kommandosubstitution wartet auf die stdout-Pipe, die der
Hintergrundjob offen hält) — der zweite Ping wäre nie gestartet; und `kill $SAMPLER` traf nur
den Wrapper, dessen Subshell samt SSH-Sitzung verwaist weiterlief. Jetzt wird der
Prozessbaum kindzuerst beendet.
⇒ **Der nächste Lauf beantwortet die Terrassen-Frage.** Diesmal nicht.

View File

@@ -57,7 +57,10 @@ aufraeumen() {
echo "Runde beendet. Aufraeumen …" echo "Runde beendet. Aufraeumen …"
bash docs/tp25-pin-s22.sh off >/dev/null 2>&1 && echo " AP-Pin geloest." bash docs/tp25-pin-s22.sh off >/dev/null 2>&1 && echo " AP-Pin geloest."
[[ -n "$KONSOLE" ]] && kill "$KONSOLE" 2>/dev/null && echo " Konsole beendet." [[ -n "$KONSOLE" ]] && kill "$KONSOLE" 2>/dev/null && echo " Konsole beendet."
pkill -f 'sshpass -e ssh.*10.40.99.1' 2>/dev/null # 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
echo echo
echo "Auswertung:" echo "Auswertung:"
echo " python3 docs/tp25-gehweg-auswertung.py --dir $TP6_RUN_DIR" echo " python3 docs/tp25-gehweg-auswertung.py --dir $TP6_RUN_DIR"

View File

@@ -19,8 +19,14 @@
set -uo pipefail set -uo pipefail
RUN_DIR="${TP6_RUN_DIR:-/private/tmp/claude-501/-Users-christophemsenhuber-dev-bf-network-r6/0d86ebd1-035f-4513-a231-5cca8111ecfd/scratchpad/tp6-run2}" RUN_DIR="${TP6_RUN_DIR:-/private/tmp/claude-501/-Users-christophemsenhuber-dev-bf-network-r6/0d86ebd1-035f-4513-a231-5cca8111ecfd/scratchpad/tp6-run2}"
PHONE="${TP25_PHONE_IP:-10.40.10.196}" PHONE="${TP25_PHONE_IP:-10.40.10.178}" # S22 -- wird gepingt und gesampelt
PHONE_MAC="${TP25_PHONE_MAC:-F2:62:4C:E1:AF:09}" PHONE_MAC="${TP25_PHONE_MAC:-2A:0D:B0:5D:AD:A7}"
# 🔴 Seit dem Lauf vom 2026-08-22 wird das ZWEITE Telefon mitgepingt. Vorher lief der Ping
# nur auf das gepinnte Geraet -- und weil das per Automatik immer auf dem GEGEN-AP sitzt,
# fehlte ausgerechnet die Strecke, die die Alltagsabdeckung zeigt. Der Terrassen-Vergleich
# liess sich deshalb nicht fuehren, obwohl beide Telefone dabei waren.
PHONE2="${TP25_PHONE2_IP:-10.40.10.196}" # iPhone -- roamt frei
PHONE2_NAME="${TP25_PHONE2_NAME:-iphone}"
STATE="$RUN_DIR/phase.json" STATE="$RUN_DIR/phase.json"
MARKERS="$RUN_DIR/markers.csv" MARKERS="$RUN_DIR/markers.csv"
HERE="$(cd "$(dirname "$0")/.." && pwd)" HERE="$(cd "$(dirname "$0")/.." && pwd)"
@@ -72,7 +78,7 @@ state "Kein Call nötig. <b>🚶</b> drücken, wenn du losgehst, <b>📍</b> wen
bash "$HERE/docs/tp6-monitor-core.sh" --out "$RUN_DIR/sampler.csv" --mac "$PHONE_MAC" --interval 1 \ bash "$HERE/docs/tp6-monitor-core.sh" --out "$RUN_DIR/sampler.csv" --mac "$PHONE_MAC" --interval 1 \
> "$RUN_DIR/sampler.out" 2>&1 & SAMPLER=$! > "$RUN_DIR/sampler.out" 2>&1 & SAMPLER=$!
# 5 Pings/s auf das Telefon — GEPINGT WIRD VOM CORE, nicht von diesem Rechner. # 5 Pings/s auf JEDES Telefon — GEPINGT WIRD VOM CORE, nicht von diesem Rechner.
# 🔴 Der Grund: pingt der Laptop, laeuft der Weg ueber ZWEI Funkstrecken (Laptop -> AP -> # 🔴 Der Grund: pingt der Laptop, laeuft der Weg ueber ZWEI Funkstrecken (Laptop -> AP ->
# Core -> AP -> Telefon). Fehlt dann ein Paket, ist nicht entscheidbar, welches der # Core -> AP -> Telefon). Fehlt dann ein Paket, ist nicht entscheidbar, welches der
# beiden Geraete weg war. Vom Core aus ist die Strecke bis zum AP durchgehend Kabel, # beiden Geraete weg war. Vom Core aus ist die Strecke bis zum AP durchgehend Kabel,
@@ -80,35 +86,84 @@ bash "$HERE/docs/tp6-monitor-core.sh" --out "$RUN_DIR/sampler.csv" --mac "$PHONE
# (Aufgefallen am 2026-08-21, als das MacBook waehrend einer Messung vom Dock ans # (Aufgefallen am 2026-08-21, als das MacBook waehrend einer Messung vom Dock ans
# WLAN wechselte und dabei auch noch die IP wechselte.) # WLAN wechselte und dabei auch noch die IP wechselte.)
PW="$(grep -m1 '^CRS326_CORE_PASS=' ~/.secrets/credentials.env | cut -d= -f2- | sed 's/^["'"'"'"'"'"']//; s/["'"'"'"'"'"']$//')" PW="$(grep -m1 '^CRS326_CORE_PASS=' ~/.secrets/credentials.env | cut -d= -f2- | sed 's/^["'"'"'"'"'"']//; s/["'"'"'"'"'"']$//')"
echo "ts_local,seq,ms" > "$RUN_DIR/walk-ping.csv"
(
while true; do
sshpass -p "$PW" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 \
-o ServerAliveInterval=15 admin@10.40.99.1 \
"/ping $PHONE interval=200ms count=100000" 2>/dev/null \
| while IFS= read -r l; do
l="${l%$'\r'}"
case "$l" in
*SEQ*HOST*) continue ;;
esac
seq="$(printf '%s' "$l" | awk '{print $1}')"
case "$seq" in
''|*[!0-9]*) continue ;;
esac
case "$l" in
*timeout*) printf '%s,%s,\n' "$(date +%H:%M:%S)" "$seq" ;;
*) printf '%s,%s,%s\n' "$(date +%H:%M:%S)" "$seq" \
"$(printf '%s' "$l" | awk '{print $NF}')" ;;
esac
done >> "$RUN_DIR/walk-ping.csv"
printf '%s,,RECONNECT\n' "$(date +%H:%M:%S)" >> "$RUN_DIR/walk-ping.csv"
sleep 2
done
) & PINGER=$!
trap 'kill $SAMPLER $PINGER 2>/dev/null; pkill -f "ping $PHONE interval=200ms" 2>/dev/null; state "Aufzeichnung gestoppt."; echo; echo "Gestoppt."' EXIT # $1 = Ziel-IP, $2 = Ausgabedatei. Schreibt "ts_local,seq,ms"; leeres ms = Verlust.
# 🔴 sshpass -e statt -p: mit -p steht das Router-Passwort in der Kommandozeile und damit
# in `ps` und in jedem Prozess-Trace. Am 2026-08-22 ist es genau so im Klartext in einem
# Sitzungsmitschnitt gelandet.
ping_lauf() {
local ziel="$1" datei="$2"
echo "ts_local,seq,ms" > "$datei"
(
while true; do
SSHPASS="$PW" sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 \
-o ServerAliveInterval=15 admin@10.40.99.1 \
"/ping $ziel interval=200ms count=100000" 2>/dev/null \
| while IFS= read -r l; do
l="${l%$'\r'}"
case "$l" in
*SEQ*HOST*) continue ;;
esac
seq="$(printf '%s' "$l" | awk '{print $1}')"
case "$seq" in
''|*[!0-9]*) continue ;;
esac
case "$l" in
*timeout*) printf '%s,%s,\n' "$(date +%H:%M:%S)" "$seq" ;;
*) printf '%s,%s,%s\n' "$(date +%H:%M:%S)" "$seq" \
"$(printf '%s' "$l" | awk '{print $NF}')" ;;
esac
done >> "$datei"
printf '%s,,RECONNECT\n' "$(date +%H:%M:%S)" >> "$datei"
sleep 2
done
) &
}
# 🔴 Die PID NICHT ueber $(ping_lauf ...) holen: die Kommandosubstitution wartet, bis die
# stdout-Pipe schliesst, und der Hintergrundjob darin haelt sie offen -- das Skript blieb
# dort stehen, der zweite Ping wurde nie gestartet (2026-08-22). Die Funktion laeuft in
# der aktuellen Shell, also liefert $! danach die richtige PID.
ping_lauf "$PHONE" "$RUN_DIR/walk-ping.csv"; PINGER=$!
ping_lauf "$PHONE2" "$RUN_DIR/walk-ping-$PHONE2_NAME.csv"; PINGER2=$!
# 🔴 Kinder ZUERST. `kill $SAMPLER` traf nur den Wrapper; dessen Subshell samt SSH-Sitzung
# wurde dabei verwaist und lief weiter -- nach dem Stopp hielt der Core stumm eine
# Sampler-Sitzung offen (2026-08-22, beim Test des Stopp-Knopfs gefunden). Wer den Vater
# zuerst killt, verliert den Zugriff auf die Kinder.
killbaum() {
local pid="$1" kind
for kind in $(pgrep -P "$pid" 2>/dev/null); do killbaum "$kind"; done
kill "$pid" 2>/dev/null
}
beenden() {
for pid in $SAMPLER $PINGER $PINGER2; do killbaum "$pid"; done
pkill -f "ping $PHONE interval=200ms" 2>/dev/null
pkill -f "ping $PHONE2 interval=200ms" 2>/dev/null
# Der /ping am Router endet mit der SSH-Sitzung (am 2026-08-22 nachgeprueft: nach dem
# Kill der Sitzungen lief am Core nichts weiter). Bewusst KEIN "job remove" am Geraet --
# ein pauschales Aufraeumen fremder Jobs waere ein zu grobes Werkzeug fuer ein Messskript.
state "Aufzeichnung gestoppt."
echo; echo "Gestoppt. Auswertung:"
echo " python3 docs/tp25-gehweg-auswertung.py --dir $RUN_DIR"
}
trap beenden EXIT
echo "Gehweg-Messung laeuft." echo "Gehweg-Messung laeuft."
echo " Funk -> $RUN_DIR/sampler.csv" echo " Funk -> $RUN_DIR/sampler.csv"
echo " Ping -> $RUN_DIR/walk-ping.csv (Quelle: crs326-core)" echo " Ping -> $RUN_DIR/walk-ping.csv ($PHONE, gepinnt)"
while true; do touch "$STATE" 2>/dev/null; sleep 5; done echo " Ping -> $RUN_DIR/walk-ping-$PHONE2_NAME.csv ($PHONE2, frei roamend)"
echo " Stoppen: '⏹ Aufzeichnung stoppen' auf der Konsole -- oder Ctrl-C hier."
# 🔴 Bis 2026-08-22 lief hier nur `while true; do touch; sleep 5; done`, und das Skript war
# ausschliesslich per Ctrl-C zu beenden -- obwohl der Anleitungstext auf der Konsole einen
# Stopp-Knopf versprach. Christoph drueckte "Call beendet" und den Not-Aus, beides schrieb
# nur Marken, und die Messung lief weiter. Jetzt gibt es den Knopf wirklich: die Konsole
# legt stop.flag ab, und diese Schleife bricht darauf ab.
rm -f "$RUN_DIR/stop.flag"
while [[ ! -f "$RUN_DIR/stop.flag" ]]; do
touch "$STATE" 2>/dev/null
sleep 2
done
echo "Stopp von der Konsole empfangen."

View File

@@ -283,6 +283,12 @@ def beobachter():
continue continue
_live.update(iphone=teile[0], iphone_sig=teile[1], _live.update(iphone=teile[0], iphone_sig=teile[1],
s22=teile[2], s22_sig=teile[3], stand=time.time()) 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() entscheide()
except Exception as e: except Exception as e:
# Ein Beobachter, der still stirbt, ist schlimmer als keiner (Lehre aus dem # Ein Beobachter, der still stirbt, ist schlimmer als keiner (Lehre aus dem
@@ -295,6 +301,19 @@ def beobachter():
time.sleep(3) 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(): def entscheide():
"""Gegen-AP setzen, sobald das iPhone den AP wechselt.""" """Gegen-AP setzen, sobald das iPhone den AP wechselt."""
if not _auto["an"] or _pin["busy"]: if not _auto["an"] or _pin["busy"]:
@@ -510,6 +529,14 @@ def render(st):
notaus = ('<form method="post" action="/m?type=ABORT" onsubmit="return send(event,\'ABORT\')">' notaus = ('<form method="post" action="/m?type=ABORT" onsubmit="return send(event,\'ABORT\')">'
'<button class="abort" type="submit">⛔ LAST SOFORT STOPPEN</button></form>' '<button class="abort" type="submit">⛔ LAST SOFORT STOPPEN</button></form>'
) if st.get("driver") else "" ) 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. # 🚶 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\')">' btns = ('<form method="post" action="/m?type=WALK" onsubmit="return send(event,\'WALK\')">'
@@ -634,6 +661,17 @@ function signale(){{
letzterAlarm = alarm ? 1 : 0; letzterBusy = busy ? 1 : 0; 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){{ function sendauto(ev){{
try{{ try{{
ev.preventDefault(); unlock(); okton(); ev.preventDefault(); unlock(); okton();
@@ -722,6 +760,13 @@ class Handler(BaseHTTPRequestHandler):
fh.write("%s %s\n" % (time.strftime("%H:%M:%S"), self.path.split("msg=", 1)[-1])) fh.write("%s %s\n" % (time.strftime("%H:%M:%S"), self.path.split("msg=", 1)[-1]))
self._send(200, "{}") self._send(200, "{}")
return 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"): if self.path.startswith("/auto"):
_auto["an"] = not _auto["an"] _auto["an"] = not _auto["an"]
_auto["aufgegeben"] = "" _auto["aufgegeben"] = ""

View File

@@ -83,7 +83,10 @@ echo "Sampler laeuft -> $OUT (Takt ${INTERVAL}s${MAC:+, Client $MAC})"
# dem Ping-Mitschnitt — ausgerechnet im interessanten Teil. Ein Sampler, der still stirbt, # dem Ping-Mitschnitt — ausgerechnet im interessanten Teil. Ein Sampler, der still stirbt,
# ist schlimmer als keiner. # ist schlimmer als keiner.
while true; do while true; do
sshpass -p "$PW" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 \ # 🔴 -e statt -p: mit -p steht das Router-Passwort in der Kommandozeile und damit in `ps`
# und in jedem Prozess-Trace. Am 2026-08-22 ist es so im Klartext in einen Mitschnitt
# geraten. Mit -e liegt es nur in der Umgebung dieses Kindprozesses.
SSHPASS="$PW" sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 \
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 "admin@${ROUTER_IP}" "$ROS" \ -o ServerAliveInterval=15 -o ServerAliveCountMax=3 "admin@${ROUTER_IP}" "$ROS" \
2>>"${OUT%.csv}.stderr.log" \ 2>>"${OUT%.csv}.stderr.log" \
| while IFS= read -r line; do | while IFS= read -r line; do