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
61 changes: 61 additions & 0 deletions handfunke/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Sammelstelle, zeileLesen, befundLesen, zeitKurz, dupeSatz }
from './logbuch.js';
import { spotLesen, marken, trefferBei } from './spots.js';
import { Zeichenbremse } from './ton-vorrang.js';
import { bildIstAlt } from './aufwachen.js';

const $ = (id) => document.getElementById(id);
const link = new TciLink();
Expand Down Expand Up @@ -2016,6 +2017,66 @@ link.addEventListener('spectrum', (e) => {
});
link.addEventListener('ready', () => zeichneBedienung());
link.addEventListener('ready', () => spotsBaldHolen(300));

// ── Zurueck aus dem Hintergrund (2026-10-05) ──────────────────────────────
//
// Bis hierher behandelte die Seite das Entsperren GAR NICHT. Was dabei
// wirklich passiert:
//
// * Der WebSocket ist tot, aber `onclose` kommt erst, wenn das System die
// Seite wieder auftaut -- und der Wiederholungsabstand ist inzwischen
// auf 10 s gewachsen. Man sieht sekundenlang nichts, obwohl das
// Funkgeraet bereit ist.
// * Der AudioContext ist von iOS angehalten; niemand weckt ihn.
// * Wasserfall und S-Meter zeigen den Stand von VOR dem Sperren, ohne das
// zu sagen. Das ist dieselbe Gattung Luege wie ein stehender
// Wasserfall: er behauptet ein leeres Band, und danach wird nicht
// gerufen.
//
// Die Abonnements selbst muessen hier nicht erneuert werden -- sie haengen
// am `open`-Ereignis und kommen mit der neuen Verbindung von selbst.
let warVerborgen = false;

function aufwachen() {
const stille = performance.now() - letzteIq;

// 1. Das Bild als alt kennzeichnen, BEVOR etwas anderes passiert. Der
// Strich ist derselbe, den ein Bandwechsel zieht: oben das Neue, unten
// das Alte. Ohne ihn sieht ein zehn Minuten alter Wasserfall aus wie
// ein lebendiger.
if (bildIstAlt(stille)) {
wasserfallSchnitt();
state.hatSpektrumstrom = false;
}

// 2. Sofort neu verbinden, wenn noetig (Entscheidung in aufwachen.js).
link.nachDemAufwachen(stille);

// 3. Den Ton wecken. iOS haelt den AudioContext beim Sperren an, und ein
// angehaltener Kontext ruft seinen Rueckruf nie wieder auf -- der Ton
// bliebe stumm, bis jemand TON AUS und wieder EIN drueckt.
if (state.audio && state.audio.state === 'suspended') {
state.audio.resume().catch(() => { /* ohne Zutun des Bedieners nicht erlaubt */ });
}

// 4. Die Spots gehoeren ebenfalls aufgefrischt: waehrend des Schlafens
// sind welche abgelaufen und neue gekommen.
spotsBaldHolen(400);
}

document.addEventListener('visibilitychange', () => {
if (document.hidden) { warVerborgen = true; return; }
if (!warVerborgen) { return; }
warVerborgen = false;
aufwachen();
});

// Nicht jedes System meldet `visibilitychange` zuverlaessig, wenn die Seite
// aus dem Ruhezustand kommt -- `pageshow` mit `persisted` ist der zweite
// Weg, und `focus` der dritte. Mehrfaches Aufwachen schadet nicht: die
// Entscheidung in aufwachen.js wirft eine lebendige Verbindung nicht weg.
window.addEventListener('pageshow', (e) => { if (e.persisted) { aufwachen(); } });
window.addEventListener('focus', () => { if (warVerborgen) { warVerborgen = false; aufwachen(); } });
link.addEventListener('state', () => {
// Der Server hat das letzte Wort über die Rate. Weicht sie von unserer
// Bitte ab, muss das Worklet es erfahren — sonst stimmt die Tonhöhe nicht.
Expand Down
59 changes: 59 additions & 0 deletions handfunke/aufwachen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Zurück aus dem Hintergrund — die Entscheidung, rein.
//
// Was auf einem Telefon beim Entsperren wirklich passiert, und was die
// Seite bis heute davon unbemerkt lässt:
//
// * Der WebSocket ist tot, aber `onclose` kommt erst, wenn das System
// die Seite wieder auftaut. Bis dahin steht der Wiederholungsabstand
// womöglich schon bei 10 s — der Bediener sieht also sekundenlang
// nichts, obwohl das Funkgerät bereit ist.
//
// * Schlimmer: der Socket kann als OFFEN gemeldet werden und trotzdem
// tot sein. iOS friert die Seite ein, die Gegenseite räumt auf, und
// `readyState` hinkt hinterher. Wer nur auf den Zustand sieht,
// wartet auf ein `onclose`, das nie kommt.
//
// Darum entscheidet hier nicht der Zustand allein, sondern der Zustand
// UND die Stille: kommt seit Sekunden nichts mehr an, wird neu verbunden,
// ganz gleich was der Socket behauptet. Eine überflüssige Neuverbindung
// kostet eine halbe Sekunde; eine ausgelassene kostet den Moment, in dem
// man das Telefon in die Hand genommen hat.

/** Ab dieser Stille gilt eine Verbindung als tot, auch wenn sie OFFEN sagt. */
export const kTotStilleMs = 3000;

/** WebSocket-Zustände, hier benannt statt als Zahl verstreut. */
export const VERBINDET = 0, OFFEN = 1, SCHLIESST = 2, ZU = 3;

/**
* Soll nach dem Aufwachen sofort neu verbunden werden?
*
* @param {{gewollt:boolean, zustand:(number|null), stilleMs:number}} lage
* `zustand` ist `WebSocket.readyState` oder null, wenn gar keiner da ist.
* `stilleMs` ist, wie lange schon nichts mehr ankam.
*/
export function sollNeuVerbinden(lage) {
const l = lage || {};
// Wer gar nicht verbunden sein will, will es auch nach dem Aufwachen nicht.
if (!l.gewollt) { return false; }
const z = (l.zustand === null || l.zustand === undefined) ? ZU : l.zustand;
if (z === ZU || z === SCHLIESST) { return true; }
// VERBINDET: einen laufenden Versuch nicht abwürgen, SOLANGE er jung ist.
// Hängt er schon länger als die Stillegrenze, ist er mit dem Telefon
// eingeschlafen und wird von selbst nicht mehr wach.
if (z === VERBINDET) { return (l.stilleMs || 0) > kTotStilleMs; }
// OFFEN: nur bei Stille. Sonst würde jeder Blick auf den Bildschirm die
// laufende Verbindung wegwerfen.
return (l.stilleMs || 0) > kTotStilleMs;
}

/**
* Ist das Bild, das gerade dasteht, noch von vor dem Schlafen?
*
* Darauf kommt es an: ein Wasserfall, der den Stand von vor zehn Minuten
* zeigt, ist von einem lebendigen nicht zu unterscheiden. Er behauptet ein
* leeres Band, und danach wird nicht gerufen.
*/
export function bildIstAlt(stilleMs, grenzeMs = 1500) {
return (stilleMs || 0) > grenzeMs;
}
63 changes: 63 additions & 0 deletions handfunke/pruefe-aufwachen.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env node
// Pruefstand fuer das Zurueckkommen aus dem Hintergrund.
//
// Der Fehler, gegen den das meiste hier steht, ist der LEISE: ein Socket,
// der OFFEN meldet und trotzdem tot ist. Wer nur auf `readyState` sieht,
// wartet auf ein `onclose`, das nie kommt -- und der Bediener steht vor
// einem Bild von vorhin, das wie ein leeres Band aussieht.
//
// node pruefe-aufwachen.mjs

import { sollNeuVerbinden, bildIstAlt, kTotStilleMs,
VERBINDET, OFFEN, SCHLIESST, ZU } from './aufwachen.js';

let fehler = 0;
const pruefe = (name, ok, text = '') => {
if (ok) { console.log(` ok ${name}`); }
else { console.log(` FEHLT ${name}${text ? ': ' + text : ''}`); fehler++; }
};

console.log('Neu verbinden?\n');
{
const l = (zustand, stilleMs, gewollt = true) => ({ gewollt, zustand, stilleMs });
pruefe('geschlossen -> ja', sollNeuVerbinden(l(ZU, 0)) === true);
pruefe('schliesst gerade -> ja', sollNeuVerbinden(l(SCHLIESST, 0)) === true);
pruefe('gar kein Socket -> ja', sollNeuVerbinden(l(null, 0)) === true);

// DER Fall: offen, aber seit Sekunden still.
pruefe('offen und still -> ja (der tote Socket)',
sollNeuVerbinden(l(OFFEN, kTotStilleMs + 1)) === true);
pruefe('offen und lebendig -> nein',
sollNeuVerbinden(l(OFFEN, 200)) === false);
pruefe('offen, Stille genau an der Grenze -> nein',
sollNeuVerbinden(l(OFFEN, kTotStilleMs)) === false);

// Ein laufender Versuch darf nicht bei jedem Blick abgewuergt werden.
pruefe('verbindet gerade, frisch -> nein',
sollNeuVerbinden(l(VERBINDET, 500)) === false);
pruefe('verbindet gerade, aber eingeschlafen -> ja',
sollNeuVerbinden(l(VERBINDET, kTotStilleMs + 1)) === true);

// Wer getrennt sein WILL, bleibt getrennt.
pruefe('nicht gewollt -> nie', sollNeuVerbinden(l(ZU, 99999, false)) === false);
pruefe('nicht gewollt, auch nicht bei Stille',
sollNeuVerbinden(l(OFFEN, 99999, false)) === false);
pruefe('ohne Angaben wird nichts weggeworfen', sollNeuVerbinden(null) === false);
}

console.log('\nIst das Bild alt?');
{
pruefe('frisch -> nein', bildIstAlt(100) === false);
pruefe('zwei Sekunden Stille -> ja', bildIstAlt(2000) === true);
pruefe('an der Grenze -> nein', bildIstAlt(1500) === false);
pruefe('nichts gemessen -> nein', bildIstAlt(undefined) === false);
// Die Grenze liegt UNTER der fuer die Neuverbindung: lieber einmal zu
// frueh sagen "das Bild ist von vorhin" als einmal zu spaet. Ein falsch
// beschriftetes altes Bild ist harmlos, ein unbeschriftetes nicht.
pruefe('Bild gilt frueher als alt als der Socket als tot',
bildIstAlt(2000) === true && sollNeuVerbinden(
{ gewollt: true, zustand: OFFEN, stilleMs: 2000 }) === false);
}

console.log(fehler === 0 ? '\nAlles gut.' : `\n${fehler} Punkt(e) offen.`);
process.exit(fehler === 0 ? 0 : 1);
26 changes: 26 additions & 0 deletions handfunke/tci.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

export const HDR = 64;

import { sollNeuVerbinden } from './aufwachen.js';

export class TciLink extends EventTarget {
constructor() {
super();
Expand Down Expand Up @@ -146,6 +148,30 @@ export class TciLink extends EventTarget {
};
}

/**
* Zurueck aus dem Hintergrund: sofort wieder versuchen, ohne den
* gewachsenen Abstand abzuwarten.
*
* Entschieden wird in aufwachen.js -- und zwar NICHT am `readyState`
* allein: iOS friert die Seite ein, die Gegenseite raeumt auf, und der
* Socket meldet noch OFFEN. Wer darauf wartet, dass `onclose` kommt,
* wartet unter Umstaenden ewig.
*
* @param {number} stilleMs wie lange schon nichts mehr ankam
* @returns {boolean} true, wenn neu verbunden wurde
*/
nachDemAufwachen(stilleMs) {
const zustand = this.ws ? this.ws.readyState : null;
if (!sollNeuVerbinden({ gewollt: this.wanted, zustand, stilleMs })) {
return false;
}
// Den Abstand zuruecksetzen: er ist beim Schlafen gewachsen, und das
// Telefon ist gerade JETZT in der Hand.
this.retryMs = 500;
this._open();
return true;
}

_retry() {
if (!this.wanted) return;
clearTimeout(this.retryTimer);
Expand Down
Loading