Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f27faa81a6 | ||
|
|
69e6c20b97 | ||
|
|
d93c40ebb3 | ||
|
|
dfe3afbf2d | ||
|
|
bb3542c920 | ||
|
|
a966abd9df | ||
|
|
95482ec1d8 | ||
|
|
500fe8bfec |
@@ -126,6 +126,7 @@ const (
|
||||
// backend's setting.
|
||||
keyCATYaesuLowLines = "cat.yaesu.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)
|
||||
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
|
||||
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.
|
||||
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)
|
||||
// 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
|
||||
// interfaces that read either line as PTT. Off by default: lowering them
|
||||
// stops some USB-serial interfaces transmitting at all.
|
||||
@@ -617,6 +622,12 @@ type App struct {
|
||||
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
||||
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
||||
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
|
||||
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||
@@ -6855,7 +6866,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
if a.settings == nil {
|
||||
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 {
|
||||
return CATSettings{}, err
|
||||
}
|
||||
@@ -6878,6 +6889,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
KenwoodBaud: 9600,
|
||||
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
|
||||
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
|
||||
KenwoodDataMode: m[keyCATKenwoodDataMode],
|
||||
IcomPort: m[keyCATIcomPort],
|
||||
IcomBaud: 115200,
|
||||
IcomAddr: 0x98, // IC-7610 default
|
||||
@@ -7047,6 +7059,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
||||
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
|
||||
keyCATYaesuLowLines: b01(s.YaesuLowLines),
|
||||
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
|
||||
keyCATKenwoodDataMode: strings.ToLower(strings.TrimSpace(s.KenwoodDataMode)),
|
||||
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
||||
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
|
||||
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
|
||||
@@ -10173,13 +10186,35 @@ func (a *App) DownloadConfirmations(service string, addNotFound bool, since stri
|
||||
}
|
||||
svc := extsvc.Service(service)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
ctx := a.ctx
|
||||
matched, total, added := 0, 0, 0
|
||||
|
||||
// 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 unmatched []string
|
||||
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)
|
||||
if !ok {
|
||||
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
|
||||
// parser treats the stream as records.
|
||||
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++
|
||||
for k := range rec {
|
||||
allKeys[k] = true
|
||||
@@ -10539,7 +10579,11 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
||||
}
|
||||
sort.Strings(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 {
|
||||
// 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.
|
||||
@@ -10591,6 +10635,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
||||
var items []ConfirmationItem
|
||||
var unmatched []string
|
||||
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)
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -12965,10 +13012,13 @@ func (a *App) reloadCAT() {
|
||||
// more deliberate setting, and silently preferring the wire would leave an
|
||||
// operator staring at a host they typed and a radio that never answers.
|
||||
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 {
|
||||
kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault)
|
||||
kw.SetLowerLines(s.KenwoodLowLines)
|
||||
kw.SetDataMode(s.KenwoodDataMode)
|
||||
a.cat.Start(kw)
|
||||
}
|
||||
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",
|
||||
"date": "",
|
||||
|
||||
@@ -2706,8 +2706,9 @@ export default function App() {
|
||||
s.engine === 'icom' ? 'icom'
|
||||
: s.engine === 'flex' ? 'flex'
|
||||
: s.engine === 'yaesu' ? 'yaesu'
|
||||
: s.engine === 'serial' ? 'serial'
|
||||
: 'winkeyer');
|
||||
: s.engine === 'kenwood' ? 'kenwood'
|
||||
: s.engine === 'serial' ? 'serial'
|
||||
: 'winkeyer');
|
||||
} catch { /* keyer not configured */ }
|
||||
}, []);
|
||||
|
||||
@@ -5702,11 +5703,11 @@ export default function App() {
|
||||
// exactly when its CAT backend is. Yaesu was missing from this
|
||||
// list, so the panel fell back to the WinKeyer status — which
|
||||
// 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,
|
||||
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}
|
||||
ports={wkPorts}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
||||
} from '@/components/ui/select';
|
||||
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 { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
|
||||
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; }
|
||||
UploadCallsign(service).then((c) => setUploadCall(c || '')).catch(() => setUploadCall(''));
|
||||
}, [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 [potaRes, setPotaRes] = useState<POTASync | null>(null);
|
||||
const [potaErr, setPotaErr] = useState('');
|
||||
|
||||
@@ -1136,7 +1136,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [modeDraft, setModeDraft] = useState('');
|
||||
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,
|
||||
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,
|
||||
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
||||
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
||||
@@ -2642,6 +2642,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</label>
|
||||
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
||||
</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' && (
|
||||
|
||||
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).
|
||||
// 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.
|
||||
export const APP_AUTHOR = 'F4BPO';
|
||||
|
||||
Vendored
+2
@@ -97,6 +97,8 @@ export function CIVTraceEnabled():Promise<boolean>;
|
||||
|
||||
export function CWDecoderRunning():Promise<boolean>;
|
||||
|
||||
export function CancelConfirmations():Promise<void>;
|
||||
|
||||
export function ChatAvailable():Promise<boolean>;
|
||||
|
||||
export function CheckForUpdate():Promise<main.UpdateInfo>;
|
||||
|
||||
@@ -142,6 +142,10 @@ export function CWDecoderRunning() {
|
||||
return window['go']['main']['App']['CWDecoderRunning']();
|
||||
}
|
||||
|
||||
export function CancelConfirmations() {
|
||||
return window['go']['main']['App']['CancelConfirmations']();
|
||||
}
|
||||
|
||||
export function ChatAvailable() {
|
||||
return window['go']['main']['App']['ChatAvailable']();
|
||||
}
|
||||
|
||||
@@ -1930,6 +1930,7 @@ export namespace main {
|
||||
kenwood_host: string;
|
||||
kenwood_port: string;
|
||||
kenwood_baud: number;
|
||||
kenwood_data_mode: string;
|
||||
yaesu_low_lines: boolean;
|
||||
kenwood_low_lines: boolean;
|
||||
icom_port: string;
|
||||
@@ -1975,6 +1976,7 @@ export namespace main {
|
||||
this.kenwood_host = source["kenwood_host"];
|
||||
this.kenwood_port = source["kenwood_port"];
|
||||
this.kenwood_baud = source["kenwood_baud"];
|
||||
this.kenwood_data_mode = source["kenwood_data_mode"];
|
||||
this.yaesu_low_lines = source["yaesu_low_lines"];
|
||||
this.kenwood_low_lines = source["kenwood_low_lines"];
|
||||
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.
|
||||
host string
|
||||
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
|
||||
port serial.Port
|
||||
@@ -360,6 +366,17 @@ func (k *Kenwood) SetMode(mode string) error {
|
||||
if k.port == nil {
|
||||
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)
|
||||
if d == 0 {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
@@ -551,11 +587,12 @@ func kenwoodModeDigit(mode string, hz int64) byte {
|
||||
}
|
||||
return '2'
|
||||
}
|
||||
// Any other digital mode (FT8, PSK, JT…) rides the DATA input, and by universal
|
||||
// convention that is ALWAYS the UPPER sideband, on every band. The SSB rule
|
||||
// (LSB below 10 MHz) must NOT be applied here: doing so put a 40/80 m data spot
|
||||
// on LSB, which an Elecraft K3 shows as "DATA REV" — the reversed sideband.
|
||||
// (RTTY/FSK is handled above as its own FSK mode, so it isn't affected.)
|
||||
// Any other digital mode (FT8, PSK, JT…) → USB on every band. There is NO single
|
||||
// mode digit that is right for both families: a K3 wants DATA (MD6), a TS-590SG
|
||||
// wants USB-DATA (MD2 + its DATA modifier), and MD6 on the TS-590SG is FSK/RTTY
|
||||
// — wrong for FT8. USB is the safe common base (the reversed-sideband "DATA REV"
|
||||
// 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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
func TestKenwoodModeToADIF(t *testing.T) {
|
||||
cases := []struct {
|
||||
|
||||
@@ -23,6 +23,12 @@ const (
|
||||
ubReadTimeout = 4 * time.Second // was 1s — too tight for a remote link
|
||||
ubKeepAlive = 15 * time.Second // OS-level TCP keepalive
|
||||
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
|
||||
@@ -221,12 +227,30 @@ func (c *Client) pollLoop() {
|
||||
}
|
||||
|
||||
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 time.Since(c.pendingDirAt) > 4*time.Second || status.Direction == c.pendingDir {
|
||||
c.pendingDirSet = false
|
||||
} else {
|
||||
status.Direction = c.pendingDir
|
||||
if status.MotorsMoving != 0 {
|
||||
c.pendingDirAt = time.Now() // still repositioning — don't start the grace timer
|
||||
}
|
||||
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
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
// 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
|
||||
// to https://us.i.posthog.com for a US project.
|
||||
|
||||
Reference in New Issue
Block a user