qsl designer

This commit is contained in:
2026-06-11 21:54:35 +02:00
parent 6150498a9e
commit 408b29896c
252 changed files with 13989 additions and 277 deletions
+210 -141
View File
@@ -18,27 +18,28 @@ import (
"hamlog/internal/adif"
"hamlog/internal/applog"
"hamlog/internal/backup"
"hamlog/internal/audio"
"hamlog/internal/cat"
"hamlog/internal/clublog"
"hamlog/internal/award"
"hamlog/internal/awardref"
"hamlog/internal/backup"
"hamlog/internal/cat"
"hamlog/internal/clublog"
"hamlog/internal/cluster"
"hamlog/internal/pota"
"hamlog/internal/db"
"hamlog/internal/dxcc"
"hamlog/internal/email"
"hamlog/internal/extsvc"
"hamlog/internal/integrations/udp"
"hamlog/internal/operating"
"hamlog/internal/dxcc"
"hamlog/internal/lookup"
"hamlog/internal/operating"
"hamlog/internal/pota"
"hamlog/internal/profile"
"hamlog/internal/qslcard"
"hamlog/internal/qso"
"hamlog/internal/rotator/pst"
"hamlog/internal/settings"
"hamlog/internal/ultrabeam"
"hamlog/internal/winkeyer"
"hamlog/internal/settings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
"go.bug.st/serial"
@@ -72,11 +73,11 @@ const (
keyListsRSTDigital = "lists.rst_digital"
keyCATEnabled = "cat.enabled"
keyCATBackend = "cat.backend" // "omnirig" (only one for now)
keyCATOmniRigNum = "cat.omnirig.rig" // 1 or 2
keyCATBackend = "cat.backend" // "omnirig" (only one for now)
keyCATOmniRigNum = "cat.omnirig.rig" // 1 or 2
keyCATPollMs = "cat.poll_ms"
keyCATDelayMs = "cat.delay_ms" // pause between commands
keyCATDigitalDefault = "cat.digital_default" // mode to use when CAT reports DATA
keyCATDelayMs = "cat.delay_ms" // pause between commands
keyCATDigitalDefault = "cat.digital_default" // mode to use when CAT reports DATA
// Audio (Digital Voice Keyer + QSO recorder). Machine-local hardware, so
// global (not per-profile) like CAT/rotator. Device fields store the
@@ -118,10 +119,10 @@ const (
// file download. Visible in the binary but must not be exposed publicly.
clublogAppAPIKey = "5767f19333363a9ef432ee9cd4141fe76b8adf38"
keyRotatorEnabled = "rotator.enabled"
keyRotatorHost = "rotator.host"
keyRotatorPort = "rotator.port"
keyRotatorHasElevation = "rotator.has_elevation"
keyRotatorEnabled = "rotator.enabled"
keyRotatorHost = "rotator.host"
keyRotatorPort = "rotator.port"
keyRotatorHasElevation = "rotator.has_elevation"
// Ultrabeam antenna (TCP, e.g. via an RS232↔Ethernet adapter) — Hardware → Antenna.
keyUltrabeamEnabled = "ultrabeam.enabled"
@@ -146,10 +147,10 @@ const (
keyWKAutoSpace = "winkeyer.autospace"
keyWKUsePTT = "winkeyer.use_ptt"
keyWKSerialEcho = "winkeyer.serial_echo"
keyWKMacros = "winkeyer.macros" // JSON array of {label,text}
keyWKEngine = "winkeyer.engine" // "winkeyer" | "tci"
keyWKMacros = "winkeyer.macros" // JSON array of {label,text}
keyWKEngine = "winkeyer.engine" // "winkeyer" | "tci"
keyWKEscClears = "winkeyer.esc_clears_call" // ESC also clears the callsign
keyWKSendOnType = "winkeyer.send_on_type" // key characters live as typed
keyWKSendOnType = "winkeyer.send_on_type" // key characters live as typed
keyClusterAutoConnect = "cluster.auto_connect" // open every enabled server at app start
@@ -172,10 +173,10 @@ const (
// External services (logbook upload). QRZ.com first; Clublog / LoTW
// will add their own keys under the same extsvc.* prefix.
keyExtQRZAPIKey = "extsvc.qrz.api_key"
keyExtQRZForceCall = "extsvc.qrz.force_station_callsign"
keyExtQRZAutoUpload = "extsvc.qrz.auto_upload"
keyExtQRZUploadMode = "extsvc.qrz.upload_mode"
keyExtQRZAPIKey = "extsvc.qrz.api_key"
keyExtQRZForceCall = "extsvc.qrz.force_station_callsign"
keyExtQRZAutoUpload = "extsvc.qrz.auto_upload"
keyExtQRZUploadMode = "extsvc.qrz.upload_mode"
keyExtClublogEmail = "extsvc.clublog.email"
keyExtClublogPassword = "extsvc.clublog.password"
@@ -240,11 +241,11 @@ type ModePreset struct {
// ListsSettings holds the user-customisable dropdown lists used by the
// entry form. Default values match common HF/VHF practice.
type ListsSettings struct {
Bands []string `json:"bands"`
Modes []ModePreset `json:"modes"`
RSTPhone []string `json:"rst_phone"` // RS reports for phone modes
RSTCW []string `json:"rst_cw"` // RST reports for CW/RTTY/PSK
RSTDigital []string `json:"rst_digital"` // dB reports for FT8/FT4/JT…
Bands []string `json:"bands"`
Modes []ModePreset `json:"modes"`
RSTPhone []string `json:"rst_phone"` // RS reports for phone modes
RSTCW []string `json:"rst_cw"` // RST reports for CW/RTTY/PSK
RSTDigital []string `json:"rst_digital"` // dB reports for FT8/FT4/JT…
}
var defaultBands = []string{
@@ -322,51 +323,52 @@ type StationSettings struct {
// Primary / Failsafe hold a provider name ("qrz" | "hamqth" | "") to
// route lookups: primary first, failsafe on not-found / error.
type LookupSettings struct {
QRZUser string `json:"qrz_user"`
QRZPassword string `json:"qrz_password"`
HamQTHUser string `json:"hamqth_user"`
HamQTHPassword string `json:"hamqth_password"`
Primary string `json:"primary"`
Failsafe string `json:"failsafe"`
DownloadImages bool `json:"download_images"` // show QRZ profile pictures in the UI
CacheTTLDays int `json:"cache_ttl_days"`
QRZUser string `json:"qrz_user"`
QRZPassword string `json:"qrz_password"`
HamQTHUser string `json:"hamqth_user"`
HamQTHPassword string `json:"hamqth_password"`
Primary string `json:"primary"`
Failsafe string `json:"failsafe"`
DownloadImages bool `json:"download_images"` // show QRZ profile pictures in the UI
CacheTTLDays int `json:"cache_ttl_days"`
}
// App is the application context bound to the Wails runtime.
type App struct {
ctx context.Context
db *sql.DB
qso *qso.Repo
settings *settings.Store
profiles *profile.Repo
lookup *lookup.Manager
cache *lookup.Cache
cat *cat.Manager
dxcc *dxcc.Manager
cluster *cluster.Manager
pota *pota.Cache
awardRefs *awardref.Repo
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
extsvc *extsvc.Manager
winkeyer *winkeyer.Manager
clublog *clublog.Manager
ultrabeam *ultrabeam.Client // Ultrabeam antenna (TCP); nil when disabled
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
pttMu sync.Mutex
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
startupErr string // captured for surfacing to the frontend
dbPath string // active database file (may be a user-chosen location)
dataDir string // <exeDir>/data — holds config.json, logs, cty.dat
migratedFromAppData bool // true when we auto-copied AppData on first portable launch
ctx context.Context
db *sql.DB
qso *qso.Repo
settings *settings.Store
profiles *profile.Repo
lookup *lookup.Manager
cache *lookup.Cache
cat *cat.Manager
dxcc *dxcc.Manager
cluster *cluster.Manager
pota *pota.Cache
awardRefs *awardref.Repo
qslTemplates *qslcard.Repo
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
extsvc *extsvc.Manager
winkeyer *winkeyer.Manager
clublog *clublog.Manager
ultrabeam *ultrabeam.Client // Ultrabeam antenna (TCP); nil when disabled
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
pttMu sync.Mutex
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
startupErr string // captured for surfacing to the frontend
dbPath string // active database file (may be a user-chosen location)
dataDir string // <exeDir>/data — holds config.json, logs, cty.dat
migratedFromAppData bool // true when we auto-copied AppData on first portable launch
// shuttingDown gates beforeClose re-entry: the first user attempt to
// close fires shutdown tasks (backup, future LoTW upload, ...) while
@@ -545,8 +547,10 @@ func (a *App) startup(ctx context.Context) {
a.db = conn
a.qso = qso.NewRepo(conn)
a.settings = settings.NewStore(conn)
a.settings.SetSensitivePredicate(isSensitiveSetting) // encrypt passwords at rest when a passphrase is set
a.profiles = profile.NewRepo(conn)
a.awardRefs = awardref.NewRepo(conn)
a.qslTemplates = qslcard.NewRepo(conn)
a.migrateAwardDefs() // upgrade legacy award definitions (enable + new fields)
a.seedBuiltinReferences() // first-run: populate built-in award reference lists
a.operating = operating.NewRepo(conn)
@@ -777,8 +781,8 @@ func (a *App) plannedShutdownSteps() []shutdownStep {
if a.extsvc != nil {
if n := a.extsvc.PendingCount(); n > 0 {
out = append(out, shutdownStep{
ID: "extsvc-upload",
Label: fmt.Sprintf("Uploading %d QSO(s) to online logbooks", n),
ID: "extsvc-upload",
Label: fmt.Sprintf("Uploading %d QSO(s) to online logbooks", n),
Status: "pending",
})
}
@@ -1744,14 +1748,14 @@ type POTAUnmatched struct {
// POTASyncResult summarises a hunter-log sync run for the UI.
type POTASyncResult struct {
Fetched int `json:"fetched"` // hunter-log entries downloaded
Updated int `json:"updated"` // QSOs stamped/appended with a park ref
AlreadyTagged int `json:"already_tagged"` // already carried the park
Added int `json:"added"` // new QSOs inserted (addMissing)
Unmatched int `json:"unmatched"` // no local QSO and not added
UnmatchedList []POTAUnmatched `json:"unmatched_list"` // per-entry detail (capped)
SkippedOtherCall int `json:"skipped_other_call"` // hunts made under another callsign (onlyMyCall)
MyCall string `json:"my_call"` // the profile call used for the onlyMyCall filter
Fetched int `json:"fetched"` // hunter-log entries downloaded
Updated int `json:"updated"` // QSOs stamped/appended with a park ref
AlreadyTagged int `json:"already_tagged"` // already carried the park
Added int `json:"added"` // new QSOs inserted (addMissing)
Unmatched int `json:"unmatched"` // no local QSO and not added
UnmatchedList []POTAUnmatched `json:"unmatched_list"` // per-entry detail (capped)
SkippedOtherCall int `json:"skipped_other_call"` // hunts made under another callsign (onlyMyCall)
MyCall string `json:"my_call"` // the profile call used for the onlyMyCall filter
}
// SyncPOTAHunterLog downloads the user's POTA hunter log and stamps pota_ref on
@@ -2815,8 +2819,8 @@ func (a *App) WorkedBefore(callsign string, dxccHint int) (qso.WorkedBefore, err
// Min size must be reduced BEFORE resizing down, otherwise the OS clamps to
// the previous (larger) min — and increased BEFORE resizing up.
const (
compactW, compactH = 1240, 158
normalW, normalH = 1400, 900
compactW, compactH = 1240, 158
normalW, normalH = 1400, 900
normalMinW, normalMinH = 1100, 700
// Large enough to never constrain a maximised window on big displays.
maxW, maxH = 8000, 6000
@@ -2868,8 +2872,8 @@ func (a *App) OpenADIFFile() (string, error) {
// existing QSO (same call + UTC-minute + band + mode) are handled:
// - "skip" : leave the existing QSO untouched (default, safe)
// - "update" : merge the file's non-empty fields onto the existing QSO —
// refreshes QSL/confirmation statuses when re-syncing from
// Log4OM / LoTW without clobbering fields the file omits
// refreshes QSL/confirmation statuses when re-syncing from
// Log4OM / LoTW without clobbering fields the file omits
// - "all" : insert every record, duplicates included
//
// applyCty, when true, recomputes country / continent / DXCC / CQ / ITU from
@@ -3924,7 +3928,11 @@ func (a *App) DVKStopRecord() error {
}
// DVKCancelRecord aborts a recording without saving.
func (a *App) DVKCancelRecord() { if a.audioMgr != nil { a.audioMgr.CancelRecording() } }
func (a *App) DVKCancelRecord() {
if a.audioMgr != nil {
a.audioMgr.CancelRecording()
}
}
// DVKPlay transmits a slot's message to the rig ("To Radio"), asserting serial
// PTT (RTS/DTR) first unless the operator uses VOX. PTT is released
@@ -4195,16 +4203,36 @@ func (a *App) applyQSLDefaults(q *qso.QSO) {
if err != nil {
return
}
if q.QSLSent == "" { q.QSLSent = d.QSLSent }
if q.QSLRcvd == "" { q.QSLRcvd = d.QSLRcvd }
if q.LOTWSent == "" { q.LOTWSent = d.LOTWSent }
if q.LOTWRcvd == "" { q.LOTWRcvd = d.LOTWRcvd }
if q.EQSLSent == "" { q.EQSLSent = d.EQSLSent }
if q.EQSLRcvd == "" { q.EQSLRcvd = d.EQSLRcvd }
if q.ClublogUploadStatus == "" { q.ClublogUploadStatus = d.ClublogStatus }
if q.HRDLogUploadStatus == "" { q.HRDLogUploadStatus = d.HRDLogStatus }
if q.QRZComUploadStatus == "" { q.QRZComUploadStatus = d.QRZComStatus }
if q.QRZComDownloadStatus == "" { q.QRZComDownloadStatus = d.QRZComCfm }
if q.QSLSent == "" {
q.QSLSent = d.QSLSent
}
if q.QSLRcvd == "" {
q.QSLRcvd = d.QSLRcvd
}
if q.LOTWSent == "" {
q.LOTWSent = d.LOTWSent
}
if q.LOTWRcvd == "" {
q.LOTWRcvd = d.LOTWRcvd
}
if q.EQSLSent == "" {
q.EQSLSent = d.EQSLSent
}
if q.EQSLRcvd == "" {
q.EQSLRcvd = d.EQSLRcvd
}
if q.ClublogUploadStatus == "" {
q.ClublogUploadStatus = d.ClublogStatus
}
if q.HRDLogUploadStatus == "" {
q.HRDLogUploadStatus = d.HRDLogStatus
}
if q.QRZComUploadStatus == "" {
q.QRZComUploadStatus = d.QRZComStatus
}
if q.QRZComDownloadStatus == "" {
q.QRZComDownloadStatus = d.QRZComCfm
}
}
// ── External services (logbook upload) ─────────────────────────────────
@@ -4305,12 +4333,12 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
StationLocation: m[keyExtLoTWStationLoc],
ForceStationCallsign: m[keyExtLoTWForceCall],
KeyPassword: m[keyExtLoTWKeyPassword],
UploadFlag: m[keyExtLoTWUploadFlag],
WriteLog: m[keyExtLoTWWriteLog] == "1",
Username: m[keyExtLoTWUsername],
Password: m[keyExtLoTWWebPassword],
AutoUpload: m[keyExtLoTWAutoUpload] == "1",
UploadMode: extsvc.UploadMode(m[keyExtLoTWUploadMode]),
UploadFlag: m[keyExtLoTWUploadFlag],
WriteLog: m[keyExtLoTWWriteLog] == "1",
Username: m[keyExtLoTWUsername],
Password: m[keyExtLoTWWebPassword],
AutoUpload: m[keyExtLoTWAutoUpload] == "1",
UploadMode: extsvc.UploadMode(m[keyExtLoTWUploadMode]),
}
// Default the TQSL path to the standard install location when unset, so
// the field is pre-populated if TQSL is present.
@@ -5236,20 +5264,53 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
// cty.dat). Best-effort: a network failure shouldn't block the log.
if a.lookup != nil {
if lr, lerr := a.lookup.Lookup(a.ctx, q.Callsign); lerr == nil {
if q.Name == "" { q.Name = lr.Name }
if q.QTH == "" { q.QTH = lr.QTH }
if q.Country == "" { q.Country = lr.Country }
if q.Grid == "" { q.Grid = lr.Grid }
if q.Continent == "" { q.Continent = lr.Continent }
if q.State == "" { q.State = lr.State }
if q.County == "" { q.County = lr.County }
if q.Address == "" { q.Address = lr.Address }
if q.Email == "" { q.Email = lr.Email }
if q.DXCC == nil && lr.DXCC != 0 { v := lr.DXCC; q.DXCC = &v }
if q.CQZ == nil && lr.CQZ != 0 { v := lr.CQZ; q.CQZ = &v }
if q.ITUZ == nil && lr.ITUZ != 0 { v := lr.ITUZ; q.ITUZ = &v }
if q.Lat == nil && lr.Lat != 0 { v := lr.Lat; q.Lat = &v }
if q.Lon == nil && lr.Lon != 0 { v := lr.Lon; q.Lon = &v }
if q.Name == "" {
q.Name = lr.Name
}
if q.QTH == "" {
q.QTH = lr.QTH
}
if q.Country == "" {
q.Country = lr.Country
}
if q.Grid == "" {
q.Grid = lr.Grid
}
if q.Continent == "" {
q.Continent = lr.Continent
}
if q.State == "" {
q.State = lr.State
}
if q.County == "" {
q.County = lr.County
}
if q.Address == "" {
q.Address = lr.Address
}
if q.Email == "" {
q.Email = lr.Email
}
if q.DXCC == nil && lr.DXCC != 0 {
v := lr.DXCC
q.DXCC = &v
}
if q.CQZ == nil && lr.CQZ != 0 {
v := lr.CQZ
q.CQZ = &v
}
if q.ITUZ == nil && lr.ITUZ != 0 {
v := lr.ITUZ
q.ITUZ = &v
}
if q.Lat == nil && lr.Lat != 0 {
v := lr.Lat
q.Lat = &v
}
if q.Lon == nil && lr.Lon != 0 {
v := lr.Lon
q.Lon = &v
}
}
}
@@ -5268,9 +5329,16 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
if a.operating != nil && a.profiles != nil {
if p, err := a.profiles.Active(a.ctx); err == nil {
if d, ok2, _ := a.operating.BandDefault(a.ctx, p.ID, q.Band); ok2 {
if q.MyRig == "" { q.MyRig = d.StationName }
if q.MyAntenna == "" { q.MyAntenna = d.AntennaName }
if q.TXPower == nil && d.TXPower != nil { v := *d.TXPower; q.TXPower = &v }
if q.MyRig == "" {
q.MyRig = d.StationName
}
if q.MyAntenna == "" {
q.MyAntenna = d.AntennaName
}
if q.TXPower == nil && d.TXPower != nil {
v := *d.TXPower
q.TXPower = &v
}
}
}
}
@@ -5328,9 +5396,10 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
// consumeUDPEvents bridges parsed UDP events to the frontend over Wails'
// event bus. The frontend listens on:
// udp:dx_call → string callsign (also Grid/Mode/Freq when known)
// udp:logged_qso → ADIF text of a QSO that finished in WSJT-X/JTDX/MSHV
// udp:remote_call → string callsign from a remote-control source
//
// udp:dx_call → string callsign (also Grid/Mode/Freq when known)
// udp:logged_qso → ADIF text of a QSO that finished in WSJT-X/JTDX/MSHV
// udp:remote_call → string callsign from a remote-control source
func (a *App) consumeUDPEvents() {
if a.udp == nil {
return
@@ -5447,11 +5516,11 @@ func (a *App) OperatingDefaultForBand(band string) (operating.BandDefault, error
// BackupSettings is the user-tweakable database backup configuration.
type BackupSettings struct {
Enabled bool `json:"enabled"`
Folder string `json:"folder"`
Rotation int `json:"rotation"`
Zip bool `json:"zip"`
LastBackupAt string `json:"last_backup_at"`
Enabled bool `json:"enabled"`
Folder string `json:"folder"`
Rotation int `json:"rotation"`
Zip bool `json:"zip"`
LastBackupAt string `json:"last_backup_at"`
DefaultFolder string `json:"default_folder"` // computed, read-only — shown as a hint
}
@@ -5692,10 +5761,10 @@ func (a *App) ClearLookupCache() error {
// if it hasn't been loaded yet). Exposed for the Maintenance menu so the
// user can see what they're working with before triggering a refresh.
type CtyDatInfo struct {
Path string `json:"path"`
Entities int `json:"entities"`
LoadedAt string `json:"loaded_at,omitempty"` // RFC3339, "" if not loaded
FileModTime string `json:"file_mod_time,omitempty"` // RFC3339, "" if missing
Path string `json:"path"`
Entities int `json:"entities"`
LoadedAt string `json:"loaded_at,omitempty"` // RFC3339, "" if not loaded
FileModTime string `json:"file_mod_time,omitempty"` // RFC3339, "" if missing
}
// GetCtyDatInfo returns metadata about the on-disk cty.dat.
@@ -5923,10 +5992,10 @@ func (a *App) DuplicateProfile(id int64, newName string) (profile.Profile, error
// RotatorSettings is the JSON shape for the Hardware → Rotator panel.
type RotatorSettings struct {
Enabled bool `json:"enabled"`
Host string `json:"host"` // default 127.0.0.1
Port int `json:"port"` // default 12000
HasElevation bool `json:"has_elevation"` // include EL in GoTo packets
Enabled bool `json:"enabled"`
Host string `json:"host"` // default 127.0.0.1
Port int `json:"port"` // default 12000
HasElevation bool `json:"has_elevation"` // include EL in GoTo packets
}
// GetRotatorSettings returns the persisted rotator config with defaults.
@@ -6201,12 +6270,12 @@ func (a *App) ultrabeamFollowLoop(c *ultrabeam.Client, stepKHz int, stop <-chan
// direction control). Enabled mirrors the setting; the rest comes from the
// device's most recent status poll.
type UltrabeamStatusInfo struct {
Enabled bool `json:"enabled"`
Connected bool `json:"connected"`
Direction int `json:"direction"` // 0=normal, 1=180°, 2=bidirectional
Frequency int `json:"frequency"` // KHz
Band int `json:"band"`
Moving bool `json:"moving"`
Enabled bool `json:"enabled"`
Connected bool `json:"connected"`
Direction int `json:"direction"` // 0=normal, 1=180°, 2=bidirectional
Frequency int `json:"frequency"` // KHz
Band int `json:"band"`
Moving bool `json:"moving"`
}
// GetUltrabeamStatus returns the antenna's current state for the UI poll.
@@ -6289,9 +6358,9 @@ type WKMacro struct {
type WinkeyerSettings struct {
Enabled bool `json:"enabled"`
winkeyer.Config
Engine string `json:"engine"` // keyer backend: "winkeyer" | "tci"
EscClearsCall bool `json:"esc_clears_call"` // ESC also resets the callsign
SendOnType bool `json:"send_on_type"` // key chars live as typed
Engine string `json:"engine"` // keyer backend: "winkeyer" | "tci"
EscClearsCall bool `json:"esc_clears_call"` // ESC also resets the callsign
SendOnType bool `json:"send_on_type"` // key chars live as typed
Macros []WKMacro `json:"macros"`
}