From 284bc6f07325edc98aade387c21a1af6f9a6dfaf Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Sat, 3 Oct 2026 20:05:31 +0200 Subject: [PATCH] Handfunke: wenn die Station sendet, steht es jetzt da MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `trx:` und `tune:` liest tci.js seit dem Anfang mit — und niemand hat die beiden je benutzt. Die Seite WUSSTE also, dass gesendet wird, und sagte nichts. Für eine Fernbedienung, die nur hört, ist das die unangenehmste Form von Stille: Wasserfall leer, Ton weg, S-Meter unten, und nichts erklärt es. Dieselbe Gattung Fehler wie das eingefrorene S-Meter, nur andersherum — dort behauptete die Anzeige etwas Falsches, hier ließ sie etwas Richtiges weg. Seit dem Mikrofon-PTT der SunSDR QRP kann das auch ohne Zutun am Pult passieren: Taste am Mikrofon gedrückt → Longpath meldet `trx:0,true` → hier steht es. Drei Teile: * Die Zeile sitzt dort, wo sonst "SENDEN / NUR IN DER APP" steht — gleiche Geometrie, gleicher Platz, nur in Messing: "STATION SENDET · EMPFÄNGER STUMM" (bzw. "STATION STIMMT AB" bei tune). Kein Rot: Rot bleibt der Warnung, und Senden ist keine Warnung, sondern ein Zustand. Es ist derselbe Ton, in dem die Seite alles Gemessene zeigt (--measured) — denn das ist es, eine Meldung des Geräts. * Keine Fehlalarme während des Sendens: "nur rauschen (n dB) — antenne?" und "⚠ stockt" werden unterdrückt. Der Empfänger IST stumm; beide Hinweise wären die falsche Erklärung für etwas, das die Sendezeile daneben schon richtig benennt. Dieselbe Regel wie bei "stockt" ohne Tonstrom. * Die Zeile wird nur bei einer Flanke neu geschrieben, nicht sechzigmal je Sekunde. Was BEWUSST nicht passiert: das S-Meter wird nicht geleert. Ob Longpath während des Sendens weiter echte Empfangswerte meldet, ist nicht geprüft — und das zu prüfen hieße senden. Ungeprüft etwas auszublenden wäre genauso geraten wie es ungeprüft stehenzulassen. Dazu die Werkbank, damit das ohne Funkgerät prüfbar ist (den Sendezustand am echten Gerät herzustellen heißt senden, und das ist ohne Antenne verboten): attrappe.py bekommt `--sendet N` (schaltet alle N Sekunden zwischen `trx:0,true` und `false`) und `--port N` (am 2026-10-03 hing eine Attrappe seit elf Stunden auf 50099; sie abzuschießen wäre die bequeme und die falsche Antwort gewesen). Geprüft am Telefonformat 375x812 gegen die Attrappe, beide Hälften: * `--sendet 6`: Zeile wechselt zwischen grau "SENDEN NUR IN DER APP" und Messing "STATION SENDET EMPFÄNGER STUMM". * `--still --sendet 6`: während des Sendens steht in der Fußzeile NUR "◆ gekoppelt 5 kB/s bild", im Empfang kommt "nur rauschen (3 dB) — antenne?" zurück. Genau so soll es sein. Co-Authored-By: Claude Opus 5 --- handfunke/app.js | 47 +++++++++++++++++++++++++++++++++++++++++-- handfunke/attrappe.py | 39 +++++++++++++++++++++++++++++++++++ handfunke/stil.css | 22 ++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/handfunke/app.js b/handfunke/app.js index 5c8be70a3..881221d8a 100644 --- a/handfunke/app.js +++ b/handfunke/app.js @@ -61,6 +61,10 @@ const state = { tonWeg: null, // 'worklet' | 'scriptprocessor' tonFehler: null, // Text fuer die Fusszeile, wenn kein Ton geht hatSpektrumstrom: false, // Server liefert fertige Bins + // Zuletzt angezeigter Sendezustand. Nicht die Wahrheit, sondern was + // auf dem Schirm steht — damit die Zeile nur bei einer FLANKE neu + // geschrieben wird und nicht sechzigmal je Sekunde. + sendetGezeigt: false, rueckfall: false, // wir rechnen selbst aus rohem I/Q iqRueckfall: null, audio: null, node: null, @@ -1794,7 +1798,12 @@ function schleife(t) { // stillen Tonstrom. Eine Kante an der falschen Stelle ist schlimmer als // keine. const bildLaeuft = r.spec > 0 || r.iq > 0; - const nurRauschen = bildLaeuft && state.hfAbstand !== null + // Waehrend gesendet wird, ist der Empfaenger stumm. "nur rauschen — + // antenne?" waere dann die falsche Erklaerung fuer etwas, das die + // Sendezeile daneben schon richtig benennt; dieselbe Regel wie bei + // "stockt" ohne Tonstrom. + const sendetGerade = link.ready && (link.st.mox || link.st.tune); + const nurRauschen = bildLaeuft && !sendetGerade && state.hfAbstand !== null && state.hfAbstand < 15; $('fussBild').className = (!bildLaeuft || nurRauschen) ? 'warn' : ''; $('fussBild').textContent = @@ -1824,7 +1833,8 @@ function schleife(t) { // Erklaerung fuer "es kommt nichts", und sie stuende dauerhaft da. Die // Pruefung auf link.ready allein genuegte nicht: die Verbindung kann // stehen, ohne dass Ton abonniert ist. - if (link.ready && r.audio > 0 && !state.tonFehler && state.tonLeerlaufZuletzt + if (link.ready && !sendetGerade && r.audio > 0 && !state.tonFehler + && state.tonLeerlaufZuletzt && performance.now() - state.tonLeerlaufZuletzt < 2000) { $('fussTon').textContent += ' ⚠ stockt'; } @@ -1841,6 +1851,39 @@ function schleife(t) { : link.ready ? '◆ gekoppelt' : (link.ws && link.ws.readyState === 1) ? '◆ verbinde…' : '◇ getrennt'; $('fussStatus').className = still ? 'warn' : (link.ready ? 'ok' : ''); + + // ── Wenn die Station sendet, darf die Seite das nicht verschweigen ────── + // + // `trx:` und `tune:` werden seit dem Anfang mitgelesen (tci.js) und waren + // bis hierher nirgends verwendet: die Seite WUSSTE, dass gesendet wird, + // und sagte nichts. Fuer eine Fernbedienung, die nur hoert, ist das die + // unangenehmste Form von Stille — Wasserfall leer, Ton weg, S-Meter + // unten, und nichts erklaert es. Genau dieselbe Gattung Fehler wie das + // eingefrorene S-Meter, nur umgekehrt: dort behauptete die Anzeige etwas + // Falsches, hier laesst sie etwas Richtiges weg. + // + // Seit dem Mikrofon-PTT der SunSDR QRP kann das auch ohne Zutun am Pult + // passieren: Taste am Mikrofon gedrueckt -> Longpath meldet `trx:0,true` + // -> hier steht es. + // + // Die Zeile sitzt dort, wo sonst "SENDEN / NUR IN DER APP" steht: gleiche + // Geometrie, gleicher Platz, nur in Messing. Kein Rot — Rot bleibt der + // Warnung, und Senden ist keine Warnung, sondern ein Zustand. + // + // Was hier bewusst NICHT passiert: das S-Meter wird nicht geleert. Ob + // Longpath waehrend des Sendens weiter echte Empfangswerte meldet, ist + // nicht geprueft — und das zu pruefen hiesse senden. Ungeprueft etwas + // ausblenden waere genauso geraten wie es ungeprueft stehenzulassen. + // `sendetGerade` ist weiter oben in derselben Runde schon bestimmt. + const txEl = $('tx'); + if (sendetGerade !== state.sendetGezeigt) { + state.sendetGezeigt = sendetGerade; + txEl.classList.toggle('sendet', sendetGerade); + txEl.innerHTML = sendetGerade + ? (link.st.tune ? 'STATION STIMMT AB EMPFÄNGER STUMM' + : 'STATION SENDET EMPFÄNGER STUMM') + : 'SENDEN NUR IN DER APP'; + } requestAnimationFrame(schleife); } requestAnimationFrame(schleife); diff --git a/handfunke/attrappe.py b/handfunke/attrappe.py index 6e1f07a5e..f8d1df8b3 100644 --- a/handfunke/attrappe.py +++ b/handfunke/attrappe.py @@ -16,9 +16,36 @@ import base64, hashlib, math, os, random, socket, struct, threading, time, sys +# --port N: fuer den Fall, dass schon eine Attrappe laeuft. Am 2026-10-03 +# hing eine seit elf Stunden auf 50099 -- sie abzuschiessen waere die +# bequeme und die falsche Antwort gewesen (sie koennte zu einer anderen +# Sitzung gehoeren). PORT = 50099 +if '--port' in sys.argv: + _i = sys.argv.index('--port') + if _i + 1 < len(sys.argv): + PORT = int(sys.argv[_i + 1]) # Ein Band ohne Stationen — siehe die Begruendung bei `traeger`. STILL = '--still' in sys.argv +# --sendet N: alle N Sekunden den Sendezustand umschalten (trx:0,true/false). +# +# Warum als Schalter der Attrappe und nicht am echten Geraet geprueft: den +# Sendezustand am echten Geraet herzustellen heisst SENDEN, und das ist ohne +# Antenne verboten. Die Seite muss aber zeigen koennen, dass die Station +# sendet -- sonst ist der leere Wasserfall waehrend einer Durchsage nicht von +# einem Fehler zu unterscheiden. Also hier. +def _zahl_nach(flagge, standard): + if flagge in sys.argv: + i = sys.argv.index(flagge) + if i + 1 < len(sys.argv): + try: + return float(sys.argv[i + 1]) + except ValueError: + pass + return standard + return 0.0 + +SENDET_ALLE = _zahl_nach('--sendet', 6.0) IQ_RATE = 48000 # bewusst klein: die Attrappe soll die Naht pruefen, AUDIO_RATE = 48000 # nicht die Bandbreite IQ_BLOCK = 4096 # Werte je Rahmen (I und Q zusammen) -> 2048 Paare @@ -142,6 +169,8 @@ def __init__(self, sock, addr): self.audio_typ = 3 # Float32 self.phase = 0.0 self.tonphase = 0.0 + # Letzter gemeldeter Sendezustand (nur fuer --sendet). + self.sendet = False # ── Handschlag ──────────────────────────────────────────────────────── def handschlag(self): @@ -407,6 +436,16 @@ def run(self): # Eine Attrappe, die gutmuetiger ist als das Original, ist keine # Hilfe — sie verschiebt Fehler nach hinten, dorthin wo sie teurer # sind. + # Sendezustand umschalten, wenn --sendet gesetzt ist. Der echte + # Server meldet `trx:,` genauso, aus demselben Anlass + # (eigenes MOX, Mikrofontaste am Geraet, CAT). + if SENDET_ALLE > 0: + soll = int(jetzt / SENDET_ALLE) % 2 == 1 + if soll != self.sendet: + self.sendet = soll + self.sende_text(f'trx:0,{"true" if soll else "false"};') + print(f' trx:0,{"true" if soll else "false"} (--sendet)') + if self.rx_sensors_an and jetzt - smeter_zeit > self.rx_sensors_ms / 1000.0: smeter_zeit = jetzt dbm = -83 + 9 * math.sin(jetzt / 2.2) + random.gauss(0, 1.2) diff --git a/handfunke/stil.css b/handfunke/stil.css index a2dc97a77..4976fb13c 100644 --- a/handfunke/stil.css +++ b/handfunke/stil.css @@ -290,6 +290,28 @@ header .rate{font-size:var(--f-caption);color:var(--text-inactive)} color:var(--text-inactive);box-shadow:inset 0 1px 0 var(--light); } +/* ── Die Station sendet ────────────────────────────────────────────────── + Dieselbe Zeile, dieselbe Geometrie, nur in Messing: an dieser Stelle + steht sonst "SENDEN / NUR IN DER APP", und genau dort schaut man hin, + wenn man wissen will, was der Sender tut. + + Messing und nicht Rot: Rot bleibt der Warnung (Betreiber-Regel), und + Senden ist kein Fehler, sondern ein Zustand. Es ist derselbe Ton, in dem + die Seite alles Gemessene zeigt (--measured) — denn das ist es: eine + Meldung des Geraets, keine Mutmassung der Seite. + + Zweimal notiert, weil der Zusammenbau der Zeile an :has() haengt (Safari + ab 16.4): die zweite Regel greift auch ohne :has(), dann eben an den + beiden getrennten Tasten. */ +.txrow:has(.tx.locked) .tx.sendet, +.tx.locked.sendet{ + color:var(--measured); + border-color:var(--amber-dim); + background:rgba(107,86,48,.20); +} +.txrow:has(.tx.locked) .tx.sendet small, +.tx.locked.sendet small{color:var(--measured);opacity:.8} + /* ── Fußzeile ───────────────────────────────────────────────────────────── */ footer{flex:none;height:30px;display:flex;align-items:center;gap:10px; padding:0 16px;margin-top:8px}