chore: release v0.23.3
This commit is contained in:
@@ -16117,3 +16117,32 @@ func (a *App) YaesuStopCW() error {
|
||||
}
|
||||
return a.cat.YaesuDo(func(y cat.YaesuController) error { return y.StopCW() })
|
||||
}
|
||||
|
||||
// KenwoodSendCW keys a CW message through the Kenwood/Elecraft keyer (CAT "KY"),
|
||||
// so a K3 keys CW over its single CAT link — no WinKeyer, no second COM port.
|
||||
func (a *App) KenwoodSendCW(text string) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
err := a.cat.KenwoodDo(func(k cat.KenwoodController) error { return k.SendCW(text) })
|
||||
if err != nil {
|
||||
applog.Printf("kenwood cw: KenwoodSendCW(%q) failed: %v", text, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// KenwoodStopCW aborts the message being sent (drops to receive).
|
||||
func (a *App) KenwoodStopCW() error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.KenwoodDo(func(k cat.KenwoodController) error { return k.StopCW() })
|
||||
}
|
||||
|
||||
// SetKenwoodKeySpeed sets the K3/Kenwood keyer speed in WPM (CAT "KS").
|
||||
func (a *App) SetKenwoodKeySpeed(wpm int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.KenwoodDo(func(k cat.KenwoodController) error { return k.SetKeySpeed(wpm) })
|
||||
}
|
||||
|
||||
+16
-26
@@ -3,34 +3,24 @@
|
||||
"version": "0.23.3",
|
||||
"date": "",
|
||||
"en": [
|
||||
"Multiple rotors: Settings → Rotator is now a list, like the amplifiers — add rotors with “Add rotor”, remove with the trash icon. Mix any types (e.g. an ARCO and a Rotator Genius); a Rotator Genius set to “drives two rotors” adds a second one. The compass shows a selector to pick the active rotor, which it turns, displays and points to the DX. Each rotor can be named.",
|
||||
"Rotors: mark which one carries the motorized antenna (Ultrabeam/SteppIR). The boom and reverse/bidirectional pattern paths then show only for that rotor; a plain Yagi on another rotor shows just its heading.",
|
||||
"CAT: a keyboard PTT hotkey. Pick a key in Settings → CAT and it keys the transmitter over CAT while OpsLog is focused — hold-to-talk, or a toggle. A rig with no CAT link falls back to the Audio-tab PTT method (RTS/DTR).",
|
||||
"TCI spots: they now actually appear on the ExpertSDR/SunSDR panorama. The colour was sent as a hex string, which ExpertSDR silently rejected; it is now the decimal integer the TCI protocol expects, and the spot mode is a valid TCI modulation.",
|
||||
"PowerGenius XL meters: forward power and drain current now come from the amplifier's own link (like temperature already did) instead of the FlexRadio meter stream. The amp's telemetry survives a remote/VPN link where the radio's VITA meter stream does not, so the meters keep working when operating over the internet. The direct poll is faster too, for a smoother reading.",
|
||||
"PowerGenius XL remote code: a password field (Settings → Amplifier) for when the amp is reached from outside the LAN. Without it the amp announces \"AUTH\" and rejects every command as \"Unauthorized\" — same as the Tuner Genius and Antenna Genius.",
|
||||
"FlexRadio spot click: clicking one of OpsLog's spots on the SmartSDR panadapter now switches the mode too, not just the frequency — OpsLog applies the spot's mode itself, since SmartSDR only retunes.",
|
||||
"PowerGenius XL meters in the FlexRadio panel: the amplifier card now shows forward power, drain current and temperature from the amp's own link, so they appear even over a remote/VPN link where the radio's meter stream doesn't.",
|
||||
"PowerGenius XL fan mode: switching the fan to Standard / Contest / Broadcast now actually takes — the set command carried a stray prefix the amp ignored, so it reverted to Contest a few seconds later.",
|
||||
"PowerGenius XL card meters: the amp is polled every 250 ms with a longer peak-hold, so forward power tracks SSB more closely and stops collapsing to ~1 W between syllables. Note that over a plain public-IP link the reading still can't match SmartSDR, which relays the radio's meter stream over SmartLink; a VPN (WireGuard) restores the fast native stream.",
|
||||
"TCI spot click: clicking one of OpsLog's spots on the ExpertSDR/SunSDR panorama now fills the entry (callsign + frequency). ExpertSDR reports the click as CLICKED_ON_SPOT / RX_CLICKED_ON_SPOT, which OpsLog was ignoring because it expected a differently-named message.",
|
||||
"PTT hotkey fixes: it now keys over CAT (it was using the Audio-tab PTT method, so a stale RTS/DTR serial setting on a COM port that no longer exists hijacked it and failed with a serial error). A key that needs AltGr on your layout (e.g. \"\\\" on AZERTY) also works now — Ctrl+Alt was being rejected. And each press is logged to the diagnostic log (key, key/unkey, and any failure) instead of failing silently.",
|
||||
"CW macro <LOGQSO>: a macro ending in the token (e.g. \"BK UR <STX> TU <LOGQSO>\") now logs the QSO once the CW before it has been sent, not the instant the macro starts — it was logging on the first keyed letter."
|
||||
"Multiple rotors: Settings → Rotator is now a list, like the amplifiers — add rotors with “Add rotor”, remove with the trash icon. Mix any types (an ARCO and a Rotator Genius); a Rotator Genius set to “drives two rotors” adds a second one. The compass has a selector to pick the active rotor, which it turns, displays and points to the DX; each rotor is named, and you mark which one carries the motorized antenna (Ultrabeam/SteppIR) so the boom and reverse/bidirectional pattern paths show only for it.",
|
||||
"CAT PTT hotkey: pick a key in Settings → CAT and it keys the transmitter over CAT while OpsLog is focused — hold-to-talk or toggle (a rig with no CAT link falls back to the Audio-tab RTS/DTR method). Keys that need AltGr on your layout (e.g. \"\\\" on AZERTY) work, and each press is written to the diagnostic log.",
|
||||
"TCI spots (ExpertSDR / SunSDR): OpsLog's cluster spots now show on the panorama — the colour is sent as the decimal integer the protocol expects (a hex string was silently dropped), with a valid modulation — and clicking one fills the entry (callsign + frequency) via the CLICKED_ON_SPOT message.",
|
||||
"FlexRadio spot click: clicking one of OpsLog's spots on the SmartSDR panadapter now switches the mode too, not just the frequency.",
|
||||
"PowerGenius XL: the widget and the FlexRadio amplifier card now read forward power, drain current and temperature from the amp's own link (polled every 250 ms with peak-hold), so the meters keep working — and stay steady — over a remote link where the radio's VITA stream never reaches OpsLog. Adds a remote-access password (Settings → Amplifier) to reach the amp from outside the LAN, and fixes the fan mode (Standard/Contest/Broadcast), which was reverting to Contest.",
|
||||
"CW macro <LOGQSO>: a macro ending in the token (e.g. \"BK UR <STX> TU <LOGQSO>\") now logs the QSO once the CW before it has been sent, not the instant the macro starts — it was logging on the first keyed letter.",
|
||||
"Filter presets: the trash icon next to a saved preset deletes it again — clicking it was loading the preset instead of removing it.",
|
||||
"Kenwood/Elecraft (K3/K4): data modes (FT8/PSK/JT) now go out on USB on every band — below 10 MHz they were landing on LSB, which a K3 shows as \"DATA REV\". The frequency refreshes promptly again (the split double-check no longer runs on every poll). And a new CW keyer engine (Settings → CW Keyer → Kenwood/Elecraft) sends CW over the CAT link with the KY command — no WinKeyer and no second COM port — for a rig whose single USB port is the CAT port. Needs on-air testing on a K3."
|
||||
],
|
||||
"fr": [
|
||||
"Plusieurs rotors : Réglages → Rotator est désormais une liste, comme les amplis — ajoutez des rotors avec « Ajouter un rotor », supprimez avec la corbeille. Mélangez les types (ex. un ARCO et un Rotator Genius) ; un Rotator Genius réglé sur « pilote deux rotors » en ajoute un second. Le compas affiche un sélecteur pour choisir le rotor actif, qu'il tourne, affiche et pointe vers le DX. Chaque rotor peut être nommé.",
|
||||
"Rotors : indiquez lequel porte l'antenne motorisée (Ultrabeam/SteppIR). Les tracés de boom et de diagramme inverse/bidirectionnel n'apparaissent alors que pour ce rotor ; une Yagi simple sur un autre rotor n'affiche que son cap.",
|
||||
"CAT : une touche PTT au clavier. Choisissez une touche dans Réglages → CAT et elle passe la radio en émission via CAT quand OpsLog a le focus — maintien ou bascule. Une radio sans liaison CAT retombe sur la méthode PTT de l'onglet Audio (RTS/DTR).",
|
||||
"Spots TCI : ils apparaissent enfin sur le panorama ExpertSDR/SunSDR. La couleur était envoyée en chaîne hexadécimale, silencieusement rejetée par ExpertSDR ; c'est désormais l'entier décimal attendu par le protocole TCI, et le mode du spot est une modulation TCI valide.",
|
||||
"Meters PowerGenius XL : la puissance directe et le courant de drain viennent désormais de la liaison propre de l'ampli (comme la température déjà) au lieu du flux de meters du FlexRadio. La télémétrie de l'ampli survit à une liaison distante/VPN là où le flux VITA du Flex non — les meters continuent donc de fonctionner en trafic à distance. Le poll direct est aussi plus rapide, pour une lecture plus fluide.",
|
||||
"Code distant PowerGenius XL : un champ mot de passe (Réglages → Amplificateur) pour joindre l'ampli hors du réseau local. Sans lui, l'ampli annonce « AUTH » et refuse toute commande (« Unauthorized ») — comme le Tuner Genius et l'Antenna Genius.",
|
||||
"Clic sur un spot FlexRadio : cliquer un spot d'OpsLog sur le panadapter SmartSDR change désormais aussi le mode, plus seulement la fréquence — OpsLog applique lui-même le mode du spot, SmartSDR ne faisant que se réaccorder.",
|
||||
"Meters PowerGenius XL dans le panneau FlexRadio : la carte ampli affiche maintenant puissance directe, courant de drain et température depuis la liaison propre de l'ampli — ils apparaissent donc même sur une liaison distante/VPN où le flux de meters de la radio ne passe pas.",
|
||||
"Mode ventilateur PowerGenius XL : passer le ventilateur en Standard / Contest / Broadcast est désormais réellement pris en compte — la commande portait un préfixe parasite que l'ampli ignorait, d'où le retour sur Contest au bout de quelques secondes.",
|
||||
"Meters de la carte PowerGenius XL : l'ampli est interrogé toutes les 250 ms avec un maintien de crête plus long, la puissance directe suit donc mieux la SSB et ne retombe plus à ~1 W entre les syllabes. À noter : sur une simple liaison IP publique la lecture ne peut pas égaler SmartSDR, qui relaie le flux de meters de la radio via SmartLink ; un VPN (WireGuard) rétablit le flux natif rapide.",
|
||||
"Clic sur un spot TCI : cliquer un spot d'OpsLog sur le panorama ExpertSDR/SunSDR remplit désormais la saisie (indicatif + fréquence). ExpertSDR signale le clic par CLICKED_ON_SPOT / RX_CLICKED_ON_SPOT, qu'OpsLog ignorait car il attendait un message nommé autrement.",
|
||||
"Corrections touche PTT : elle passe désormais par le CAT (elle utilisait la méthode PTT de l'onglet Audio, donc un réglage série RTS/DTR résiduel sur un port COM disparu la détournait et échouait avec une erreur série). Une touche nécessitant AltGr (ex. « \\ » en AZERTY) fonctionne aussi maintenant — Ctrl+Alt était rejeté. Et chaque appui est tracé dans le journal de diagnostic (touche, émission/repos, et toute erreur) au lieu d'échouer en silence.",
|
||||
"Macro CW <LOGQSO> : une macro se terminant par le token (ex. « BK UR <STX> TU <LOGQSO> ») journalise le QSO une fois le CW qui précède envoyé, et non à l'instant où la macro démarre — elle journalisait dès la première lettre manipulée."
|
||||
"Plusieurs rotors : Réglages → Rotator est désormais une liste, comme les amplis — ajoutez des rotors avec « Ajouter un rotor », supprimez avec la corbeille. Mélangez les types (un ARCO et un Rotator Genius) ; un Rotator Genius réglé sur « pilote deux rotors » en ajoute un second. Le compas a un sélecteur pour choisir le rotor actif, qu'il tourne, affiche et pointe vers le DX ; chaque rotor est nommé, et vous indiquez lequel porte l'antenne motorisée (Ultrabeam/SteppIR) pour que les tracés de boom et de diagramme inverse/bidirectionnel n'apparaissent que pour lui.",
|
||||
"Touche PTT CAT : choisissez une touche dans Réglages → CAT et elle passe la radio en émission via CAT quand OpsLog a le focus — maintien ou bascule (une radio sans liaison CAT retombe sur la méthode RTS/DTR de l'onglet Audio). Les touches nécessitant AltGr (ex. « \\ » en AZERTY) fonctionnent, et chaque appui est tracé dans le journal de diagnostic.",
|
||||
"Spots TCI (ExpertSDR / SunSDR) : les spots cluster d'OpsLog apparaissent sur le panorama — la couleur est envoyée en entier décimal attendu par le protocole (une chaîne hexadécimale était silencieusement rejetée), avec une modulation valide — et cliquer un spot remplit la saisie (indicatif + fréquence) via le message CLICKED_ON_SPOT.",
|
||||
"Clic sur un spot FlexRadio : cliquer un spot d'OpsLog sur le panadapter SmartSDR change désormais aussi le mode, plus seulement la fréquence.",
|
||||
"PowerGenius XL : le widget et la carte ampli du panneau FlexRadio lisent maintenant puissance directe, courant de drain et température depuis la liaison propre de l'ampli (interrogée toutes les 250 ms avec maintien de crête) — les meters continuent donc de fonctionner, et restent stables, sur une liaison distante où le flux VITA de la radio n'atteint jamais OpsLog. Ajoute un mot de passe d'accès distant (Réglages → Amplificateur) pour joindre l'ampli hors du réseau local, et corrige le mode ventilateur (Standard/Contest/Broadcast) qui repassait sur Contest.",
|
||||
"Macro CW <LOGQSO> : une macro se terminant par le token (ex. « BK UR <STX> TU <LOGQSO> ») journalise le QSO une fois le CW qui précède envoyé, et non à l'instant où la macro démarre — elle journalisait dès la première lettre manipulée.",
|
||||
"Presets de filtre : la corbeille à côté d'un preset enregistré le supprime de nouveau — le clic chargeait le preset au lieu de le retirer.",
|
||||
"Kenwood/Elecraft (K3/K4) : les modes data (FT8/PSK/JT) partent désormais en USB sur toutes les bandes — sous 10 MHz ils passaient en LSB, ce qu'un K3 affiche comme « DATA REV ». La fréquence se rafraîchit de nouveau rapidement (la double-vérification du split ne se fait plus à chaque cycle). Et un nouveau moteur de keyer (Réglages → Keyer CW → Kenwood/Elecraft) envoie le CW sur la liaison CAT avec la commande KY — sans WinKeyer ni second port COM — pour une radio dont l'unique port USB est le port CAT. À tester sur l'air avec un K3."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts, GetWinkeyerStatus,
|
||||
WinkeyerConnect, WinkeyerDisconnect, WinkeyerSend, WinkeyerStop, WinkeyerSetSpeed, WinkeyerBackspace,
|
||||
IcomSendCW, YaesuSendCW, YaesuStopCW, SetYaesuKeySpeed, IcomStopCW, IcomSetKeySpeed, IcomSetBreakIn, GetIcomState,
|
||||
KenwoodSendCW, KenwoodStopCW, SetKenwoodKeySpeed,
|
||||
FlexSendCW, FlexStopCW, FlexSetKeySpeed, FlexBackspaceCW,
|
||||
GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop,
|
||||
StartCWDecoder, StopCWDecoder, SetCWDecoderPitch,
|
||||
@@ -1080,7 +1081,7 @@ export default function App() {
|
||||
// CI-V 0x17 (no extra hardware — sends over the CAT connection). Macros,
|
||||
// auto-call and <LOGQSO> are shared; only the transport differs.
|
||||
const [wkEngine, setWkEngine] = useState<string>('winkeyer');
|
||||
const cwSource: 'winkeyer' | 'icom' | 'flex' | 'yaesu' = wkEngine === 'icom' ? 'icom' : wkEngine === 'flex' ? 'flex' : wkEngine === 'yaesu' ? 'yaesu' : 'winkeyer';
|
||||
const cwSource: 'winkeyer' | 'icom' | 'flex' | 'yaesu' | 'kenwood' = wkEngine === 'icom' ? 'icom' : wkEngine === 'flex' ? 'flex' : wkEngine === 'yaesu' ? 'yaesu' : wkEngine === 'kenwood' ? 'kenwood' : 'winkeyer';
|
||||
// Setting the CW speed has to reach the keyer that is ACTUALLY sending, and
|
||||
// both the CW panel and the Yaesu console can ask for it. With DTR/RTS line
|
||||
// keying the PC does the timing, so the rig's internal keyer speed changes
|
||||
@@ -1093,6 +1094,7 @@ export default function App() {
|
||||
if (src === 'icom') IcomSetKeySpeed(w).catch(() => {});
|
||||
else if (src === 'flex') FlexSetKeySpeed(w).catch(() => {});
|
||||
else if (src === 'yaesu') SetYaesuKeySpeed(w).catch(() => {});
|
||||
else if (src === 'kenwood') SetKenwoodKeySpeed(w).catch(() => {});
|
||||
else WinkeyerSetSpeed(w).catch(() => {});
|
||||
// The rig's own keyer follows too whenever a Yaesu is on CAT, even when it is
|
||||
// not the sending engine: its front panel and OpsLog then agree.
|
||||
@@ -1136,6 +1138,7 @@ export default function App() {
|
||||
const connected = cwSource === 'icom' ? (catState.backend === 'icom' && catState.connected)
|
||||
: cwSource === 'flex' ? (catState.backend === 'flex' && catState.connected)
|
||||
: cwSource === 'yaesu' ? (catState.backend === 'yaesu' && catState.connected)
|
||||
: cwSource === 'kenwood' ? (catState.backend === 'kenwood' && catState.connected)
|
||||
: wkStatus.connected;
|
||||
wkActiveRef.current = wkEnabled && connected;
|
||||
}, [wkEnabled, wkStatus.connected, cwSource, catState.backend, catState.connected]);
|
||||
@@ -2830,7 +2833,7 @@ export default function App() {
|
||||
// segment AFTER the <LOGQSO> (which logs and clears the form) still expands its
|
||||
// variables correctly.
|
||||
const parts = rawText.split(/<LOGQSO>/i).map((pt) => resolveCW(pt));
|
||||
const isRig = cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex' || cwSourceRef.current === 'yaesu';
|
||||
const isRig = cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex' || cwSourceRef.current === 'yaesu' || cwSourceRef.current === 'kenwood';
|
||||
for (let p = 0; p < parts.length; p++) {
|
||||
if (aborted()) return; // ESC / Stop before this segment → stop sending, don't log
|
||||
const resolved = parts[p];
|
||||
@@ -2842,7 +2845,7 @@ export default function App() {
|
||||
// current WPM, so it scales automatically.
|
||||
const keyed = resolved + ' ';
|
||||
setWkSent(resolved);
|
||||
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : cwSourceRef.current === 'icom' ? IcomSendCW : cwSourceRef.current === 'yaesu' ? YaesuSendCW : null;
|
||||
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : cwSourceRef.current === 'icom' ? IcomSendCW : cwSourceRef.current === 'yaesu' ? YaesuSendCW : cwSourceRef.current === 'kenwood' ? KenwoodSendCW : null;
|
||||
if (sendFn) await sendFn(keyed).catch((e) => setError(String(e?.message ?? e)));
|
||||
else await WinkeyerSend(keyed).catch((e) => setError(String(e?.message ?? e)));
|
||||
// WAIT for THIS segment's CW to finish before moving on — so a <LOGQSO>
|
||||
@@ -2884,6 +2887,7 @@ export default function App() {
|
||||
if (cwSourceRef.current === 'icom') IcomStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'yaesu') YaesuStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'kenwood') KenwoodStopCW().catch(() => {});
|
||||
else WinkeyerStop().catch(() => {});
|
||||
}
|
||||
// runAutoCall sends macro i, waits for the keyer to finish, waits the chosen
|
||||
@@ -3811,6 +3815,7 @@ export default function App() {
|
||||
if (cwSourceRef.current === 'icom') IcomStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'yaesu') YaesuStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'kenwood') KenwoodStopCW().catch(() => {});
|
||||
else WinkeyerStop().catch(() => {});
|
||||
}
|
||||
if (!keyerLive || wkEscClearsRef.current) {
|
||||
|
||||
@@ -3606,6 +3606,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<SelectItem value="serial">{t('wk.engSerial')}</SelectItem>
|
||||
<SelectItem value="icom">{t('wk.engIcom')}</SelectItem>
|
||||
<SelectItem value="yaesu">{t('wk.engYaesu')}</SelectItem>
|
||||
<SelectItem value="kenwood">{t('wk.engKenwood')}</SelectItem>
|
||||
<SelectItem value="flex">{t('wk.engFlex')}</SelectItem>
|
||||
<SelectItem value="tci" disabled>{t('wk.engTci')}</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -3652,6 +3653,22 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : wk.engine === 'kenwood' ? (
|
||||
<>
|
||||
{(!catCfg.enabled || catCfg.backend !== 'kenwood') && (
|
||||
<p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
|
||||
<span aria-hidden>⚠</span>
|
||||
<span>{t('wk.catWarnKenwood', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground -mt-1">{t('wk.kenwoodHint')}</p>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('wk.speed')}</Label>
|
||||
<Input type="number" min={4} max={60} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : wk.engine === 'flex' ? (
|
||||
<>
|
||||
{(!catCfg.enabled || catCfg.backend !== 'flex') && (
|
||||
|
||||
@@ -26,7 +26,7 @@ interface Props {
|
||||
wpm: number;
|
||||
macros: WKMacro[];
|
||||
sent: string; // text echoed back by the keyer as it transmits
|
||||
source: 'winkeyer' | 'icom' | 'flex' | 'yaesu'; // CW output engine (chosen in Settings → CW Keyer)
|
||||
source: 'winkeyer' | 'icom' | 'flex' | 'yaesu' | 'kenwood'; // CW output engine (chosen in Settings → CW Keyer)
|
||||
breakIn?: number; // Icom CW break-in: 0=OFF, 1=SEMI, 2=FULL
|
||||
onSetBreakIn?: (mode: number) => void;
|
||||
onSelectPort: (p: string) => void;
|
||||
@@ -101,16 +101,16 @@ export function WinkeyerPanel({
|
||||
<Radio className="size-4 text-primary shrink-0" />
|
||||
{/* CW output engine (chosen in Settings → CW Keyer). */}
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground shrink-0">
|
||||
{source === 'icom' ? 'Icom CW' : source === 'flex' ? 'Flex CWX' : source === 'yaesu' ? 'Yaesu CW' : 'WinKeyer'}
|
||||
{source === 'icom' ? 'Icom CW' : source === 'flex' ? 'Flex CWX' : source === 'yaesu' ? 'Yaesu CW' : source === 'kenwood' ? 'Kenwood CW' : 'WinKeyer'}
|
||||
</span>
|
||||
<span className={cn('size-2 rounded-full', connected ? (status.busy ? 'bg-warning animate-pulse' : 'bg-success') : 'bg-muted-foreground/40')}
|
||||
title={connected ? (status.busy ? t('wkp.sending') : t('wkp.connectedV', { version: status.version })) : t('wkp.disconnected')} />
|
||||
<div className="flex-1" />
|
||||
{source === 'icom' || source === 'flex' || source === 'yaesu' ? (
|
||||
{source === 'icom' || source === 'flex' || source === 'yaesu' || source === 'kenwood' ? (
|
||||
<span className="text-[11px] font-medium text-muted-foreground">
|
||||
{source === 'flex'
|
||||
? (connected ? t('wkp.cwxReady') : t('wkp.cwxOffline'))
|
||||
: source === 'yaesu'
|
||||
: source === 'yaesu' || source === 'kenwood'
|
||||
? (connected ? t('wkp.rigReady') : t('wkp.rigOffline'))
|
||||
: (connected ? t('wkp.civReady') : t('wkp.civOffline'))}
|
||||
</span>
|
||||
|
||||
@@ -163,7 +163,7 @@ const en: Dict = {
|
||||
'wk.escClears': 'ESC clears the callsign too (otherwise ESC only stops transmission)',
|
||||
'wk.esm': 'ESM — Enter Sends Message (CW)',
|
||||
'wk.esmHint': 'In CW with the keyer on, Enter fires a macro by QSO stage instead of logging: empty callsign → F1 (CQ); callsign entered → F2 (report) and focus jumps to RST; Enter in RST → F3 (TU), which logs if the macro contains <LOGQSO>.',
|
||||
'wk.engWinkeyer': 'WinKeyer (K1EL, serial)', 'wk.engSerial': 'Serial port (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (rig keyer)', 'wk.engYaesu': 'Yaesu (rig keyer)', 'wk.yaesuHint': 'Keying goes over the CAT link already configured in Settings → CAT — no second COM port. For the FTDX101 / FT-991A / FT-710 family. An FTDX10 does NOT accept it (DAKY included): on that one use the "Serial port (DTR=CW / RTS=PTT)" keyer on its other COM port (the standard one) with PC KEYING on DTR — confirmed working.', 'wk.catWarnYaesu': 'The Yaesu keyer needs the Yaesu CAT backend — CAT is currently {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (coming soon)',
|
||||
'wk.engWinkeyer': 'WinKeyer (K1EL, serial)', 'wk.engSerial': 'Serial port (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (rig keyer)', 'wk.engYaesu': 'Yaesu (rig keyer)', 'wk.yaesuHint': 'Keying goes over the CAT link already configured in Settings → CAT — no second COM port. For the FTDX101 / FT-991A / FT-710 family. An FTDX10 does NOT accept it (DAKY included): on that one use the "Serial port (DTR=CW / RTS=PTT)" keyer on its other COM port (the standard one) with PC KEYING on DTR — confirmed working.', 'wk.catWarnYaesu': 'The Yaesu keyer needs the Yaesu CAT backend — CAT is currently {backend}.', 'wk.engKenwood': 'Kenwood / Elecraft (rig keyer)', 'wk.kenwoodHint': 'Keying goes over the CAT link already configured in Settings → CAT (the KY command) — no second COM port. Ideal for an Elecraft K3/K4 whose single USB port is the CAT port.', 'wk.catWarnKenwood': 'The Kenwood/Elecraft keyer needs the Kenwood/Elecraft CAT backend — CAT is currently {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (coming soon)',
|
||||
'wk.icomNote': "Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).",
|
||||
'wk.flexNote': "FlexRadio keys CW through the radio's CWX keyer over the existing SmartSDR CAT connection — no WinKeyer or SmartCAT needed. It reuses the connection set in Settings → CAT, so there's nothing else to wire up here. Put a slice in CW mode. Only the speed is set from here; weight, sidetone and break-in are configured on the radio (break-in must be on for CW to actually transmit).",
|
||||
'wk.catWarnIcom': 'Your CAT backend is set to {backend}. Icom CI-V CW needs the CAT backend set to Icom and connected — change it under Settings → CAT interface, otherwise sending CW will fail.',
|
||||
@@ -580,7 +580,7 @@ const fr: Dict = {
|
||||
'wk.escClears': "ÉCHAP efface aussi l'indicatif (sinon ÉCHAP arrête seulement la transmission)",
|
||||
'wk.esm': 'ESM — Entrée envoie le message (CW)',
|
||||
'wk.esmHint': "En CW avec le keyer actif, Entrée envoie un macro selon l'étape du QSO au lieu de loguer : indicatif vide → F1 (CQ) ; indicatif saisi → F2 (report) et le focus passe au RST ; Entrée dans le RST → F3 (TU), qui logue si le macro contient <LOGQSO>.",
|
||||
'wk.engWinkeyer': 'WinKeyer (K1EL, série)', 'wk.engSerial': 'Port série (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (keyer de la radio)', 'wk.engYaesu': 'Yaesu (keyer de la radio)', 'wk.yaesuHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT — sans second port COM. Pour la famille FTDX101 / FT-991A / FT-710. Un FTDX10 ne l'accepte PAS (DAKY compris) : sur celui-ci, utilisez le keyer « Port série (DTR=CW / RTS=PTT) » sur son autre port COM (le standard) avec PC KEYING sur DTR — confirmé fonctionnel.", 'wk.catWarnYaesu': 'Le keyer Yaesu nécessite le backend CAT Yaesu — le CAT est actuellement : {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (bientôt)',
|
||||
'wk.engWinkeyer': 'WinKeyer (K1EL, série)', 'wk.engSerial': 'Port série (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (keyer de la radio)', 'wk.engYaesu': 'Yaesu (keyer de la radio)', 'wk.yaesuHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT — sans second port COM. Pour la famille FTDX101 / FT-991A / FT-710. Un FTDX10 ne l'accepte PAS (DAKY compris) : sur celui-ci, utilisez le keyer « Port série (DTR=CW / RTS=PTT) » sur son autre port COM (le standard) avec PC KEYING sur DTR — confirmé fonctionnel.", 'wk.catWarnYaesu': 'Le keyer Yaesu nécessite le backend CAT Yaesu — le CAT est actuellement : {backend}.', 'wk.engKenwood': 'Kenwood / Elecraft (keyer de la radio)', 'wk.kenwoodHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT (la commande KY) — sans second port COM. Idéal pour un Elecraft K3/K4 dont l'unique port USB est le port CAT.", 'wk.catWarnKenwood': 'Le keyer Kenwood/Elecraft nécessite le backend CAT Kenwood/Elecraft — le CAT est actuellement : {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (bientôt)',
|
||||
'wk.icomNote': "L'Icom CI-V manipule la CW via le keyer interne de la radio sur la connexion CAT existante (commande 0x17) — il réutilise le port COM CAT défini dans Réglages → CAT, rien d'autre à câbler ici. Mets la radio en mode CW. Poids, ratio, sidetone, mode paddle… se règlent sur la radio ; seule la vitesse est définie ici (KEY SPEED de la radio).",
|
||||
'wk.flexNote': "FlexRadio manipule la CW via le keyer CWX de la radio sur la connexion SmartSDR CAT existante — pas besoin de WinKeyer ni de SmartCAT. Il réutilise la connexion définie dans Réglages → CAT, rien d'autre à câbler ici. Mets une slice en mode CW. Seule la vitesse est définie ici ; poids, sidetone et break-in se règlent sur la radio (le break-in doit être activé pour que la CW parte vraiment).",
|
||||
'wk.catWarnIcom': "Ton backend CAT est réglé sur {backend}. La CW Icom CI-V nécessite le backend CAT réglé sur Icom et connecté — change-le dans Réglages → Interface CAT, sinon l'envoi CW échouera.",
|
||||
|
||||
@@ -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.2';
|
||||
export const APP_VERSION = '0.23.3';
|
||||
|
||||
// Author / credits, shown in Help -> About.
|
||||
export const APP_AUTHOR = 'F4BPO';
|
||||
|
||||
Vendored
+6
@@ -607,6 +607,10 @@ export function InspectAwardImport():Promise<main.AwardImportPreview>;
|
||||
|
||||
export function IsNewUSCounty(arg1:string,arg2:string):Promise<boolean>;
|
||||
|
||||
export function KenwoodSendCW(arg1:string):Promise<void>;
|
||||
|
||||
export function KenwoodStopCW():Promise<void>;
|
||||
|
||||
export function LaunchAutostartProgram(arg1:string):Promise<main.AutostartLaunchResult>;
|
||||
|
||||
export function LaunchAutostartPrograms():Promise<Array<main.AutostartLaunchResult>>;
|
||||
@@ -935,6 +939,8 @@ export function SetCompactMode(arg1:boolean):Promise<void>;
|
||||
|
||||
export function SetDVKLabel(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function SetKenwoodKeySpeed(arg1:number):Promise<void>;
|
||||
|
||||
export function SetPassphrase(arg1:string):Promise<void>;
|
||||
|
||||
export function SetScpEnabled(arg1:boolean):Promise<void>;
|
||||
|
||||
@@ -1162,6 +1162,14 @@ export function IsNewUSCounty(arg1, arg2) {
|
||||
return window['go']['main']['App']['IsNewUSCounty'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function KenwoodSendCW(arg1) {
|
||||
return window['go']['main']['App']['KenwoodSendCW'](arg1);
|
||||
}
|
||||
|
||||
export function KenwoodStopCW() {
|
||||
return window['go']['main']['App']['KenwoodStopCW']();
|
||||
}
|
||||
|
||||
export function LaunchAutostartProgram(arg1) {
|
||||
return window['go']['main']['App']['LaunchAutostartProgram'](arg1);
|
||||
}
|
||||
@@ -1818,6 +1826,10 @@ export function SetDVKLabel(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetDVKLabel'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetKenwoodKeySpeed(arg1) {
|
||||
return window['go']['main']['App']['SetKenwoodKeySpeed'](arg1);
|
||||
}
|
||||
|
||||
export function SetPassphrase(arg1) {
|
||||
return window['go']['main']['App']['SetPassphrase'](arg1);
|
||||
}
|
||||
|
||||
@@ -869,3 +869,22 @@ func (m *Manager) YaesuDo(fn func(YaesuController) error) error {
|
||||
return fn(yc)
|
||||
})
|
||||
}
|
||||
|
||||
// KenwoodController is the Kenwood/Elecraft CW-over-CAT capability (the KY keyer),
|
||||
// so a K3 can key CW through its single CAT link instead of a second COM port.
|
||||
type KenwoodController interface {
|
||||
SendCW(string) error
|
||||
StopCW() error
|
||||
SetKeySpeed(int) error
|
||||
}
|
||||
|
||||
// KenwoodDo dispatches a Kenwood control onto the CAT goroutine.
|
||||
func (m *Manager) KenwoodDo(fn func(KenwoodController) error) error {
|
||||
return m.exec(func(b Backend) error {
|
||||
kc, ok := b.(KenwoodController)
|
||||
if !ok {
|
||||
return fmt.Errorf("active CAT backend is not a Kenwood/Elecraft")
|
||||
}
|
||||
return fn(kc)
|
||||
})
|
||||
}
|
||||
|
||||
+38
-16
@@ -70,6 +70,15 @@ type Kenwood struct {
|
||||
curFreq int64
|
||||
curRXFreq int64
|
||||
curVFO string // "A" or "B"
|
||||
// Split is confirmed with FR;/FT; because IF's split bit is empty on some
|
||||
// Kenwood-dialect rigs — but that check is THROTTLED so it doesn't run every
|
||||
// poll: the two extra commands tripled the poll time on a slow K3 and made the
|
||||
// frequency (read from IF at the top of the poll) lag by seconds. Between
|
||||
// checks the last result stands.
|
||||
splitCheckAt time.Time
|
||||
splitCached bool
|
||||
splitVFOCached string
|
||||
keyWPM int // CW keyer speed, for pacing the KY buffer (see kenwood_cw.go)
|
||||
// Commands this rig answered "?;" to — asked once, then never again.
|
||||
unsupported map[string]bool
|
||||
|
||||
@@ -269,18 +278,31 @@ func (k *Kenwood) ReadState() (RigState, error) {
|
||||
// this costs two short commands per poll only where it actually works.
|
||||
split := f.Split
|
||||
if !split {
|
||||
rxv, rxOK := k.askVFO("FR;")
|
||||
txv, txOK := k.askVFO("FT;")
|
||||
if rxOK && txOK && rxv != txv {
|
||||
// Confirm with FR;/FT; only once a second, not every poll. Running the two
|
||||
// extra commands each cycle tripled the poll time on a slow K3, so the
|
||||
// frequency — already read from IF above — only surfaced every couple of
|
||||
// seconds. Between checks the last FR/FT result stands.
|
||||
if time.Since(k.splitCheckAt) >= time.Second {
|
||||
k.splitCheckAt = time.Now()
|
||||
k.splitCached, k.splitVFOCached = false, ""
|
||||
rxv, rxOK := k.askVFO("FR;")
|
||||
txv, txOK := k.askVFO("FT;")
|
||||
if rxOK && txOK && rxv != txv {
|
||||
k.splitCached = true
|
||||
// Trust FR over IF for which VFO is in use: they were asked in the
|
||||
// same breath, and a rig that leaves the split bit empty may be just
|
||||
// as vague about the VFO field. The block below picks the TRANSMIT
|
||||
// VFO as "the other one" from f.VFO, so leaving them disagreeing
|
||||
// would read the transmit frequency off the wrong dial.
|
||||
if rxv == "A" || rxv == "B" {
|
||||
k.splitVFOCached = rxv
|
||||
}
|
||||
}
|
||||
}
|
||||
if k.splitCached {
|
||||
split = true
|
||||
// Trust FR over IF for which VFO is in use: they were asked in the
|
||||
// same breath, and a rig that leaves the split bit empty may be just
|
||||
// as vague about the VFO field.
|
||||
// f.VFO too, not just the reported state: the block below picks the
|
||||
// TRANSMIT VFO as "the other one" from f.VFO, and leaving the two
|
||||
// disagreeing would read the transmit frequency off the wrong dial.
|
||||
if rxv == "A" || rxv == "B" {
|
||||
k.curVFO, s.Vfo, f.VFO = rxv, rxv, rxv
|
||||
if k.splitVFOCached != "" {
|
||||
k.curVFO, s.Vfo, f.VFO = k.splitVFOCached, k.splitVFOCached, k.splitVFOCached
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,11 +551,11 @@ func kenwoodModeDigit(mode string, hz int64) byte {
|
||||
}
|
||||
return '2'
|
||||
}
|
||||
// Any other digital mode rides on the data sideband, which on Kenwood is
|
||||
// plain USB/LSB with the rig's DATA input selected.
|
||||
if hz > 0 && hz < 10_000_000 {
|
||||
return '1'
|
||||
}
|
||||
// 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.)
|
||||
return '2'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package cat
|
||||
|
||||
// CW keying through the Kenwood/Elecraft keyer — the KY command.
|
||||
//
|
||||
// Same idea as the Yaesu KY engine and the Icom / Flex keyers: the radio holds
|
||||
// the text and keys it with its own timing, so an Elecraft K3 (or any rig that
|
||||
// speaks this dialect) needs NO WinKeyer and NO second COM port — the single CAT
|
||||
// link does frequency, mode AND CW. That matters on a K3, whose one USB port is
|
||||
// the CAT port; a separate serial keyer would need a second cable OpsLog can't
|
||||
// give it.
|
||||
//
|
||||
// KY; → KYn; n=0 buffer has room, n=1 buffer full
|
||||
// KY <text>; queue up to 24 characters (the space after KY is part
|
||||
// of the command, not padding)
|
||||
// KS nnn; keyer speed in WPM (three digits)
|
||||
// RX; drop to receive — used to abort a send
|
||||
//
|
||||
// KY is the Elecraft-documented CW-over-CAT path on the K3/K4. A rig that refuses
|
||||
// it answers "?;", which SendCW turns into a stated reason rather than silence.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// kenwoodCWChunk is the most characters one KY command accepts.
|
||||
const kenwoodCWChunk = 24
|
||||
|
||||
// kenwoodCWAllowed is what the keyer can send; anything else is dropped, since an
|
||||
// unsupported byte can abort the buffer and lose the rest of the message.
|
||||
const kenwoodCWAllowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /?.,-=+:;()"
|
||||
|
||||
// SendCW queues a message on the rig's keyer, fed in 24-character pieces, waiting
|
||||
// for buffer room between pieces so a long macro doesn't lose its tail.
|
||||
func (k *Kenwood) SendCW(text string) error {
|
||||
msg := filterKenwoodCW(text)
|
||||
if msg == "" {
|
||||
return nil
|
||||
}
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if k.port == nil {
|
||||
return fmt.Errorf("kenwood: not connected")
|
||||
}
|
||||
for len(msg) > 0 {
|
||||
n := kenwoodCWChunk
|
||||
if len(msg) < n {
|
||||
n = len(msg)
|
||||
}
|
||||
chunk := msg[:n]
|
||||
msg = msg[n:]
|
||||
k.waitCWBuffer(3 * time.Second)
|
||||
if err := k.write("KY " + chunk + ";"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.afterKY(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Pace the next piece by how long this one takes to key, so we never overrun
|
||||
// the 24-character buffer (the rig silently drops what doesn't fit).
|
||||
if len(msg) > 0 {
|
||||
time.Sleep(kenwoodCWDuration(chunk, k.keyerWPM()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// afterKY reads briefly after a KY write. An accepted KY says nothing; a REJECT
|
||||
// answers "?;". Reading it straight off the port (not through the shared rx
|
||||
// buffer) keeps that stray frame from being picked up by the next poll's ask —
|
||||
// which would mis-mark an unrelated command unsupported and desync the link — and
|
||||
// turns a silent non-transmission into a stated reason. The caller holds k.mu.
|
||||
func (k *Kenwood) afterKY() error {
|
||||
deadline := time.Now().Add(150 * time.Millisecond)
|
||||
tmp := make([]byte, 64)
|
||||
var buf []byte
|
||||
for time.Now().Before(deadline) {
|
||||
n, err := k.port.Read(tmp)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if n > 0 {
|
||||
buf = append(buf, tmp[:n]...)
|
||||
}
|
||||
}
|
||||
if strings.Contains(string(buf), "?;") {
|
||||
return fmt.Errorf("this radio rejected CW over CAT (it answered \"?;\" to KY). " +
|
||||
"Switch the keyer engine to the serial-port keyer (DTR=CW) on a COM port instead")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitCWBuffer blocks until the keyer reports room, or the deadline passes. A rig
|
||||
// that never answers the KY; status query is not a reason to refuse to send — we
|
||||
// go ahead, and the per-chunk pacing covers the worst case. The caller holds k.mu.
|
||||
func (k *Kenwood) waitCWBuffer(within time.Duration) {
|
||||
deadline := time.Now().Add(within)
|
||||
for time.Now().Before(deadline) {
|
||||
if k.unsupported["KY"] {
|
||||
return // this rig doesn't report buffer state — pacing covers it
|
||||
}
|
||||
r, err := k.ask("KY;")
|
||||
if err != nil {
|
||||
return // unsupported / timeout — send anyway, pacing covers it
|
||||
}
|
||||
if !kenwoodCWBufferFull(r) {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// kenwoodCWBufferFull reads the KY; status reply. Deliberately asymmetric: only a
|
||||
// clear "1" after KY means full. Anything else reads as "go ahead" — refusing to
|
||||
// send because a status line was phrased unexpectedly is the worse failure.
|
||||
func kenwoodCWBufferFull(reply string) bool {
|
||||
r := strings.TrimSpace(reply)
|
||||
if !strings.HasPrefix(strings.ToUpper(r), "KY") {
|
||||
return false
|
||||
}
|
||||
for _, c := range r[2:] {
|
||||
switch c {
|
||||
case '0':
|
||||
return false
|
||||
case '1':
|
||||
return true
|
||||
case ' ', ';':
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StopCW aborts the message being sent. Kenwood documents no KY buffer-clear, so
|
||||
// this drops the transmitter — RX; forces receive, which is what an operator
|
||||
// pressing Escape wants; anything still queued is not keyed on the air.
|
||||
func (k *Kenwood) StopCW() error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if k.port == nil {
|
||||
return fmt.Errorf("kenwood: not connected")
|
||||
}
|
||||
return k.write("RX;")
|
||||
}
|
||||
|
||||
// SetKeySpeed sets the keyer speed (WPM) via KS and remembers it for pacing.
|
||||
func (k *Kenwood) SetKeySpeed(wpm int) error {
|
||||
if wpm < 4 {
|
||||
wpm = 4
|
||||
}
|
||||
if wpm > 99 {
|
||||
wpm = 99 // KS is three digits but the K3 keyer tops out well below 100
|
||||
}
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.keyWPM = wpm
|
||||
if k.port == nil {
|
||||
return fmt.Errorf("kenwood: not connected")
|
||||
}
|
||||
return k.write(fmt.Sprintf("KS%03d;", wpm))
|
||||
}
|
||||
|
||||
// keyerWPM is the speed to pace the buffer by. The caller holds k.mu.
|
||||
func (k *Kenwood) keyerWPM() int {
|
||||
if k.keyWPM >= 4 {
|
||||
return k.keyWPM
|
||||
}
|
||||
return 20
|
||||
}
|
||||
|
||||
// kenwoodCWDuration estimates how long a piece of text takes to key (PARIS
|
||||
// timing: a character averages 10 dits, a dit is 1.2/wpm seconds).
|
||||
func kenwoodCWDuration(text string, wpm int) time.Duration {
|
||||
if wpm < 4 {
|
||||
wpm = 20
|
||||
}
|
||||
ditMs := 1200.0 / float64(wpm)
|
||||
return time.Duration(float64(len(text))*10*ditMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// filterKenwoodCW upper-cases and strips what the keyer cannot send; whitespace
|
||||
// becomes a single word gap.
|
||||
func filterKenwoodCW(text string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range strings.ToUpper(text) {
|
||||
if r == '\t' || r == '\n' || r == '\r' {
|
||||
b.WriteByte(' ')
|
||||
continue
|
||||
}
|
||||
if strings.ContainsRune(kenwoodCWAllowed, r) {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return strings.Join(strings.Fields(b.String()), " ")
|
||||
}
|
||||
@@ -300,6 +300,11 @@ func TestKenwoodSplitFromFRFT(t *testing.T) {
|
||||
|
||||
// Split on, IF still silent about it: receive on A, transmit on B.
|
||||
rig.split = true
|
||||
// The FR/FT split re-check is throttled to once a second (it tripled the poll
|
||||
// time on a slow K3), so back-to-back reads reuse the last result. Simulate the
|
||||
// recheck window having elapsed — in the field the 250 ms poll covers it inside
|
||||
// a second.
|
||||
k.splitCheckAt = time.Time{}
|
||||
if s, err = k.ReadState(); err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@ func TestKenwoodModeDigit(t *testing.T) {
|
||||
{"RTTY", 14080000, '6'},
|
||||
{"AM", 7150000, '5'},
|
||||
{"FM", 145000000, '4'},
|
||||
{"FT8", 7074000, '1'}, // data rides on the sideband for the band
|
||||
{"FT8", 14074000, '2'},
|
||||
{"FT8", 7074000, '2'}, // data is ALWAYS USB, even below 10 MHz (K3 "DATA REV" otherwise)
|
||||
{"FT8", 14074000, '2'}, // …and above
|
||||
{"", 14074000, 0}, // nothing to set
|
||||
}
|
||||
for _, c := range cases {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||
appVersion = "0.23.2"
|
||||
appVersion = "0.23.3"
|
||||
|
||||
// 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