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}