165 lines
4.3 KiB
Go
165 lines
4.3 KiB
Go
// Package cabrillo writes a Cabrillo v3 contest log from OpsLog QSOs — the
|
|
// format most contest sponsors require for log submission (a header of
|
|
// CATEGORY-* / CONTEST / CALLSIGN tags followed by one "QSO:" line each).
|
|
package cabrillo
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"hamlog/internal/qso"
|
|
)
|
|
|
|
// Header holds the Cabrillo header fields. Only Callsign and Contest really
|
|
// matter; the rest carry sensible defaults the operator can edit in the file
|
|
// before submitting (categories vary per contest).
|
|
type Header struct {
|
|
Callsign string
|
|
Contest string
|
|
Operators string
|
|
CatOperator string // SINGLE-OP | MULTI-OP | CHECKLOG
|
|
CatBand string // ALL | 20M | …
|
|
CatMode string // CW | SSB | MIXED | …
|
|
CatPower string // HIGH | LOW | QRP
|
|
GridLocator string
|
|
Name string
|
|
Email string
|
|
Club string
|
|
}
|
|
|
|
func def(v, d string) string {
|
|
if strings.TrimSpace(v) == "" {
|
|
return d
|
|
}
|
|
return v
|
|
}
|
|
|
|
// Generate writes the Cabrillo document for qsos and returns the number of QSO
|
|
// lines written (rows without a usable frequency/callsign are skipped).
|
|
func Generate(w io.Writer, h Header, qsos []qso.QSO) (int, error) {
|
|
myCall := strings.ToUpper(strings.TrimSpace(h.Callsign))
|
|
fmt.Fprintln(w, "START-OF-LOG: 3.0")
|
|
fmt.Fprintln(w, "CREATED-BY: OpsLog")
|
|
fmt.Fprintf(w, "CONTEST: %s\n", strings.ToUpper(def(h.Contest, "UNKNOWN")))
|
|
fmt.Fprintf(w, "CALLSIGN: %s\n", myCall)
|
|
fmt.Fprintf(w, "CATEGORY-OPERATOR: %s\n", strings.ToUpper(def(h.CatOperator, "SINGLE-OP")))
|
|
fmt.Fprintf(w, "CATEGORY-BAND: %s\n", strings.ToUpper(def(h.CatBand, "ALL")))
|
|
fmt.Fprintf(w, "CATEGORY-MODE: %s\n", strings.ToUpper(def(h.CatMode, "MIXED")))
|
|
fmt.Fprintf(w, "CATEGORY-POWER: %s\n", strings.ToUpper(def(h.CatPower, "HIGH")))
|
|
fmt.Fprintln(w, "CLAIMED-SCORE: 0")
|
|
fmt.Fprintf(w, "OPERATORS: %s\n", strings.ToUpper(def(h.Operators, myCall)))
|
|
if h.GridLocator != "" {
|
|
fmt.Fprintf(w, "GRID-LOCATOR: %s\n", strings.ToUpper(h.GridLocator))
|
|
}
|
|
if h.Name != "" {
|
|
fmt.Fprintf(w, "NAME: %s\n", h.Name)
|
|
}
|
|
if h.Club != "" {
|
|
fmt.Fprintf(w, "CLUB: %s\n", h.Club)
|
|
}
|
|
if h.Email != "" {
|
|
fmt.Fprintf(w, "EMAIL: %s\n", h.Email)
|
|
}
|
|
|
|
n := 0
|
|
for _, q := range qsos {
|
|
call := strings.ToUpper(strings.TrimSpace(q.Callsign))
|
|
if call == "" {
|
|
continue
|
|
}
|
|
khz := freqKHz(q)
|
|
mode := cabrilloMode(q.Mode)
|
|
myRST := def(q.RSTSent, defaultRST(mode))
|
|
hisRST := def(q.RSTRcvd, defaultRST(mode))
|
|
myExch := exchange(q.STX, q.STXString)
|
|
hisExch := exchange(q.SRX, q.SRXString)
|
|
// QSO: <freq> <mo> <date> <time> <mycall> <myrst> <myexch> <call> <rst> <exch>
|
|
fmt.Fprintf(w, "QSO: %5d %-2s %s %s %-13s %-3s %-6s %-13s %-3s %-6s\n",
|
|
khz, mode,
|
|
q.QSODate.UTC().Format("2006-01-02"), q.QSODate.UTC().Format("1504"),
|
|
myCall, myRST, myExch, call, hisRST, hisExch)
|
|
n++
|
|
}
|
|
fmt.Fprintln(w, "END-OF-LOG:")
|
|
return n, nil
|
|
}
|
|
|
|
// freqKHz returns the QSO frequency in kHz, falling back to a nominal frequency
|
|
// for the band when the exact frequency wasn't logged.
|
|
func freqKHz(q qso.QSO) int {
|
|
if q.FreqHz != nil && *q.FreqHz > 0 {
|
|
return int((*q.FreqHz + 500) / 1000)
|
|
}
|
|
return bandNominalKHz(q.Band)
|
|
}
|
|
|
|
func bandNominalKHz(band string) int {
|
|
switch strings.ToLower(strings.TrimSpace(band)) {
|
|
case "160m":
|
|
return 1800
|
|
case "80m":
|
|
return 3500
|
|
case "60m":
|
|
return 5350
|
|
case "40m":
|
|
return 7000
|
|
case "30m":
|
|
return 10100
|
|
case "20m":
|
|
return 14000
|
|
case "17m":
|
|
return 18068
|
|
case "15m":
|
|
return 21000
|
|
case "12m":
|
|
return 24890
|
|
case "10m":
|
|
return 28000
|
|
case "6m":
|
|
return 50000
|
|
case "2m":
|
|
return 144000
|
|
case "70cm":
|
|
return 432000
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// cabrilloMode maps an ADIF mode to the two-letter Cabrillo mode code.
|
|
func cabrilloMode(mode string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
|
case "CW":
|
|
return "CW"
|
|
case "RTTY":
|
|
return "RY"
|
|
case "FM":
|
|
return "FM"
|
|
case "SSB", "USB", "LSB", "AM":
|
|
return "PH"
|
|
case "":
|
|
return "PH"
|
|
default:
|
|
return "DG" // FT8/FT4/PSK/JT/DATA and other digital modes
|
|
}
|
|
}
|
|
|
|
func defaultRST(cabMode string) string {
|
|
if cabMode == "CW" || cabMode == "RY" || cabMode == "DG" {
|
|
return "599"
|
|
}
|
|
return "59"
|
|
}
|
|
|
|
// exchange renders the contest exchange: the string field if set, else the
|
|
// numeric serial, else empty.
|
|
func exchange(num *int, str string) string {
|
|
if s := strings.TrimSpace(str); s != "" {
|
|
return s
|
|
}
|
|
if num != nil && *num > 0 {
|
|
return fmt.Sprintf("%d", *num)
|
|
}
|
|
return ""
|
|
}
|