Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f27faa81a6 | ||
|
|
69e6c20b97 | ||
|
|
d93c40ebb3 | ||
|
|
dfe3afbf2d | ||
|
|
bb3542c920 | ||
|
|
a966abd9df | ||
|
|
95482ec1d8 | ||
|
|
500fe8bfec |
@@ -126,6 +126,7 @@ const (
|
|||||||
// backend's setting.
|
// backend's setting.
|
||||||
keyCATYaesuLowLines = "cat.yaesu.low_dtr_rts" // deassert DTR/RTS on connect
|
keyCATYaesuLowLines = "cat.yaesu.low_dtr_rts" // deassert DTR/RTS on connect
|
||||||
keyCATKenwoodLowLines = "cat.kenwood.low_dtr_rts" // deassert DTR/RTS on connect
|
keyCATKenwoodLowLines = "cat.kenwood.low_dtr_rts" // deassert DTR/RTS on connect
|
||||||
|
keyCATKenwoodDataMode = "cat.kenwood.data_mode" // data modes → "usb" | "data" (MD6) | "keep"
|
||||||
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
|
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
|
||||||
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
|
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
|
||||||
keyCATIcomAddr = "cat.icom.addr" // Icom CI-V address, decimal (IC-7610 = 152 / 0x98)
|
keyCATIcomAddr = "cat.icom.addr" // Icom CI-V address, decimal (IC-7610 = 152 / 0x98)
|
||||||
@@ -403,6 +404,10 @@ type CATSettings struct {
|
|||||||
// adapter). NOT the radio’s own RJ45, which speaks Kenwood’s KNS/ARCP.
|
// adapter). NOT the radio’s own RJ45, which speaks Kenwood’s KNS/ARCP.
|
||||||
KenwoodPort string `json:"kenwood_port"` // Kenwood CAT serial port (TS-590/890/2000, Elecraft)
|
KenwoodPort string `json:"kenwood_port"` // Kenwood CAT serial port (TS-590/890/2000, Elecraft)
|
||||||
KenwoodBaud int `json:"kenwood_baud"` // Kenwood CAT baud (TS-590 default 9600)
|
KenwoodBaud int `json:"kenwood_baud"` // Kenwood CAT baud (TS-590 default 9600)
|
||||||
|
// What a DATA/digital mode (FT8, PSK…) sets on the rig: "usb" (default), "data"
|
||||||
|
// (MD6 — Elecraft K3/K4 DATA mode) or "keep" (leave the rig's mode untouched,
|
||||||
|
// for a TS-590SG/TS-990S whose data mode is a USB modifier set on the rig).
|
||||||
|
KenwoodDataMode string `json:"kenwood_data_mode"`
|
||||||
// Per-backend: deassert DTR and RTS after opening the CAT port, for
|
// Per-backend: deassert DTR and RTS after opening the CAT port, for
|
||||||
// interfaces that read either line as PTT. Off by default: lowering them
|
// interfaces that read either line as PTT. Off by default: lowering them
|
||||||
// stops some USB-serial interfaces transmitting at all.
|
// stops some USB-serial interfaces transmitting at all.
|
||||||
@@ -617,6 +622,12 @@ type App struct {
|
|||||||
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
||||||
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
||||||
pttMu sync.Mutex
|
pttMu sync.Mutex
|
||||||
|
// confDLMu/confDLCancel cancel the in-flight confirmation download (LoTW/QRZ)
|
||||||
|
// so closing the QSL Manager — or starting another download — stops the previous
|
||||||
|
// one instead of leaving it running against the app-lifetime context (which made
|
||||||
|
// a still-running QRZ sync bleed its log into a freshly started LoTW download).
|
||||||
|
confDLMu sync.Mutex
|
||||||
|
confDLCancel context.CancelFunc
|
||||||
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
||||||
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
||||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||||
@@ -6855,7 +6866,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
|||||||
if a.settings == nil {
|
if a.settings == nil {
|
||||||
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
|
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPttHotkeyEnabled, keyCATPttHotkey, keyCATPttHotkeyToggle, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort)
|
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATKenwoodDataMode, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPttHotkeyEnabled, keyCATPttHotkey, keyCATPttHotkeyToggle, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CATSettings{}, err
|
return CATSettings{}, err
|
||||||
}
|
}
|
||||||
@@ -6878,6 +6889,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
|||||||
KenwoodBaud: 9600,
|
KenwoodBaud: 9600,
|
||||||
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
|
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
|
||||||
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
|
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
|
||||||
|
KenwoodDataMode: m[keyCATKenwoodDataMode],
|
||||||
IcomPort: m[keyCATIcomPort],
|
IcomPort: m[keyCATIcomPort],
|
||||||
IcomBaud: 115200,
|
IcomBaud: 115200,
|
||||||
IcomAddr: 0x98, // IC-7610 default
|
IcomAddr: 0x98, // IC-7610 default
|
||||||
@@ -7047,6 +7059,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
|||||||
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
|
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
|
||||||
keyCATYaesuLowLines: b01(s.YaesuLowLines),
|
keyCATYaesuLowLines: b01(s.YaesuLowLines),
|
||||||
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
|
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
|
||||||
|
keyCATKenwoodDataMode: strings.ToLower(strings.TrimSpace(s.KenwoodDataMode)),
|
||||||
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
||||||
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
|
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
|
||||||
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
|
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
|
||||||
@@ -10173,13 +10186,35 @@ func (a *App) DownloadConfirmations(service string, addNotFound bool, since stri
|
|||||||
}
|
}
|
||||||
svc := extsvc.Service(service)
|
svc := extsvc.Service(service)
|
||||||
cfg := a.loadExternalServices()
|
cfg := a.loadExternalServices()
|
||||||
go a.runDownloadConfirmations(svc, cfg, addNotFound, since)
|
// Cancel any download still running (a slow QRZ sync, say) before starting this
|
||||||
|
// one, so the two don't run at once and interleave their logs — and derive a
|
||||||
|
// cancellable context so closing the QSL Manager can stop it too.
|
||||||
|
a.confDLMu.Lock()
|
||||||
|
if a.confDLCancel != nil {
|
||||||
|
a.confDLCancel()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(a.ctx)
|
||||||
|
a.confDLCancel = cancel
|
||||||
|
a.confDLMu.Unlock()
|
||||||
|
go a.runDownloadConfirmations(ctx, svc, cfg, addNotFound, since)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalServices, addNotFound bool, since string) {
|
// CancelConfirmations stops the in-flight LoTW/QRZ download — the QSL Manager
|
||||||
|
// calls it when the window closes, so work doesn't keep running in the background.
|
||||||
|
func (a *App) CancelConfirmations() {
|
||||||
|
a.confDLMu.Lock()
|
||||||
|
if a.confDLCancel != nil {
|
||||||
|
a.confDLCancel()
|
||||||
|
a.confDLCancel = nil
|
||||||
|
}
|
||||||
|
a.confDLMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) runDownloadConfirmations(ctx context.Context, svc extsvc.Service, cfg extsvc.ExternalServices, addNotFound bool, since string) {
|
||||||
emit := func(line string) {
|
emit := func(line string) {
|
||||||
if a.ctx != nil {
|
// Don't keep logging into a closed/cancelled window.
|
||||||
|
if a.ctx != nil && ctx.Err() == nil {
|
||||||
wruntime.EventsEmit(a.ctx, "qslmgr:log", line)
|
wruntime.EventsEmit(a.ctx, "qslmgr:log", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10188,7 +10223,6 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
wruntime.EventsEmit(a.ctx, "qslmgr:done", map[string]any{"uploaded": matched, "total": total})
|
wruntime.EventsEmit(a.ctx, "qslmgr:done", map[string]any{"uploaded": matched, "total": total})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx := a.ctx
|
|
||||||
matched, total, added := 0, 0, 0
|
matched, total, added := 0, 0, 0
|
||||||
|
|
||||||
// resolveSince turns the UI's request into a concrete date (or ""):
|
// resolveSince turns the UI's request into a concrete date (or ""):
|
||||||
@@ -10240,6 +10274,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
var items []ConfirmationItem
|
var items []ConfirmationItem
|
||||||
var unmatched []string
|
var unmatched []string
|
||||||
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
|
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err() // window closed / superseded
|
||||||
|
}
|
||||||
q, ok := adif.RecordToQSO(rec)
|
q, ok := adif.RecordToQSO(rec)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@@ -10422,6 +10459,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
// QRZ FETCH returns headerless ADIF (no <EOH>); prepend one so the
|
// QRZ FETCH returns headerless ADIF (no <EOH>); prepend one so the
|
||||||
// parser treats the stream as records.
|
// parser treats the stream as records.
|
||||||
perr := adif.Parse(strings.NewReader("<EOH>\n"+adifText), func(rec adif.Record) error {
|
perr := adif.Parse(strings.NewReader("<EOH>\n"+adifText), func(rec adif.Record) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err() // window closed / superseded — stop clearing flags
|
||||||
|
}
|
||||||
parsed++
|
parsed++
|
||||||
for k := range rec {
|
for k := range rec {
|
||||||
allKeys[k] = true
|
allKeys[k] = true
|
||||||
@@ -10539,7 +10579,11 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
}
|
}
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
emit(fmt.Sprintf("Parsed %d record(s). Fields seen: %s", parsed, strings.Join(keys, ", ")))
|
emit(fmt.Sprintf("Parsed %d record(s). Fields seen: %s", parsed, strings.Join(keys, ", ")))
|
||||||
emit(fmt.Sprintf("Confirmed %d, added %d (of %d returned)", matched, added, total))
|
// Spell out the funnel: QRZ hands back the WHOLE logbook, but a confirmation
|
||||||
|
// download only acts on the ones QRZ marks confirmed — the "26165 but only
|
||||||
|
// 7950?" question. Nothing is lost; the rest simply aren't confirmed yet.
|
||||||
|
emit(fmt.Sprintf("%d of your %d QRZ QSOs are confirmed by the other station — the rest aren't confirmed yet, so there's nothing to import for them.", total, parsed))
|
||||||
|
emit(fmt.Sprintf("Of those %d confirmed: %d newly marked, %d added.", total, matched, added))
|
||||||
if unconfirmed > 0 {
|
if unconfirmed > 0 {
|
||||||
// Worth its own line and not a footnote: these QSOs were showing as
|
// Worth its own line and not a footnote: these QSOs were showing as
|
||||||
// confirmed a moment ago, and an award total may move because of it.
|
// confirmed a moment ago, and an award total may move because of it.
|
||||||
@@ -10591,6 +10635,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
var items []ConfirmationItem
|
var items []ConfirmationItem
|
||||||
var unmatched []string
|
var unmatched []string
|
||||||
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
|
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err() // window closed / superseded
|
||||||
|
}
|
||||||
q, ok := adif.RecordToQSO(rec)
|
q, ok := adif.RecordToQSO(rec)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@@ -12965,10 +13012,13 @@ func (a *App) reloadCAT() {
|
|||||||
// more deliberate setting, and silently preferring the wire would leave an
|
// more deliberate setting, and silently preferring the wire would leave an
|
||||||
// operator staring at a host they typed and a radio that never answers.
|
// operator staring at a host they typed and a radio that never answers.
|
||||||
if h := strings.TrimSpace(s.KenwoodHost); h != "" {
|
if h := strings.TrimSpace(s.KenwoodHost); h != "" {
|
||||||
a.cat.Start(cat.NewKenwoodTCP(h, s.DigitalDefault))
|
kw := cat.NewKenwoodTCP(h, s.DigitalDefault)
|
||||||
|
kw.SetDataMode(s.KenwoodDataMode)
|
||||||
|
a.cat.Start(kw)
|
||||||
} else {
|
} else {
|
||||||
kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault)
|
kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault)
|
||||||
kw.SetLowerLines(s.KenwoodLowLines)
|
kw.SetLowerLines(s.KenwoodLowLines)
|
||||||
|
kw.SetDataMode(s.KenwoodDataMode)
|
||||||
a.cat.Start(kw)
|
a.cat.Start(kw)
|
||||||
}
|
}
|
||||||
case "icom":
|
case "icom":
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.23.4",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"CW keyer (Kenwood/Elecraft): choosing the Kenwood/Elecraft engine now actually switches to it. A bug left the keyer on WinKeyer even though the setting showed Kenwood/Elecraft, so its CW-over-CAT never ran.",
|
||||||
|
"Kenwood/Elecraft data mode: a new setting (Settings → CAT) chooses what a data mode (FT8/PSK…) sets on the rig — USB (default), DATA mode (MD6, for an Elecraft K3/K4), or leave the rig's mode unchanged (safest for a TS-590SG/TS-990S, whose data mode is a USB modifier set on the radio). There is no single command that fits every rig, so it's now the operator's choice.",
|
||||||
|
"Ultrabeam over a remote link: changing the pattern (Normal / 180° / bidirectional) no longer snaps back to the old one after a few seconds. The commanded pattern is now held while the motors are still moving plus a grace window after they stop, so the slow remote status poll — which lagged behind the antenna — can't revert the display while the change is in flight.",
|
||||||
|
"QSL Manager: closing the window — or starting another download — now cancels the one still in progress, so a slow QRZ sync no longer keeps running in the background and bleeds its log into a freshly started LoTW run. The QRZ download summary also spells out that only confirmed QSOs are acted on (e.g. \"7950 of 26164 are confirmed\") — a full QRZ logbook has many not-yet-confirmed QSOs, so nothing is lost even though the old wording read as if only a fraction came back. (A LoTW \"http 503\" is separate: the ARRL server being down — retry later.)"
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Keyer CW (Kenwood/Elecraft) : choisir le moteur Kenwood/Elecraft y bascule désormais réellement. Un bug laissait le keyer sur WinKeyer alors que le réglage affichait Kenwood/Elecraft, son CW-sur-CAT ne démarrait donc jamais.",
|
||||||
|
"Mode data Kenwood/Elecraft : un nouveau réglage (Réglages → CAT) choisit ce qu'un mode data (FT8/PSK…) règle sur la radio — USB (défaut), mode DATA (MD6, pour un Elecraft K3/K4), ou ne pas changer le mode de la radio (le plus sûr pour un TS-590SG/TS-990S dont le mode data est un modificateur d'USB réglé sur la radio). Aucune commande unique ne convient à toutes les radios, c'est donc désormais au choix de l'opérateur.",
|
||||||
|
"Ultrabeam en remote : changer le diagramme (Normal / 180° / bidirectionnel) ne revient plus à l'ancien au bout de quelques secondes. Le diagramme commandé est maintenu tant que les moteurs bougent, plus une fenêtre de grâce après leur arrêt — le poll de statut distant, lent et en retard sur l'antenne, ne peut donc plus faire revenir l'affichage pendant que le changement est en cours.",
|
||||||
|
"QSL Manager : fermer la fenêtre — ou lancer un autre téléchargement — annule désormais celui en cours, une synchro QRZ lente ne continue donc plus en arrière-plan à mélanger son journal à un téléchargement LoTW fraîchement lancé. Le récapitulatif QRZ indique aussi clairement que seuls les QSO confirmés sont traités (ex. « 7950 sur 26164 confirmés ») — un logbook QRZ complet contient beaucoup de QSO pas encore confirmés, rien n'est donc perdu même si l'ancien texte donnait l'impression qu'une fraction seulement était revenue. (Un « http 503 » sur LoTW est un autre sujet : serveur de l'ARRL indisponible — réessayez plus tard.)"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.23.3",
|
"version": "0.23.3",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
@@ -2706,8 +2706,9 @@ export default function App() {
|
|||||||
s.engine === 'icom' ? 'icom'
|
s.engine === 'icom' ? 'icom'
|
||||||
: s.engine === 'flex' ? 'flex'
|
: s.engine === 'flex' ? 'flex'
|
||||||
: s.engine === 'yaesu' ? 'yaesu'
|
: s.engine === 'yaesu' ? 'yaesu'
|
||||||
: s.engine === 'serial' ? 'serial'
|
: s.engine === 'kenwood' ? 'kenwood'
|
||||||
: 'winkeyer');
|
: s.engine === 'serial' ? 'serial'
|
||||||
|
: 'winkeyer');
|
||||||
} catch { /* keyer not configured */ }
|
} catch { /* keyer not configured */ }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -5702,11 +5703,11 @@ export default function App() {
|
|||||||
// exactly when its CAT backend is. Yaesu was missing from this
|
// exactly when its CAT backend is. Yaesu was missing from this
|
||||||
// list, so the panel fell back to the WinKeyer status — which
|
// list, so the panel fell back to the WinKeyer status — which
|
||||||
// reported disconnected, no WinKeyer being attached.
|
// reported disconnected, no WinKeyer being attached.
|
||||||
status={cwSource === 'icom' || cwSource === 'flex' || cwSource === 'yaesu'
|
status={cwSource === 'icom' || cwSource === 'flex' || cwSource === 'yaesu' || cwSource === 'kenwood'
|
||||||
? {
|
? {
|
||||||
connected: catState.backend === cwSource && catState.connected,
|
connected: catState.backend === cwSource && catState.connected,
|
||||||
busy: false, wpm: wkWpm, version: 0,
|
busy: false, wpm: wkWpm, version: 0,
|
||||||
port: cwSource === 'flex' ? 'CWX' : cwSource === 'yaesu' ? 'CAT' : 'CI-V',
|
port: cwSource === 'flex' ? 'CWX' : cwSource === 'yaesu' || cwSource === 'kenwood' ? 'CAT' : 'CI-V',
|
||||||
}
|
}
|
||||||
: wkStatus}
|
: wkStatus}
|
||||||
ports={wkPorts}
|
ports={wkPorts}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign, GetSlotStats } from '../../wailsjs/go/main/App';
|
import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, CancelConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign, GetSlotStats } from '../../wailsjs/go/main/App';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
|
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
|
||||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||||
@@ -120,6 +120,10 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
if (service === 'pota' || service === 'paper') { setUploadCall(''); return; }
|
if (service === 'pota' || service === 'paper') { setUploadCall(''); return; }
|
||||||
UploadCallsign(service).then((c) => setUploadCall(c || '')).catch(() => setUploadCall(''));
|
UploadCallsign(service).then((c) => setUploadCall(c || '')).catch(() => setUploadCall(''));
|
||||||
}, [service]);
|
}, [service]);
|
||||||
|
// Closing the QSL Manager (the tab's ×) unmounts this panel — cancel any download
|
||||||
|
// still running so a slow QRZ/LoTW sync doesn't keep going in the background and
|
||||||
|
// bleed its log into the next one.
|
||||||
|
useEffect(() => () => { CancelConfirmations().catch(() => {}); }, []);
|
||||||
const [potaSyncing, setPotaSyncing] = useState(false);
|
const [potaSyncing, setPotaSyncing] = useState(false);
|
||||||
const [potaRes, setPotaRes] = useState<POTASync | null>(null);
|
const [potaRes, setPotaRes] = useState<POTASync | null>(null);
|
||||||
const [potaErr, setPotaErr] = useState('');
|
const [potaErr, setPotaErr] = useState('');
|
||||||
|
|||||||
@@ -1136,7 +1136,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
const [modeDraft, setModeDraft] = useState('');
|
const [modeDraft, setModeDraft] = useState('');
|
||||||
const [catCfg, setCatCfg] = useState<CATSettings>({
|
const [catCfg, setCatCfg] = useState<CATSettings>({
|
||||||
enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120,
|
enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120,
|
||||||
yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, kenwood_low_lines: false, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70, xiegu_ptt_line: '',
|
yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, kenwood_low_lines: false, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', kenwood_data_mode: 'usb', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70, xiegu_ptt_line: '',
|
||||||
icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
|
icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
|
||||||
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
||||||
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
||||||
@@ -2642,6 +2642,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
</label>
|
</label>
|
||||||
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>{t('cat.kwDataMode')}</Label>
|
||||||
|
<Select value={(catCfg as any).kenwood_data_mode || 'usb'} onValueChange={(v) => setCatCfg((s) => ({ ...s, kenwood_data_mode: v } as any))}>
|
||||||
|
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="usb">{t('cat.kwDataUsb')}</SelectItem>
|
||||||
|
<SelectItem value="data">{t('cat.kwDataMd6')}</SelectItem>
|
||||||
|
<SelectItem value="keep">{t('cat.kwDataKeep')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<span className="text-xs text-muted-foreground">{t('cat.kwDataHint')}</span>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{catCfg.backend === 'icom' && (
|
{catCfg.backend === 'icom' && (
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
// Single source of truth for the app version shown in the UI (header + About).
|
// Single source of truth for the app version shown in the UI (header + About).
|
||||||
// Bump this on a release (the release script updates it alongside telemetry.go).
|
// Bump this on a release (the release script updates it alongside telemetry.go).
|
||||||
export const APP_VERSION = '0.23.3';
|
export const APP_VERSION = '0.23.4';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
Vendored
+2
@@ -97,6 +97,8 @@ export function CIVTraceEnabled():Promise<boolean>;
|
|||||||
|
|
||||||
export function CWDecoderRunning():Promise<boolean>;
|
export function CWDecoderRunning():Promise<boolean>;
|
||||||
|
|
||||||
|
export function CancelConfirmations():Promise<void>;
|
||||||
|
|
||||||
export function ChatAvailable():Promise<boolean>;
|
export function ChatAvailable():Promise<boolean>;
|
||||||
|
|
||||||
export function CheckForUpdate():Promise<main.UpdateInfo>;
|
export function CheckForUpdate():Promise<main.UpdateInfo>;
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ export function CWDecoderRunning() {
|
|||||||
return window['go']['main']['App']['CWDecoderRunning']();
|
return window['go']['main']['App']['CWDecoderRunning']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CancelConfirmations() {
|
||||||
|
return window['go']['main']['App']['CancelConfirmations']();
|
||||||
|
}
|
||||||
|
|
||||||
export function ChatAvailable() {
|
export function ChatAvailable() {
|
||||||
return window['go']['main']['App']['ChatAvailable']();
|
return window['go']['main']['App']['ChatAvailable']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1930,6 +1930,7 @@ export namespace main {
|
|||||||
kenwood_host: string;
|
kenwood_host: string;
|
||||||
kenwood_port: string;
|
kenwood_port: string;
|
||||||
kenwood_baud: number;
|
kenwood_baud: number;
|
||||||
|
kenwood_data_mode: string;
|
||||||
yaesu_low_lines: boolean;
|
yaesu_low_lines: boolean;
|
||||||
kenwood_low_lines: boolean;
|
kenwood_low_lines: boolean;
|
||||||
icom_port: string;
|
icom_port: string;
|
||||||
@@ -1975,6 +1976,7 @@ export namespace main {
|
|||||||
this.kenwood_host = source["kenwood_host"];
|
this.kenwood_host = source["kenwood_host"];
|
||||||
this.kenwood_port = source["kenwood_port"];
|
this.kenwood_port = source["kenwood_port"];
|
||||||
this.kenwood_baud = source["kenwood_baud"];
|
this.kenwood_baud = source["kenwood_baud"];
|
||||||
|
this.kenwood_data_mode = source["kenwood_data_mode"];
|
||||||
this.yaesu_low_lines = source["yaesu_low_lines"];
|
this.yaesu_low_lines = source["yaesu_low_lines"];
|
||||||
this.kenwood_low_lines = source["kenwood_low_lines"];
|
this.kenwood_low_lines = source["kenwood_low_lines"];
|
||||||
this.icom_port = source["icom_port"];
|
this.icom_port = source["icom_port"];
|
||||||
|
|||||||
+42
-5
@@ -56,6 +56,12 @@ type Kenwood struct {
|
|||||||
// a wire, which is how most operators actually put a rig on the network.
|
// a wire, which is how most operators actually put a rig on the network.
|
||||||
host string
|
host string
|
||||||
digital string // mode name logged for data (FT8 by default)
|
digital string // mode name logged for data (FT8 by default)
|
||||||
|
// dataMode chooses what CAT mode a DATA/digital mode (FT8, PSK…) sets, because
|
||||||
|
// no single Kenwood mode digit is right for every rig: "usb" → USB (the SSB-data
|
||||||
|
// base, default), "data" → the DATA mode digit MD6 (Elecraft K3/K4), "keep" →
|
||||||
|
// leave the rig's current mode untouched (safest for a TS-590SG/TS-990S whose
|
||||||
|
// data mode is a USB modifier the operator sets on the rig). See SetMode.
|
||||||
|
dataMode string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
port serial.Port
|
port serial.Port
|
||||||
@@ -360,6 +366,17 @@ func (k *Kenwood) SetMode(mode string) error {
|
|||||||
if k.port == nil {
|
if k.port == nil {
|
||||||
return fmt.Errorf("kenwood: not connected")
|
return fmt.Errorf("kenwood: not connected")
|
||||||
}
|
}
|
||||||
|
// Honour the operator's DATA-mode preference for a digital mode. No single
|
||||||
|
// mode digit fits every rig, so the operator picks: keep the rig's mode, use
|
||||||
|
// MD6 (K3/K4 DATA), or fall through to the default USB from kenwoodModeDigit.
|
||||||
|
if isKenwoodDataMode(mode) {
|
||||||
|
switch k.dataMode {
|
||||||
|
case "keep":
|
||||||
|
return nil // leave whatever data mode the operator set on the rig
|
||||||
|
case "data":
|
||||||
|
return k.write("MD6;") // Elecraft K3/K4 DATA mode
|
||||||
|
}
|
||||||
|
}
|
||||||
d := kenwoodModeDigit(mode, k.curFreq)
|
d := kenwoodModeDigit(mode, k.curFreq)
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
return fmt.Errorf("kenwood: no CAT mode for %q", mode)
|
return fmt.Errorf("kenwood: no CAT mode for %q", mode)
|
||||||
@@ -367,6 +384,25 @@ func (k *Kenwood) SetMode(mode string) error {
|
|||||||
return k.write(fmt.Sprintf("MD%c;", d))
|
return k.write(fmt.Sprintf("MD%c;", d))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDataMode configures how a DATA/digital mode is set: "usb" (default), "data"
|
||||||
|
// (MD6, Elecraft K3/K4) or "keep" (don't change the rig's mode).
|
||||||
|
func (k *Kenwood) SetDataMode(m string) {
|
||||||
|
k.mu.Lock()
|
||||||
|
k.dataMode = strings.ToLower(strings.TrimSpace(m))
|
||||||
|
k.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isKenwoodDataMode reports whether a mode name is a soundcard/data mode (FT8,
|
||||||
|
// PSK, JT…) rather than a voice/CW/RTTY mode the rig sets natively.
|
||||||
|
func isKenwoodDataMode(mode string) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
||||||
|
case "", "LSB", "USB", "SSB", "CW", "CW-R", "CWR", "FM", "NFM", "AM",
|
||||||
|
"RTTY", "FSK", "RTTY-R", "FSK-R":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (k *Kenwood) SetPTT(on bool) error {
|
func (k *Kenwood) SetPTT(on bool) error {
|
||||||
k.mu.Lock()
|
k.mu.Lock()
|
||||||
defer k.mu.Unlock()
|
defer k.mu.Unlock()
|
||||||
@@ -551,11 +587,12 @@ func kenwoodModeDigit(mode string, hz int64) byte {
|
|||||||
}
|
}
|
||||||
return '2'
|
return '2'
|
||||||
}
|
}
|
||||||
// Any other digital mode (FT8, PSK, JT…) rides the DATA input, and by universal
|
// Any other digital mode (FT8, PSK, JT…) → USB on every band. There is NO single
|
||||||
// convention that is ALWAYS the UPPER sideband, on every band. The SSB rule
|
// mode digit that is right for both families: a K3 wants DATA (MD6), a TS-590SG
|
||||||
// (LSB below 10 MHz) must NOT be applied here: doing so put a 40/80 m data spot
|
// wants USB-DATA (MD2 + its DATA modifier), and MD6 on the TS-590SG is FSK/RTTY
|
||||||
// on LSB, which an Elecraft K3 shows as "DATA REV" — the reversed sideband.
|
// — wrong for FT8. USB is the safe common base (the reversed-sideband "DATA REV"
|
||||||
// (RTTY/FSK is handled above as its own FSK mode, so it isn't affected.)
|
// only came from the old LSB-below-10-MHz rule). A rig-specific DATA mode is a
|
||||||
|
// follow-up that needs the model, not a blind default.
|
||||||
return '2'
|
return '2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,23 @@ func TestKenwoodModeDigit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The DATA-mode preference only applies to soundcard/data modes, never to voice,
|
||||||
|
// CW or native RTTY — getting this wrong would hijack an SSB or CW mode change.
|
||||||
|
func TestIsKenwoodDataMode(t *testing.T) {
|
||||||
|
data := []string{"FT8", "FT4", "PSK31", "JT65", "DATA", "DIGITAL", "OLIVIA"}
|
||||||
|
notData := []string{"", "LSB", "USB", "SSB", "CW", "CW-R", "FM", "AM", "RTTY", "FSK", "RTTY-R"}
|
||||||
|
for _, m := range data {
|
||||||
|
if !isKenwoodDataMode(m) {
|
||||||
|
t.Errorf("isKenwoodDataMode(%q) = false, want true", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, m := range notData {
|
||||||
|
if isKenwoodDataMode(m) {
|
||||||
|
t.Errorf("isKenwoodDataMode(%q) = true, want false", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// What the log records for each mode digit the rig reports.
|
// What the log records for each mode digit the rig reports.
|
||||||
func TestKenwoodModeToADIF(t *testing.T) {
|
func TestKenwoodModeToADIF(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ const (
|
|||||||
ubReadTimeout = 4 * time.Second // was 1s — too tight for a remote link
|
ubReadTimeout = 4 * time.Second // was 1s — too tight for a remote link
|
||||||
ubKeepAlive = 15 * time.Second // OS-level TCP keepalive
|
ubKeepAlive = 15 * time.Second // OS-level TCP keepalive
|
||||||
ubMaxPollTimeout = 3 // consecutive read timeouts tolerated before reconnecting
|
ubMaxPollTimeout = 3 // consecutive read timeouts tolerated before reconnecting
|
||||||
|
// How long a just-commanded direction is trusted AFTER the motors have stopped
|
||||||
|
// but before the antenna's status confirms it. The timer is held off entirely
|
||||||
|
// while the motors are still moving, so this is only the grace period for the
|
||||||
|
// confirmation poll to arrive once the elements have settled — generous, because
|
||||||
|
// over a remote link that poll lags by several seconds.
|
||||||
|
ubPendingDirGrace = 8 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Protocol constants
|
// Protocol constants
|
||||||
@@ -221,12 +227,30 @@ func (c *Client) pollLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.statusMu.Lock()
|
c.statusMu.Lock()
|
||||||
// Keep a just-commanded direction until the antenna reports it.
|
// Keep a just-commanded direction until the antenna actually reports it.
|
||||||
|
// Over a remote link the confirmation arrives several seconds after the
|
||||||
|
// command — the motors flip the elements first — so the old fixed 4 s
|
||||||
|
// timeout expired WHILE the change was still in flight, and the stale poll
|
||||||
|
// reverted the UI to the old pattern even though the antenna was on its way
|
||||||
|
// to the new one. Now: while the motors are still moving the change is in
|
||||||
|
// progress, so hold the commanded pattern and keep resetting the timer;
|
||||||
|
// only once the motors have stopped does the short grace window run, giving
|
||||||
|
// the confirmation poll time to land. The poll only wins if the motors are
|
||||||
|
// idle AND the antenna still reports a different pattern past that window —
|
||||||
|
// i.e. the command genuinely did not take.
|
||||||
if c.pendingDirSet {
|
if c.pendingDirSet {
|
||||||
if time.Since(c.pendingDirAt) > 4*time.Second || status.Direction == c.pendingDir {
|
if status.MotorsMoving != 0 {
|
||||||
c.pendingDirSet = false
|
c.pendingDirAt = time.Now() // still repositioning — don't start the grace timer
|
||||||
} else {
|
}
|
||||||
status.Direction = c.pendingDir
|
switch {
|
||||||
|
case status.Direction == c.pendingDir:
|
||||||
|
c.pendingDirSet = false // confirmed by the antenna
|
||||||
|
case time.Since(c.pendingDirAt) > ubPendingDirGrace:
|
||||||
|
c.pendingDirSet = false // motors idle, still unconfirmed → accept the poll
|
||||||
|
log.Printf("Ultrabeam: antenna never confirmed direction %d (reports %d) — dropping the hold",
|
||||||
|
c.pendingDir, status.Direction)
|
||||||
|
default:
|
||||||
|
status.Direction = c.pendingDir // still changing, or within the grace window
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.lastStatus = status
|
c.lastStatus = status
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||||
appVersion = "0.23.3"
|
appVersion = "0.23.4"
|
||||||
|
|
||||||
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
||||||
// to https://us.i.posthog.com for a US project.
|
// to https://us.i.posthog.com for a US project.
|
||||||
|
|||||||
Reference in New Issue
Block a user