fix(autocall): rest in overs, no phantom miss, and a readable auto readout
The rest between two series was two minutes — four overs on FT8, by which time the DX has worked four other callers and half the time has gone. It is now counted in the station's OWN overs and is one by default: seven calls, one over listened through, and it goes again if the station is still there. The deadline lands mid-cycle on purpose, so "sit out one over" means one over rather than depending on a millisecond of clock skew. A period the target was DECODED in is never counted as a miss. A period is judged more than once — decodes arrive in a burst and stragglers follow — and a later judgement holds a partial view of it, not evidence of absence: D2ACE answered in the very period the counter then read as a miss. The readout moved out of the Auto button and beside it. "Auto D2ACE 1/7 ·1/3" read as one number, and the control changed width every period. The callsign is now the biggest thing on the row, the calls and the missed periods each carry a label, the miss count appears only once there is one, and a station being waited for gets its own amber chip.
This commit is contained in:
@@ -169,11 +169,17 @@ type Settings struct {
|
||||
// Misses is how many of the station's OWN transmit periods may pass with no
|
||||
// decode of it before it is given up on.
|
||||
Misses int
|
||||
// Rest is how long a released callsign waits before it can be picked again,
|
||||
// and MaxRounds how many such series it gets before being parked for the
|
||||
// session.
|
||||
Rest time.Duration
|
||||
MaxRounds int
|
||||
// RestPeriods is how many of the station's OWN OVERS a released callsign sits
|
||||
// out before it can be picked again, and MaxRounds how many such series it
|
||||
// gets before being parked for the session.
|
||||
//
|
||||
// Counted in overs, not in minutes, because that is the unit the thing is
|
||||
// happening in: an operator who has called a DX seven times without an answer
|
||||
// listens through one of its transmissions and calls again if it is still
|
||||
// there. Two minutes is four overs on FT8 — the DX has worked four other
|
||||
// callers by then, and half the time it has gone.
|
||||
RestPeriods int
|
||||
MaxRounds int
|
||||
// MaxHold is the wall-clock backstop on one target.
|
||||
MaxHold time.Duration
|
||||
// OnScreenOnly restricts the calling to what the decodes panel is actually
|
||||
@@ -195,7 +201,7 @@ type Settings struct {
|
||||
func Defaults() Settings {
|
||||
return Settings{
|
||||
Attempts: 7, WatchedAttempts: 15, Misses: 3,
|
||||
Rest: 2 * time.Minute, MaxRounds: 3, MaxHold: 4 * time.Minute,
|
||||
RestPeriods: 1, MaxRounds: 3, MaxHold: 4 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +216,8 @@ func (s Settings) withDefaults() Settings {
|
||||
if s.Misses <= 0 {
|
||||
s.Misses = d.Misses
|
||||
}
|
||||
if s.Rest <= 0 {
|
||||
s.Rest = d.Rest
|
||||
if s.RestPeriods <= 0 {
|
||||
s.RestPeriods = d.RestPeriods
|
||||
}
|
||||
if s.MaxRounds <= 0 {
|
||||
s.MaxRounds = d.MaxRounds
|
||||
@@ -284,6 +290,12 @@ type Engine struct {
|
||||
heldSince time.Time
|
||||
attempts int
|
||||
misses int
|
||||
// seenKey is the period in which the target was last DECODED. A period is
|
||||
// judged more than once — the decodes arrive in bursts and stragglers follow —
|
||||
// and a later judgement holds a partial view of it, not evidence of absence:
|
||||
// the station answered in that very period and the counter still read one
|
||||
// miss out of three.
|
||||
seenKey string
|
||||
lastMiss string // period already counted, so one period counts once
|
||||
txSlot int // which of the two slots the target transmits in; -1 unknown
|
||||
stopped bool // an explicit "Only" target gave up: needs a restart
|
||||
@@ -735,7 +747,7 @@ func (e *Engine) OnPeriod(p Period) Action {
|
||||
// The decode is refreshed so a reply carries a current timestamp; the
|
||||
// hold clock is NOT — see heldSince.
|
||||
e.target = seen
|
||||
e.misses, e.lastMiss = 0, ""
|
||||
e.misses, e.lastMiss, e.seenKey = 0, "", p.Key
|
||||
e.txSlot = slotOf(p.At, periodSecs(p, *seen))
|
||||
// In QSO: the decoder is sequencing the exchange on its own and the
|
||||
// attempt counter has done its job. Interrupting it with another
|
||||
@@ -780,6 +792,10 @@ func (e *Engine) OnPeriod(p Period) Action {
|
||||
if p.TX.Transmitting {
|
||||
return Action{}
|
||||
}
|
||||
// Nor a period it was already decoded in: see seenKey.
|
||||
if e.seenKey == p.Key {
|
||||
return Action{}
|
||||
}
|
||||
if e.txSlot >= 0 && slotOf(p.At, periodSecs(p, t)) != e.txSlot {
|
||||
return Action{}
|
||||
}
|
||||
@@ -1094,7 +1110,7 @@ func (e *Engine) preempt(p Period) (Action, bool) {
|
||||
// is picked again like any other the moment it is.
|
||||
func (e *Engine) drop() {
|
||||
e.target, e.attempts, e.misses, e.txSlot, e.lastMiss = nil, 0, 0, -1, ""
|
||||
e.targetInst, e.answered = "", false
|
||||
e.targetInst, e.answered, e.seenKey = "", false, ""
|
||||
}
|
||||
|
||||
// addressee is who a message is being sent to — the first token, which is the
|
||||
@@ -1133,7 +1149,7 @@ func (e *Engine) giveUp(call string, t Candidate) {
|
||||
if at.IsZero() {
|
||||
at = time.Now()
|
||||
}
|
||||
e.rested[call] = at.Add(e.set.Rest)
|
||||
e.rested[call] = at.Add(restFor(e.set.RestPeriods, candidateSecs(t)))
|
||||
// An explicit "call this station" that runs out of attempts stops the
|
||||
// feature instead of moving on. There is nothing else it was asked to do.
|
||||
if strings.TrimSpace(e.set.Only) != "" {
|
||||
@@ -1235,6 +1251,28 @@ func workedNow(decodes []Candidate, call string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// restFor turns a number of the station's own overs into a deadline.
|
||||
//
|
||||
// A station transmits every OTHER slot, so one of its overs is two T/R periods —
|
||||
// and the extra half-cycle is what makes the answer stable: without it the
|
||||
// deadline lands exactly on the station's next transmission, where a
|
||||
// millisecond of clock skew decides whether it is called or passed over. Landing
|
||||
// it mid-cycle makes "sit out one over" mean one over, every time.
|
||||
func restFor(overs, trSec int) time.Duration {
|
||||
if overs <= 0 {
|
||||
return time.Duration(trSec) * time.Second // no rest: its next over will do
|
||||
}
|
||||
return time.Duration(overs*2*trSec+trSec) * time.Second
|
||||
}
|
||||
|
||||
// candidateSecs is the station's own T/R period in seconds, defaulting to FT8's.
|
||||
func candidateSecs(c Candidate) int {
|
||||
if c.TRPeriod > 0 {
|
||||
return c.TRPeriod
|
||||
}
|
||||
return 15
|
||||
}
|
||||
|
||||
func periodSecs(p Period, c Candidate) int {
|
||||
if c.TRPeriod > 0 {
|
||||
return c.TRPeriod
|
||||
|
||||
Reference in New Issue
Block a user