From 71adbfd8ff7e806007985edbb342b6e4a2dbd755 Mon Sep 17 00:00:00 2001 From: rouggy Date: Sun, 6 Sep 2026 00:31:16 +0200 Subject: [PATCH] fix(autocall): a late decode belongs to its own period MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A decoder sends a period's decodes in a burst, and stragglers follow — a deep decode a second behind the rest. The sweeper judged the burst and CLEARED the buffer, so the straggler opened a fresh one under the same period key and was judged on its own: the ladder applied to a handful of late arrivals with the other thirty stations of that period nowhere in sight, and often after the reply to the burst had already put us on the air, where nothing can act on it at all. The buffer now outlives the judgement. A period stays open until a decode stamped with the NEXT slot arrives; a straggler appends to it and the period is judged again, whole. Judged once per period otherwise — acDirty says whether anything new has come in — and the same flag now answers the dead-band case that the cleared buffer used to stand for. --- app.go | 5 +++++ autocall.go | 38 +++++++++++++++++++++++++++++++++----- changelog.json | 4 ++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app.go b/app.go index e94deb6..e4e248c 100644 --- a/app.go +++ b/app.go @@ -789,6 +789,11 @@ type App struct { acLastJudge time.Time // acTXPeriod is the last transmit period already counted — see autoCallNoteTX. acTXPeriod string + // acJudged is the last period key each receiver has been judged on, and + // acDirty says decodes have arrived for it since. Together they let a period + // be judged AGAIN when a straggler turns up, with the whole period in hand. + acJudged map[string]string + acDirty map[string]bool // What the decodes panel is SHOWING, and whether it is publishing at all. // The panel owns the filters; this is its answer, not a second copy of them. acVisible map[string]bool diff --git a/autocall.go b/autocall.go index aeed512..0ccaada 100644 --- a/autocall.go +++ b/autocall.go @@ -160,6 +160,7 @@ func (a *App) applyAutoCall() { e.Reset() a.acMu.Lock() a.acPeriod, a.acBuf = nil, nil + a.acJudged, a.acDirty = nil, nil a.acMu.Unlock() } a.emitAutoCall() @@ -279,17 +280,37 @@ func (a *App) autoCallFeed(d autocall.Decode) { if a.acPeriod == nil { a.acPeriod, a.acAt, a.acTR, a.acBuf = map[string]string{}, map[string]time.Time{}, map[string]int{}, map[string][]acDecode{} a.acFed = map[string]time.Time{} + a.acJudged, a.acDirty = map[string]string{}, map[string]bool{} } if prev := a.acPeriod[inst]; prev != "" && prev != key { prevAt, prevTR, buf := a.acAt[inst], a.acTR[inst], a.acBuf[inst] + // Only if something in it has NOT been judged. The buffer now outlives + // the judgement (see below), so without this the arrival of the next + // period would judge the previous one a second time on the very same + // decodes — and act on them, a slot late. + pending := a.acDirty[inst] || a.acJudged[inst] != prev a.acPeriod[inst], a.acAt[inst], a.acTR[inst] = key, d.At, d.TRPeriod a.acBuf[inst] = []acDecode{{d: d}} + a.acFed[inst], a.acDirty[inst] = time.Now(), true a.acMu.Unlock() - a.autoCallJudge(inst, prev, prevAt, prevTR, buf) + // The previous period ends here whatever the sweeper was going to do: a + // decode stamped with the next slot is proof the old one is over. + if pending { + a.autoCallJudge(inst, prev, prevAt, prevTR, buf) + } return } + // APPENDED, never restarted. The buffer survives the period being judged, + // so a decode that arrives after the others belongs to the same period and + // is weighed against all of them. + // + // It used to be dropped and then judged on its own: the sweeper cleared the + // buffer, a straggler opened a "new" one under the same key, and the ladder + // was applied to whatever handful had come late — with the other thirty + // stations of that period nowhere in sight. A deep decode arriving a second + // after the burst is exactly the station worth calling. a.acPeriod[inst], a.acAt[inst], a.acTR[inst] = key, d.At, d.TRPeriod - a.acFed[inst] = time.Now() + a.acFed[inst], a.acDirty[inst] = time.Now(), true a.acBuf[inst] = append(a.acBuf[inst], acDecode{d: d}) a.acMu.Unlock() } @@ -344,9 +365,14 @@ func (a *App) autoCallSweep() { if time.Since(a.acFed[inst]) < acQuiet { continue } + // Judged once per period, and again only when something new has come in + // for it. The period itself is kept open until a decode from the NEXT one + // arrives, so a straggler is judged with the whole period behind it. + if a.acJudged[inst] == key && !a.acDirty[inst] { + continue + } ready = append(ready, due{inst, key, a.acAt[inst], tr, a.acBuf[inst]}) - delete(a.acPeriod, inst) - delete(a.acBuf, inst) + a.acJudged[inst], a.acDirty[inst] = key, false } a.acMu.Unlock() for _, d := range ready { @@ -369,7 +395,9 @@ func (a *App) autoCallSilence() { return } a.acMu.Lock() - quiet := a.acPeriod[inst] == "" && time.Since(a.acLastJudge) > 20*time.Second + // Nothing pending for this receiver, and nothing judged for a while: the + // band has gone quiet under it. + quiet := !a.acDirty[inst] && time.Since(a.acLastJudge) > 20*time.Second tr := a.acTR[inst] a.acMu.Unlock() if !quiet { diff --git a/changelog.json b/changelog.json index d525b39..ccf86e8 100644 --- a/changelog.json +++ b/changelog.json @@ -18,7 +18,7 @@ "Auto-call: what it calls, and in what order. It answers only what the decodes list is SHOWING — the filters above it (CQ only, LoTW only, the category chips, continents, minimum report, search) now steer the transmitter as well as the eye, and the separate “LoTW users only” option is gone. A new prefix, county, state, square or park is worth a call too, at the foot of the ladder and only for the kinds you chase (Settings → DX Cluster). A watched callsign outranks everything not on the list, a real need beats an unconfirmed one at the same level, and a better station may take over from one that has not answered yet — never from a QSO in progress.", "Auto-call: it calls THROUGH a pileup. It used to give up the moment the station it was calling answered somebody else — which is precisely how a DX with a queue behaves, and the only way to be the next one is to keep calling while it works the others. Still bounded by the call and miss counters, and a station in mid-exchange is still never CHOSEN as a new target.", "Auto-call: safety and control. It is always OFF at launch and after a profile switch, never armed from a stored setting — it is the one feature that puts the station on the air by itself, and OpsLog starts with Windows. Halt is now a verdict: the station is set aside for the session and never called again until auto-call is switched off and on. It says what it is waiting for, showing a wanted station that is working somebody else next to the Auto button. And it can log every decision (Settings → DXHunter): one line per period saying what was on the air and why each station was refused.", - "Auto-call: a round of fixes from on-air use. It no longer cuts your own 73 short, calls four to six seconds late, starts a call and drops it a second later, ignores a station calling you, misreads MSHV’s two-answers-in-one-line, refuses nearly everything on the air because it read the entity’s “worked” as the station’s, or opens with two misses already counted against a station it has only just picked.", + "Auto-call: a round of fixes from on-air use. It no longer cuts your own 73 short, calls four to six seconds late, starts a call and drops it a second later, ignores a station calling you, misreads MSHV’s two-answers-in-one-line, refuses nearly everything on the air because it read the entity’s “worked” as the station’s, opens with two misses already counted against a station it has only just picked, or loses sight of a decode that arrives after the others — a straggler used to be judged on its own, with the thirty stations of its own period nowhere in sight.", "Switching profile no longer leaves the previous logbook’s verdicts on the screen. Coming back from a profile with an empty log, every decode and spot stayed badged NEW until a restart: the worked-index, the chase-new list and the cached verdicts are now all dropped when the logbook changes.", "Chase new fixes: the header counts both what the filters show and what the panel holds (“3 of 12 heard”), a stored filter set that hides everything is ignored, and switching between “new” and “new + unconfirmed” re-reads the list instead of leaving old verdicts on screen.", "PSK Reporter panel fixes: the history is asked for in both feed scopes and refreshed every five minutes, so a target picked shortly after start-up no longer shows an empty page; the suggested transmit offset always has an answer when there is data; and the texts say ten minutes, which is the window actually used.", @@ -40,7 +40,7 @@ "Auto-call : ce qu’il appelle, et dans quel ordre. Il ne répond qu’à ce que la liste des décodes AFFICHE — les filtres du dessus (CQ seul, LoTW seul, les pastilles de catégorie, les continents, le rapport minimum, la recherche) pilotent désormais l’émission autant que l’œil, et l’option distincte « utilisateurs LoTW uniquement » disparaît. Un nouveau préfixe, comté, état, locator ou parc vaut aussi un appel, au dernier barreau de l’échelle et seulement pour ce que vous chassez (Réglages → DX Cluster). Un indicatif en watchlist passe devant tout ce qui n’y est pas, un vrai besoin devant un besoin non confirmé de même niveau, et une meilleure station peut prendre la place d’une autre qui n’a pas encore répondu — jamais celle d’un QSO en cours.", "Auto-call : il appelle À TRAVERS un pile-up. Il abandonnait dès que la station appelée répondait à quelqu’un d’autre — or c’est exactement ce que fait un DX avec une file, et le seul moyen d’être le suivant est de continuer à appeler pendant qu’il travaille les autres. Toujours borné par les compteurs d’appels et de ratés, et une station en plein échange n’est toujours jamais CHOISIE comme nouvelle cible.", "Auto-call : sécurité et contrôle. Il est toujours ARRÊTÉ au lancement et après un changement de profil, jamais armé depuis un réglage enregistré — c’est la seule fonction qui met la station en émission toute seule, et OpsLog démarre avec Windows. Halt devient un verdict : la station est mise de côté pour la session et n’est plus appelée jusqu’à ce que l’auto-call soit désactivé puis réactivé. Il dit ce qu’il attend, en affichant à côté du bouton Auto la station voulue qui travaille quelqu’un d’autre. Et il peut journaliser chaque décision (Réglages → DXHunter) : une ligne par période disant ce qui était sur l’air et pourquoi chaque station a été refusée.", - "Auto-call : série de corrections issues du trafic. Il ne coupe plus votre 73, n’appelle plus avec quatre à six secondes de retard, ne lance plus un appel pour le couper une seconde après, ne laisse plus sans réponse une station qui vous appelle, lit correctement les doubles réponses MSHV sur une même ligne, ne refuse plus presque tout ce qui passe parce qu’il prenait le « worked » de l’entité pour celui de la station, et ne démarre plus avec deux ratés au compteur sur une station tout juste choisie.", + "Auto-call : série de corrections issues du trafic. Il ne coupe plus votre 73, n’appelle plus avec quatre à six secondes de retard, ne lance plus un appel pour le couper une seconde après, ne laisse plus sans réponse une station qui vous appelle, lit correctement les doubles réponses MSHV sur une même ligne, ne refuse plus presque tout ce qui passe parce qu’il prenait le « worked » de l’entité pour celui de la station, ne démarre plus avec deux ratés au compteur sur une station tout juste choisie, et ne perd plus de vue un décodage qui arrive après les autres — un retardataire était jugé tout seul, sans les trente stations de sa propre période.", "Changer de profil ne laisse plus les verdicts du carnet précédent à l’écran. En revenant d’un profil au log vide, tous les décodes et spots restaient marqués NEW jusqu’au redémarrage : l’index des contacts, la liste chase new et les verdicts en cache sont désormais vidés au changement de carnet.", "Chase new, corrections : l’en-tête compte à la fois ce que les filtres montrent et ce que le panneau contient (« 3 sur 12 entendues »), un jeu de filtres enregistré qui cache tout est ignoré, et le passage entre « new » et « new + non confirmés » relit la liste au lieu de laisser d’anciens verdicts à l’écran.", "Panneau PSK Reporter, corrections : l’historique est demandé dans les deux portées du flux et rafraîchi toutes les cinq minutes, donc une cible choisie peu après le démarrage n’affiche plus une page vide ; l’offset d’émission suggéré donne toujours une réponse quand il y a des données ; et les textes annoncent dix minutes, la fenêtre réellement utilisée.",