Compare commits

..
2 Commits
Author SHA1 Message Date
rouggy e20f32c918 chore(changelog): the late-decode fix opens 0.27.14
It went in after the 0.27.13 release commit, so the shipped build does not
contain it — and the entry had been folded into that block's fixes line,
where it claimed a fix nobody had. The 0.27.13 block is back to what was
released.
2026-09-06 06:56:01 +02:00
rouggy 71adbfd8ff fix(autocall): a late decode belongs to its own period
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.
2026-09-06 00:31:16 +02:00
3 changed files with 48 additions and 5 deletions
+5
View File
@@ -789,6 +789,11 @@ type App struct {
acLastJudge time.Time acLastJudge time.Time
// acTXPeriod is the last transmit period already counted — see autoCallNoteTX. // acTXPeriod is the last transmit period already counted — see autoCallNoteTX.
acTXPeriod string 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. // 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. // The panel owns the filters; this is its answer, not a second copy of them.
acVisible map[string]bool acVisible map[string]bool
+33 -5
View File
@@ -160,6 +160,7 @@ func (a *App) applyAutoCall() {
e.Reset() e.Reset()
a.acMu.Lock() a.acMu.Lock()
a.acPeriod, a.acBuf = nil, nil a.acPeriod, a.acBuf = nil, nil
a.acJudged, a.acDirty = nil, nil
a.acMu.Unlock() a.acMu.Unlock()
} }
a.emitAutoCall() a.emitAutoCall()
@@ -279,17 +280,37 @@ func (a *App) autoCallFeed(d autocall.Decode) {
if a.acPeriod == nil { 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.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.acFed = map[string]time.Time{}
a.acJudged, a.acDirty = map[string]string{}, map[string]bool{}
} }
if prev := a.acPeriod[inst]; prev != "" && prev != key { if prev := a.acPeriod[inst]; prev != "" && prev != key {
prevAt, prevTR, buf := a.acAt[inst], a.acTR[inst], a.acBuf[inst] 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.acPeriod[inst], a.acAt[inst], a.acTR[inst] = key, d.At, d.TRPeriod
a.acBuf[inst] = []acDecode{{d: d}} a.acBuf[inst] = []acDecode{{d: d}}
a.acFed[inst], a.acDirty[inst] = time.Now(), true
a.acMu.Unlock() 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 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.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.acBuf[inst] = append(a.acBuf[inst], acDecode{d: d})
a.acMu.Unlock() a.acMu.Unlock()
} }
@@ -344,9 +365,14 @@ func (a *App) autoCallSweep() {
if time.Since(a.acFed[inst]) < acQuiet { if time.Since(a.acFed[inst]) < acQuiet {
continue 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]}) ready = append(ready, due{inst, key, a.acAt[inst], tr, a.acBuf[inst]})
delete(a.acPeriod, inst) a.acJudged[inst], a.acDirty[inst] = key, false
delete(a.acBuf, inst)
} }
a.acMu.Unlock() a.acMu.Unlock()
for _, d := range ready { for _, d := range ready {
@@ -369,7 +395,9 @@ func (a *App) autoCallSilence() {
return return
} }
a.acMu.Lock() 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] tr := a.acTR[inst]
a.acMu.Unlock() a.acMu.Unlock()
if !quiet { if !quiet {
+10
View File
@@ -1,4 +1,14 @@
[ [
{
"version": "0.27.14",
"date": "",
"en": [
"Auto-call sees a decode that arrives after the others. A decoder sends a period in a burst and stragglers follow — a deep decode a second behind the rest — and the straggler was judged on its own, with the thirty stations of its own period nowhere in sight. The period now stays open until the next one starts, and a late arrival is weighed against all of it."
],
"fr": [
"Lauto-call voit un décodage qui arrive après les autres. Un décodeur envoie une période en rafale, puis les retardataires — un décodage « deep » une seconde plus tard — et le retardataire était jugé tout seul, sans les trente stations de sa propre période. La période reste maintenant ouverte jusquau début de la suivante, et un arrivant tardif est pesé face à lensemble."
]
},
{ {
"version": "0.27.13", "version": "0.27.13",
"date": "", "date": "",