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}