Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions handfunke/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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';
}
Expand All @@ -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 <small>EMPFÄNGER STUMM</small>'
: 'STATION SENDET <small>EMPFÄNGER STUMM</small>')
: 'SENDEN <small>NUR IN DER APP</small>';
}
requestAnimationFrame(schleife);
}
requestAnimationFrame(schleife);
Expand Down
39 changes: 39 additions & 0 deletions handfunke/attrappe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:<rx>,<bool>` 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)
Expand Down
22 changes: 22 additions & 0 deletions handfunke/stil.css
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading