fix(lotw): stop reporting suppressed QSOs as uploaded
Reported: ten fresh contacts upload and appear on LoTW; twenty older ones,
bulk-edited to exactly the same LOTW_SENT/RCVD state, are accepted by OpsLog
and never arrive. The operator's reading was that a failed attempt could not be
overridden. It is simpler and worse than that.
TQSL's exit codes, from its own cmdline documentation:
8 NO QSOs were processed — already uploaded OR OUT OF DATE RANGE
9 some processed, some ignored — same two reasons
14 some already uploaded, the rest signed
8 and 9 were both read as plain success. So on 8 — nothing uploaded at all —
OpsLog announced "already uploaded (duplicate)" and stamped every selected
contact as sent. They were never on LoTW and now looked as if they were, which
is exactly the reported symptom, and it is not recoverable by re-uploading
because the operator has no reason to try.
"Out of date range" is the cause that bites here: a contact older than the
callsign certificate's validity is silently left out. Older QSOs failing while
today's succeed is the signature.
Now: 8 is a failure and nothing is stamped — a duplicate left at "R" is
harmless and will be refused again, whereas a contact wrongly marked sent is
one nobody will look at twice. 9 and 14 succeed but carry Ignored, and the
caller says so in the console and a toast.
TQSL's own sentence ("20 QSO records are out of date range") is captured and
shown. It was being read and thrown away, and it is the whole answer to "why is
my contact not on LoTW".
This commit is contained in:
@@ -142,4 +142,10 @@ type UploadResult struct {
|
||||
OK bool // the service accepted (or already had) the QSO
|
||||
LogID string // service-assigned record id, when provided
|
||||
Message string // human-readable detail (reason on failure)
|
||||
// Ignored is set when the service accepted the submission but left some or
|
||||
// all of the QSOs out of it. TQSL does this for contacts already uploaded
|
||||
// AND for contacts outside the certificate's date range, and reports both
|
||||
// with the same exit code — so the caller must show the message rather than
|
||||
// decide on its own that everything went through.
|
||||
Ignored bool
|
||||
}
|
||||
|
||||
+51
-4
@@ -178,8 +178,12 @@ func fileExists(p string) bool {
|
||||
//
|
||||
// tqsl -d -x -a all -l "<location>" -u [-p <keypass>] <file.adi>
|
||||
//
|
||||
// Exit codes (TQSL): 0 = uploaded; 8 = nothing new (all duplicates/out of
|
||||
// range); 9 = some uploaded, some skipped; anything else = failure.
|
||||
// Exit codes are TQSL's own (see the table in its cmdline help). 8 and 9 are
|
||||
// the ones that matter and both used to be read as plain success: 8 means NO
|
||||
// QSOs were processed and 9 means some were left out — in each case because
|
||||
// they were already uploaded OR outside the callsign certificate's date range.
|
||||
// Reporting either as success is how a contact came to be stamped "uploaded"
|
||||
// while LoTW had never seen it.
|
||||
func UploadLoTW(ctx context.Context, cfg ServiceConfig, tempDir, adifRecord string) (UploadResult, error) {
|
||||
tqsl := strings.TrimSpace(cfg.TQSLPath)
|
||||
loc := strings.TrimSpace(cfg.StationLocation)
|
||||
@@ -249,11 +253,31 @@ func UploadLoTW(ctx context.Context, cfg ServiceConfig, tempDir, adifRecord stri
|
||||
}
|
||||
}
|
||||
|
||||
// TQSL's exit codes, from its own cmdline documentation. Two of them used to
|
||||
// be read as plain success, and that is how contacts came to be stamped
|
||||
// "uploaded" while LoTW had never seen them:
|
||||
//
|
||||
// 8 NO QSOs were processed — already uploaded OR OUT OF DATE RANGE
|
||||
// 9 some processed, some ignored — same two reasons
|
||||
// 14 some already uploaded, the rest signed
|
||||
//
|
||||
// "Out of date range" is the one that bites: a contact older than the
|
||||
// callsign certificate's validity is silently left out, and reporting that as
|
||||
// success stamped it sent for ever. TQSL says which case it is in its output,
|
||||
// so the message is carried up rather than replaced with a guess.
|
||||
switch code {
|
||||
case 0, 9:
|
||||
case 0:
|
||||
return UploadResult{OK: true, Message: "uploaded to LoTW"}, nil
|
||||
case 9, 14:
|
||||
return UploadResult{OK: true, Ignored: true, Message: tqslDetail(msg,
|
||||
"uploaded — but TQSL left some contacts out (already uploaded, or outside the certificate's date range)")}, nil
|
||||
case 8:
|
||||
return UploadResult{OK: true, Message: "already uploaded (duplicate)"}, nil
|
||||
// Nothing reached LoTW. NOT stamped as sent: a duplicate left at "R" is
|
||||
// harmless and will be refused again, while a contact wrongly marked sent
|
||||
// is one the operator will never think to look at again.
|
||||
return UploadResult{OK: false, Ignored: true, Message: tqslDetail(msg,
|
||||
"TQSL uploaded nothing — every contact was already uploaded, or outside the certificate's date range")},
|
||||
fmt.Errorf("lotw: no QSOs processed")
|
||||
default:
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("tqsl exit code %d", code)
|
||||
@@ -262,6 +286,29 @@ func UploadLoTW(ctx context.Context, cfg ServiceConfig, tempDir, adifRecord stri
|
||||
}
|
||||
}
|
||||
|
||||
// tqslDetail keeps the lines of TQSL's own output that say what happened to the
|
||||
// contacts, and appends the summary.
|
||||
//
|
||||
// TQSL is explicit — "414 QSO records were already uploaded", "N QSO records
|
||||
// are out of date range" — and that sentence is the whole answer to "why is my
|
||||
// contact not on LoTW". It used to be captured and thrown away.
|
||||
func tqslDetail(out, summary string) string {
|
||||
var keep []string
|
||||
for _, ln := range strings.Split(out, "\n") {
|
||||
ln = strings.TrimSpace(ln)
|
||||
l := strings.ToLower(ln)
|
||||
if strings.Contains(l, "qso") && (strings.Contains(l, "already uploaded") ||
|
||||
strings.Contains(l, "date range") || strings.Contains(l, "ignored") ||
|
||||
strings.Contains(l, "duplicate")) {
|
||||
keep = append(keep, ln)
|
||||
}
|
||||
}
|
||||
if len(keep) == 0 {
|
||||
return summary
|
||||
}
|
||||
return summary + " — " + strings.Join(keep, "; ")
|
||||
}
|
||||
|
||||
// TestLoTW validates the LoTW config: tqsl present and the chosen station
|
||||
// location exists in station_data.
|
||||
func TestLoTW(cfg ServiceConfig, stationDataPath string) (string, error) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package extsvc
|
||||
|
||||
import "testing"
|
||||
|
||||
// TQSL's own exit codes, from its cmdline documentation:
|
||||
//
|
||||
// 0 success
|
||||
// 8 NO QSOs were processed — already uploaded OR out of date range
|
||||
// 9 some processed, some ignored — same two reasons
|
||||
// 14 some already uploaded, the rest signed
|
||||
//
|
||||
// 8 and 9 both used to be read as plain success, and that is how a contact came
|
||||
// to be stamped "uploaded to LoTW" while LoTW had never seen it. The one that
|
||||
// bites is "out of date range": a contact older than the callsign certificate's
|
||||
// validity is silently left out of the submission.
|
||||
func TestTQSLDetailKeepsTheReason(t *testing.T) {
|
||||
out := `13:22:01: Signing using Callsign G0ABC, DXCC Entity ENGLAND
|
||||
13:22:02: /tmp/x.adi: 20 QSO records are out of date range
|
||||
13:22:02: Final Status: No QSOs were processed (8)`
|
||||
got := tqslDetail(out, "summary")
|
||||
if got == "summary" {
|
||||
t.Fatal("threw away TQSL's explanation — that sentence is the whole answer to \"why is my contact not on LoTW\"")
|
||||
}
|
||||
if !contains(got, "out of date range") {
|
||||
t.Errorf("the reason was lost: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTQSLDetailFallsBackToTheSummary(t *testing.T) {
|
||||
if got := tqslDetail("nothing useful here", "summary"); got != "summary" {
|
||||
t.Errorf("got %q, want the plain summary when TQSL said nothing specific", got)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(h, n string) bool {
|
||||
return len(h) >= len(n) && (func() bool {
|
||||
for i := 0; i+len(n) <= len(h); i++ {
|
||||
if h[i:i+len(n)] == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})()
|
||||
}
|
||||
Reference in New Issue
Block a user