feat: added Cabrillo export
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// 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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user