Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e00488fad8 | ||
|
|
12c61dc35a | ||
|
|
0c0e8b06ba | ||
|
|
1bd3896ca7 | ||
|
|
7e6c0b4f7e |
@@ -10239,7 +10239,7 @@ func titleEntity(s string) string {
|
|||||||
// the configured "Recording mic", transmit via "To Radio", preview via
|
// the configured "Recording mic", transmit via "To Radio", preview via
|
||||||
// "Listening".
|
// "Listening".
|
||||||
|
|
||||||
const dvkSlots = 6
|
const dvkSlots = 12
|
||||||
|
|
||||||
// DVKMessage is one voice-keyer slot for the UI.
|
// DVKMessage is one voice-keyer slot for the UI.
|
||||||
type DVKMessage struct {
|
type DVKMessage struct {
|
||||||
@@ -10300,6 +10300,22 @@ func (a *App) GetDVKMessages() []DVKMessage {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DVKDelete removes a slot's recording AND its label — a deleted message is a
|
||||||
|
// free slot, per its operator.
|
||||||
|
func (a *App) DVKDelete(slot int) error {
|
||||||
|
if slot < 1 || slot > dvkSlots {
|
||||||
|
return fmt.Errorf("bad slot")
|
||||||
|
}
|
||||||
|
if err := os.Remove(a.dvkPath(slot)); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("delete message %d: %w", slot, err)
|
||||||
|
}
|
||||||
|
if a.settings != nil {
|
||||||
|
_ = a.settings.Set(a.ctx, dvkLabelKey(slot), "")
|
||||||
|
}
|
||||||
|
applog.Printf("dvk: message F%d deleted (label cleared)", slot)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetDVKLabel renames a voice-keyer slot.
|
// SetDVKLabel renames a voice-keyer slot.
|
||||||
func (a *App) SetDVKLabel(slot int, label string) error {
|
func (a *App) SetDVKLabel(slot int, label string) error {
|
||||||
if a.settings == nil {
|
if a.settings == nil {
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.27.3",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"KPA500: the amplifier no longer switches itself off and commands respond instantly. A command this model does not know (the KPA1500’s ATU poll) was tearing the link down every cycle, and each reconnect toggled the serial control lines — which are the KPA500’s power switch. The lines are now held steady, silence is not treated as a dead link, and the baud is picked from a list.",
|
||||||
|
"FT decodes: within a period, decodes are listed in arrival order — mirroring the decoder’s own window — instead of strongest-first.",
|
||||||
|
"Voice keyer: twelve message slots (F1–F12) instead of six.",
|
||||||
|
"Preferences open smoothly on a busy station: while the dialog is open, cluster spots, FT decodes and CAT snapshots queue quietly instead of repainting the whole window behind it — everything catches up the moment it closes.",
|
||||||
|
"Voice keyer: a delete button per message — removes the recording and clears the label."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"KPA500 : l’ampli ne s’éteint plus tout seul et les commandes répondent instantanément. Une commande inconnue de ce modèle (le poll ATU du KPA1500) détruisait le lien à chaque cycle, et chaque reconnexion basculait les lignes de contrôle série — qui sont l’interrupteur du KPA500. Les lignes sont désormais tenues stables, le silence n’est plus traité comme un lien mort, et le baud se choisit dans une liste.",
|
||||||
|
"FT decodes : dans une période, les décodages sont listés dans l’ordre d’arrivée — comme la fenêtre du décodeur — au lieu du plus fort d’abord.",
|
||||||
|
"Manipulateur vocal : douze messages (F1–F12) au lieu de six.",
|
||||||
|
"Les Préférences restent fluides sur une station chargée : dialogue ouvert, les spots cluster, les décodages FT et les instantanés CAT patientent en file au lieu de repeindre toute la fenêtre derrière — tout se rattrape à la fermeture.",
|
||||||
|
"Manipulateur vocal : un bouton supprimer par message — efface l’enregistrement et le libellé."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
+36
-1
@@ -2228,6 +2228,15 @@ export default function App() {
|
|||||||
const [bulkEditIds, setBulkEditIds] = useState<number[]>([]);
|
const [bulkEditIds, setBulkEditIds] = useState<number[]>([]);
|
||||||
const [bulkEditOpen, setBulkEditOpen] = useState(false);
|
const [bulkEditOpen, setBulkEditOpen] = useState(false);
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
// While the Settings dialog is open, the spot/decode flushes and the CAT
|
||||||
|
// snapshot stream are PAUSED (data keeps accumulating in the pending refs).
|
||||||
|
// Every flush re-renders the whole App tree behind the dialog — cluster
|
||||||
|
// grid, decodes panel, thousands of nodes — and with a busy cluster plus
|
||||||
|
// two decoders the pointer visibly stuttered over the preferences.
|
||||||
|
const showSettingsRef = useRef(false);
|
||||||
|
useEffect(() => { showSettingsRef.current = showSettings; }, [showSettings]);
|
||||||
|
const flushSpotsRef = useRef<() => void>(() => {});
|
||||||
|
const flushDecodesRef = useRef<() => void>(() => {});
|
||||||
// Re-read the "beam on map" toggle when Preferences closes (it's edited there).
|
// Re-read the "beam on map" toggle when Preferences closes (it's edited there).
|
||||||
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
|
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
|
||||||
useEffect(() => { if (!showSettings) setRotorCompact(localStorage.getItem('opslog.rotorCompact') === '1'); }, [showSettings]);
|
useEffect(() => { if (!showSettings) setRotorCompact(localStorage.getItem('opslog.rotorCompact') === '1'); }, [showSettings]);
|
||||||
@@ -3435,7 +3444,15 @@ export default function App() {
|
|||||||
// Apply a CAT snapshot to the entry strip (freq/band/mode), unless the user
|
// Apply a CAT snapshot to the entry strip (freq/band/mode), unless the user
|
||||||
// just typed something (freeze window) or locked a field. Shared by the live
|
// just typed something (freeze window) or locked a field. Shared by the live
|
||||||
// cat:state event and the startup poll below.
|
// cat:state event and the startup poll below.
|
||||||
|
const lastCatWhileSettingsRef = useRef(0);
|
||||||
function applyCatState(s: CATState) {
|
function applyCatState(s: CATState) {
|
||||||
|
// Behind the Settings dialog nobody reads a frequency four times a second;
|
||||||
|
// each snapshot re-renders the whole App tree under the pointer.
|
||||||
|
if (showSettingsRef.current) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCatWhileSettingsRef.current < 2000) return;
|
||||||
|
lastCatWhileSettingsRef.current = now;
|
||||||
|
}
|
||||||
setCatState(s);
|
setCatState(s);
|
||||||
if (!s?.connected) return;
|
if (!s?.connected) return;
|
||||||
// A snapshot arriving during the freeze used to be DROPPED, and that lost the
|
// A snapshot arriving during the freeze used to be DROPPED, and that lost the
|
||||||
@@ -3580,11 +3597,19 @@ export default function App() {
|
|||||||
// Commit the staged spots: resolve the status for any slot we don't know yet
|
// Commit the staged spots: resolve the status for any slot we don't know yet
|
||||||
// FIRST, then insert the rows — so they appear with the right badge already
|
// FIRST, then insert the rows — so they appear with the right badge already
|
||||||
// painted instead of flashing plain text then flipping to a pill.
|
// painted instead of flashing plain text then flipping to a pill.
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
const flushPendingSpots = async () => {
|
const flushPendingSpots = async () => {
|
||||||
pendingSpotTimer.current = undefined;
|
pendingSpotTimer.current = undefined;
|
||||||
|
// Settings open: leave everything queued (bounded) and repaint nothing.
|
||||||
|
if (showSettingsRef.current) {
|
||||||
|
const cap = spotsCapRef.current;
|
||||||
|
if (pendingSpotsRef.current.length > cap) pendingSpotsRef.current = pendingSpotsRef.current.slice(-cap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const batch = pendingSpotsRef.current;
|
const batch = pendingSpotsRef.current;
|
||||||
pendingSpotsRef.current = [];
|
pendingSpotsRef.current = [];
|
||||||
if (batch.length === 0) return;
|
if (batch.length === 0) return;
|
||||||
|
// (registered below so closing Settings can drain the queue)
|
||||||
// Resolve unknown statuses before the rows go in.
|
// Resolve unknown statuses before the rows go in.
|
||||||
try {
|
try {
|
||||||
const known = spotStatusRef.current;
|
const known = spotStatusRef.current;
|
||||||
@@ -3640,6 +3665,7 @@ export default function App() {
|
|||||||
return next.length > cap ? next.slice(0, cap) : next;
|
return next.length > cap ? next.slice(0, cap) : next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
flushSpotsRef.current = () => { void flushPendingSpots(); };
|
||||||
const unsubSpot = EventsOn('cluster:spot', (sp: ClusterSpot) => {
|
const unsubSpot = EventsOn('cluster:spot', (sp: ClusterSpot) => {
|
||||||
// Stage the spot; a short timer resolves its status then commits it.
|
// Stage the spot; a short timer resolves its status then commits it.
|
||||||
pendingSpotsRef.current.push(sp);
|
pendingSpotsRef.current.push(sp);
|
||||||
@@ -3687,6 +3713,10 @@ export default function App() {
|
|||||||
// decodes panel and plain worked in the cluster list two seconds later.
|
// decodes panel and plain worked in the cluster list two seconds later.
|
||||||
const flushDecodes = async () => {
|
const flushDecodes = async () => {
|
||||||
pendingDecodeTimer.current = undefined;
|
pendingDecodeTimer.current = undefined;
|
||||||
|
if (showSettingsRef.current) {
|
||||||
|
if (pendingDecodesRef.current.length > 3000) pendingDecodesRef.current = pendingDecodesRef.current.slice(-3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const batch = pendingDecodesRef.current;
|
const batch = pendingDecodesRef.current;
|
||||||
pendingDecodesRef.current = [];
|
pendingDecodesRef.current = [];
|
||||||
if (batch.length === 0) return;
|
if (batch.length === 0) return;
|
||||||
@@ -3729,6 +3759,7 @@ export default function App() {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
flushDecodesRef.current = () => { void flushDecodes(); };
|
||||||
|
|
||||||
const unsubDecode = EventsOn('udp:decode', (d: DecodeRow) => {
|
const unsubDecode = EventsOn('udp:decode', (d: DecodeRow) => {
|
||||||
pendingDecodesRef.current.push(d);
|
pendingDecodesRef.current.push(d);
|
||||||
@@ -4375,6 +4406,10 @@ export default function App() {
|
|||||||
// The stable wrapper the dialog actually receives.
|
// The stable wrapper the dialog actually receives.
|
||||||
const openQSOFromSettings = useCallback((id: number) => openEditRef.current(id), []);
|
const openQSOFromSettings = useCallback((id: number) => openEditRef.current(id), []);
|
||||||
const closeSettings = useCallback(() => {
|
const closeSettings = useCallback(() => {
|
||||||
|
// Synchronously: the ref effect runs after the next render, and flushing
|
||||||
|
// through a still-true ref would hit the pause gate again.
|
||||||
|
showSettingsRef.current = false;
|
||||||
|
window.setTimeout(() => { flushSpotsRef.current(); flushDecodesRef.current(); }, 50);
|
||||||
setShowSettings(false);
|
setShowSettings(false);
|
||||||
setSettingsSection(undefined);
|
setSettingsSection(undefined);
|
||||||
refreshChaseNew();
|
refreshChaseNew();
|
||||||
@@ -5203,7 +5238,7 @@ export default function App() {
|
|||||||
if (dvkActiveRef.current) {
|
if (dvkActiveRef.current) {
|
||||||
// Voice keyer: plain F1..F6 transmit the message; Ctrl+F1..F5 → tabs.
|
// Voice keyer: plain F1..F6 transmit the message; Ctrl+F1..F5 → tabs.
|
||||||
if (mod && n <= 5) { e.preventDefault(); setDetailTab(TABS[n - 1]); return; }
|
if (mod && n <= 5) { e.preventDefault(); setDetailTab(TABS[n - 1]); return; }
|
||||||
if (plain && n <= 6) { e.preventDefault(); dvkPlayRef.current(n); return; }
|
if (plain && n <= 12) { e.preventDefault(); dvkPlayRef.current(n); return; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// No keyer: plain F1..F5 switch the detail tab (labels read "F1…").
|
// No keyer: plain F1..F5 switch the detail tab (labels read "F1…").
|
||||||
|
|||||||
@@ -498,10 +498,11 @@ function buildPeriods(filtered: Decode[], txMsgs: TxMsg[]) {
|
|||||||
// the same way it was grouped.
|
// the same way it was grouped.
|
||||||
tr: trSeconds(g.decodes[0]?.mode ?? g.tx[0]?.mode, g.decodes[0]?.tr_period),
|
tr: trSeconds(g.decodes[0]?.mode ?? g.tx[0]?.mode, g.decodes[0]?.tr_period),
|
||||||
tx: g.tx,
|
tx: g.tx,
|
||||||
// Strongest first inside a period: the eye should land on what is
|
// ARRIVAL order inside a period, per the operator: it mirrors the
|
||||||
// workable, and time within a slot means nothing — they were all
|
// decoder's own window line for line, which makes the two screens
|
||||||
// transmitting simultaneously.
|
// comparable at a glance — the strongest-first sort scrambled that
|
||||||
decodes: g.decodes.sort((x, y) => y.snr - x.snr),
|
// correspondence, and SNR is right there in its column anyway.
|
||||||
|
decodes: g.decodes,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Props = {
|
|||||||
phoneOk: boolean; // false when the rig is on a non-phone mode → DVK TX blocked
|
phoneOk: boolean; // false when the rig is on a non-phone mode → DVK TX blocked
|
||||||
};
|
};
|
||||||
|
|
||||||
// Operating panel for the Digital Voice Keyer — transmits the recorded F1–F6
|
// Operating panel for the Digital Voice Keyer — transmits the recorded F1–F12
|
||||||
// voice messages to the rig ("To Radio"). Mirrors the WinKeyer panel's slot in
|
// voice messages to the rig ("To Radio"). Mirrors the WinKeyer panel's slot in
|
||||||
// the reserved area. Recording/labeling lives in Settings → Audio.
|
// the reserved area. Recording/labeling lives in Settings → Audio.
|
||||||
export function DvkPanel({ messages, status, onPlay, onStop, onClose, autoCq, autoCqSecs, onToggleAutoCq, onSetAutoCqSecs, phoneOk }: Props) {
|
export function DvkPanel({ messages, status, onPlay, onStop, onClose, autoCq, autoCqSecs, onToggleAutoCq, onSetAutoCqSecs, phoneOk }: Props) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
|
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
|
||||||
GetListsSettings, SaveListsSettings,
|
GetListsSettings, SaveListsSettings,
|
||||||
GetCATSettings, SaveCATSettings, GetRadios, SaveRadios, SetActiveRadio, ActiveRadioID, DiscoverFlexRadios,
|
GetCATSettings, SaveCATSettings, GetRadios, SaveRadios, SetActiveRadio, ActiveRadioID, DiscoverFlexRadios, DVKDelete,
|
||||||
GetAudioMonitorPref,
|
GetAudioMonitorPref,
|
||||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||||
@@ -4416,8 +4416,17 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>Baud</Label>
|
<Label>Baud</Label>
|
||||||
<Input type="number" min={1200} value={amp.baud}
|
{/* A list, not a free number: the KPA500 report that
|
||||||
onChange={(e) => patchAmp(i, { baud: parseInt(e.target.value) || 115200 })} className="font-mono" />
|
began this had its operator wondering whether a typed
|
||||||
|
baud was the whole problem. These are the rates the
|
||||||
|
supported amplifiers actually speak. */}
|
||||||
|
<select value={String(amp.baud)}
|
||||||
|
onChange={(e) => patchAmp(i, { baud: parseInt(e.target.value) })}
|
||||||
|
className="h-9 w-full px-2 rounded-md border border-border bg-background text-sm font-mono">
|
||||||
|
{[4800, 9600, 19200, 38400, 57600, 115200].map((b) => (
|
||||||
|
<option key={b} value={String(b)}>{b}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -7169,6 +7178,15 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
>
|
>
|
||||||
{dvkStat.playing ? t('aud.stop') : t('aud.play')}
|
{dvkStat.playing ? t('aud.stop') : t('aud.play')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline" size="sm" className="h-8 w-9 shrink-0 px-0 text-danger hover:text-danger"
|
||||||
|
title={t('aud.deleteMsg')}
|
||||||
|
disabled={!m.has_audio || dvkStat.recording || dvkStat.playing}
|
||||||
|
onClick={() => DVKDelete(m.slot).then(reloadDvk).catch((err) => setDvkErr(String(err?.message ?? err)))}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ const en: Dict = {
|
|||||||
'wkp.cwSpeed': 'CW speed (WPM)', 'wkp.faster': 'Faster', 'wkp.slower': 'Slower', 'wkp.cwText': 'CW text', 'wkp.sendOnTypeHint': 'Key each character live as you type (backspace removes un-sent chars)', 'wkp.sendOnType': 'send on type', 'wkp.phLive': 'Type — sent live…', 'wkp.phEnter': 'Type and press Enter to send…', 'wkp.clear': 'Clear', 'wkp.send': 'Send', 'wkp.abort': 'Abort (clear keyer buffer)', 'wkp.stop': 'Stop',
|
'wkp.cwSpeed': 'CW speed (WPM)', 'wkp.faster': 'Faster', 'wkp.slower': 'Slower', 'wkp.cwText': 'CW text', 'wkp.sendOnTypeHint': 'Key each character live as you type (backspace removes un-sent chars)', 'wkp.sendOnType': 'send on type', 'wkp.phLive': 'Type — sent live…', 'wkp.phEnter': 'Type and press Enter to send…', 'wkp.clear': 'Clear', 'wkp.send': 'Send', 'wkp.abort': 'Abort (clear keyer buffer)', 'wkp.stop': 'Stop',
|
||||||
'wkp.breakIn': 'Break-in', 'wkp.breakInHint': "The rig's CW keyer only transmits when break-in is SEMI or FULL. OFF keys the sidetone but stays in receive.", 'wkp.bkOff': 'OFF', 'wkp.bkOffWarn': "won't transmit — set SEMI or FULL",
|
'wkp.breakIn': 'Break-in', 'wkp.breakInHint': "The rig's CW keyer only transmits when break-in is SEMI or FULL. OFF keys the sidetone but stays in receive.", 'wkp.bkOff': 'OFF', 'wkp.bkOffWarn': "won't transmit — set SEMI or FULL",
|
||||||
'wkp.autoCallHint': 'Click a CQ macro (one whose text contains CQ) to resend it on a loop — message, gap, repeat — until you send another macro (e.g. a report), press Stop, or hit ESC. Non-CQ macros send once.', 'wkp.autoCall': 'Auto-call', 'wkp.gap': 'gap', 'wkp.gapHint': 'Seconds to wait after the message before resending', 'wkp.loopHint': 'click a CQ macro to loop it', 'wkp.macroN': 'Macro {n}',
|
'wkp.autoCallHint': 'Click a CQ macro (one whose text contains CQ) to resend it on a loop — message, gap, repeat — until you send another macro (e.g. a report), press Stop, or hit ESC. Non-CQ macros send once.', 'wkp.autoCall': 'Auto-call', 'wkp.gap': 'gap', 'wkp.gapHint': 'Seconds to wait after the message before resending', 'wkp.loopHint': 'click a CQ macro to loop it', 'wkp.macroN': 'Macro {n}',
|
||||||
'dvkp.voiceKeyer': 'Voice keyer', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Repeat a CQ-labelled message on a timer until you stop it or play another slot', 'dvkp.gap': 'Gap', 'dvkp.notPhone': 'The voice keyer only transmits on a phone mode (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Disable voice keyer', 'dvkp.noMsgPre': 'No messages recorded yet. Open', 'dvkp.settingsPath': 'Settings → Audio devices & voice keyer', 'dvkp.noMsgPost': 'to record F1–F6.', 'dvkp.transmit': 'Transmit F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — empty', 'dvkp.message': 'message',
|
'dvkp.voiceKeyer': 'Voice keyer', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Repeat a CQ-labelled message on a timer until you stop it or play another slot', 'dvkp.gap': 'Gap', 'dvkp.notPhone': 'The voice keyer only transmits on a phone mode (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Disable voice keyer', 'dvkp.noMsgPre': 'No messages recorded yet. Open', 'dvkp.settingsPath': 'Settings → Audio devices & voice keyer', 'dvkp.noMsgPost': 'to record F1–F12.', 'dvkp.transmit': 'Transmit F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — empty', 'dvkp.message': 'message',
|
||||||
'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.', 'agp.filterOnHint': 'Showing antennas for {band} only — click to show all bands', 'agp.filterOffHint': 'Showing all antennas — click to show only the current band',
|
'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.', 'agp.filterOnHint': 'Showing antennas for {band} only — click to show all bands', 'agp.filterOffHint': 'Showing all antennas — click to show only the current band',
|
||||||
'ampw.title': 'Amplifier', 'ampw.all': 'All amplifiers', 'ampw.pick': 'Which amplifier the widget shows',
|
'ampw.title': 'Amplifier', 'ampw.all': 'All amplifiers', 'ampw.pick': 'Which amplifier the widget shows',
|
||||||
'ampw.showHint': 'Amplifier · click to show', 'ampw.hideHint': 'Amplifier — shown · click to hide',
|
'ampw.showHint': 'Amplifier · click to show', 'ampw.hideHint': 'Amplifier — shown · click to hide',
|
||||||
@@ -535,7 +535,7 @@ const en: Dict = {
|
|||||||
'aud.preroll': 'Pre-roll (seconds)', 'aud.format': 'File format', 'aud.wav': 'WAV (lossless, larger)', 'aud.mp3': 'MP3 (compressed, small)',
|
'aud.preroll': 'Pre-roll (seconds)', 'aud.format': 'File format', 'aud.wav': 'WAV (lossless, larger)', 'aud.mp3': 'MP3 (compressed, small)',
|
||||||
'aud.fromLevel': 'From Radio level', 'aud.txLevel': 'Voice keyer level', 'aud.txLevelHint': 'Level of the recorded messages sent to the radio. Raise it if your voice keyer is much quieter than your microphone; Play previews at this same level. If the radio transmits almost nothing, its modulation source is still the front microphone: on an FTDX10 set MENU → SSB MOD SOURCE to REAR (the USB input).', 'aud.micLevel': 'Mic level', 'aud.qsoPlayLevel': 'QSO playback level', 'aud.levelHint': 'If your voice is louder than the station, lower Mic level.',
|
'aud.fromLevel': 'From Radio level', 'aud.txLevel': 'Voice keyer level', 'aud.txLevelHint': 'Level of the recorded messages sent to the radio. Raise it if your voice keyer is much quieter than your microphone; Play previews at this same level. If the radio transmits almost nothing, its modulation source is still the front microphone: on an FTDX10 set MENU → SSB MOD SOURCE to REAR (the USB input).', 'aud.micLevel': 'Mic level', 'aud.qsoPlayLevel': 'QSO playback level', 'aud.levelHint': 'If your voice is louder than the station, lower Mic level.',
|
||||||
'aud.autoSend': 'Auto-send the recording to the station by e-mail when I log a QSO',
|
'aud.autoSend': 'Auto-send the recording to the station by e-mail when I log a QSO',
|
||||||
'aud.dvkTitle': 'Voice keyer messages (F1–F6)', 'aud.pttMethod': 'PTT method', 'aud.pttNone': 'None (VOX)', 'aud.pttCat': 'CAT (the radio link)', 'aud.pttRts': 'Serial RTS', 'aud.pttDtr': 'Serial DTR',
|
'aud.dvkTitle': 'Voice keyer messages (F1–F12)', 'aud.deleteMsg': 'Delete this message', 'aud.pttMethod': 'PTT method', 'aud.pttNone': 'None (VOX)', 'aud.pttCat': 'CAT (the radio link)', 'aud.pttRts': 'Serial RTS', 'aud.pttDtr': 'Serial DTR',
|
||||||
'aud.testPtt': 'Test PTT', 'aud.pttPort': 'PTT COM port', 'aud.pickPort': 'Pick a COM port', 'aud.selectPort': '— select —', 'aud.refresh': 'Refresh',
|
'aud.testPtt': 'Test PTT', 'aud.pttPort': 'PTT COM port', 'aud.pickPort': 'Pick a COM port', 'aud.selectPort': '— select —', 'aud.refresh': 'Refresh',
|
||||||
'aud.msgPlaceholder': 'Message {n} label (CQ, report, 73…)', 'aud.holdRec': '● Hold to rec', 'aud.recordingNow': '● Recording…', 'aud.play': '▶ Play', 'aud.stop': '■ Stop',
|
'aud.msgPlaceholder': 'Message {n} label (CQ, report, 73…)', 'aud.holdRec': '● Hold to rec', 'aud.recordingNow': '● Recording…', 'aud.play': '▶ Play', 'aud.stop': '■ Stop',
|
||||||
'aud.errPttTest': 'PTT test: ', 'aud.errRecord': 'Record: ', 'aud.errSave': 'Save: ', 'aud.errPlay': 'Play: ',
|
'aud.errPttTest': 'PTT test: ', 'aud.errRecord': 'Record: ', 'aud.errSave': 'Save: ', 'aud.errPlay': 'Play: ',
|
||||||
@@ -955,7 +955,7 @@ const fr: Dict = {
|
|||||||
'wkp.cwSpeed': 'Vitesse CW (WPM)', 'wkp.faster': 'Plus rapide', 'wkp.slower': 'Plus lent', 'wkp.cwText': 'Texte CW', 'wkp.sendOnTypeHint': 'Manipule chaque caractère en direct à la frappe (retour arrière supprime les caractères non émis)', 'wkp.sendOnType': 'émission à la frappe', 'wkp.phLive': 'Tape — émis en direct…', 'wkp.phEnter': 'Tape et appuie sur Entrée pour émettre…', 'wkp.clear': 'Effacer', 'wkp.send': 'Émettre', 'wkp.abort': 'Interrompre (vider le tampon du manipulateur)', 'wkp.stop': 'Stop',
|
'wkp.cwSpeed': 'Vitesse CW (WPM)', 'wkp.faster': 'Plus rapide', 'wkp.slower': 'Plus lent', 'wkp.cwText': 'Texte CW', 'wkp.sendOnTypeHint': 'Manipule chaque caractère en direct à la frappe (retour arrière supprime les caractères non émis)', 'wkp.sendOnType': 'émission à la frappe', 'wkp.phLive': 'Tape — émis en direct…', 'wkp.phEnter': 'Tape et appuie sur Entrée pour émettre…', 'wkp.clear': 'Effacer', 'wkp.send': 'Émettre', 'wkp.abort': 'Interrompre (vider le tampon du manipulateur)', 'wkp.stop': 'Stop',
|
||||||
'wkp.breakIn': 'Break-in', 'wkp.breakInHint': "Le manipulateur interne de la radio n'émet que si le break-in est SEMI ou FULL. OFF génère la tonalité mais reste en réception.", 'wkp.bkOff': 'OFF', 'wkp.bkOffWarn': "n'émettra pas — mettre SEMI ou FULL",
|
'wkp.breakIn': 'Break-in', 'wkp.breakInHint': "Le manipulateur interne de la radio n'émet que si le break-in est SEMI ou FULL. OFF génère la tonalité mais reste en réception.", 'wkp.bkOff': 'OFF', 'wkp.bkOffWarn': "n'émettra pas — mettre SEMI ou FULL",
|
||||||
'wkp.autoCallHint': "Clique une macro CQ (dont le texte contient CQ) pour la réémettre en boucle — message, pause, répétition — jusqu'à envoyer une autre macro (ex. un report), appuyer sur Stop ou ESC. Les macros non-CQ ne sont émises qu'une fois.", 'wkp.autoCall': 'Appel auto', 'wkp.gap': 'pause', 'wkp.gapHint': 'Secondes à attendre après le message avant de réémettre', 'wkp.loopHint': 'clique une macro CQ pour la boucler', 'wkp.macroN': 'Macro {n}',
|
'wkp.autoCallHint': "Clique une macro CQ (dont le texte contient CQ) pour la réémettre en boucle — message, pause, répétition — jusqu'à envoyer une autre macro (ex. un report), appuyer sur Stop ou ESC. Les macros non-CQ ne sont émises qu'une fois.", 'wkp.autoCall': 'Appel auto', 'wkp.gap': 'pause', 'wkp.gapHint': 'Secondes à attendre après le message avant de réémettre', 'wkp.loopHint': 'clique une macro CQ pour la boucler', 'wkp.macroN': 'Macro {n}',
|
||||||
'dvkp.voiceKeyer': 'Manipulateur vocal', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Répète un message libellé CQ à intervalle régulier jusqu\'à l\'arrêt ou la lecture d\'un autre slot', 'dvkp.gap': 'Intervalle', 'dvkp.notPhone': 'Le manipulateur vocal n\'émet qu\'en mode phonie (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Désactiver le manipulateur vocal', 'dvkp.noMsgPre': 'Aucun message enregistré. Ouvre', 'dvkp.settingsPath': 'Réglages → Périphériques audio & manipulateur vocal', 'dvkp.noMsgPost': 'pour enregistrer F1–F6.', 'dvkp.transmit': 'Émettre F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — vide', 'dvkp.message': 'message',
|
'dvkp.voiceKeyer': 'Manipulateur vocal', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Répète un message libellé CQ à intervalle régulier jusqu\'à l\'arrêt ou la lecture d\'un autre slot', 'dvkp.gap': 'Intervalle', 'dvkp.notPhone': 'Le manipulateur vocal n\'émet qu\'en mode phonie (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Désactiver le manipulateur vocal', 'dvkp.noMsgPre': 'Aucun message enregistré. Ouvre', 'dvkp.settingsPath': 'Réglages → Périphériques audio & manipulateur vocal', 'dvkp.noMsgPost': 'pour enregistrer F1–F12.', 'dvkp.transmit': 'Émettre F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — vide', 'dvkp.message': 'message',
|
||||||
'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.', 'agp.filterOnHint': 'Antennes du {band} uniquement — clic pour afficher toutes les bandes', 'agp.filterOffHint': 'Toutes les antennes affichées — clic pour n’afficher que la bande courante',
|
'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.', 'agp.filterOnHint': 'Antennes du {band} uniquement — clic pour afficher toutes les bandes', 'agp.filterOffHint': 'Toutes les antennes affichées — clic pour n’afficher que la bande courante',
|
||||||
'ampw.title': 'Amplificateur', 'ampw.all': 'Tous les amplis', 'ampw.pick': 'Ampli affiché par le widget',
|
'ampw.title': 'Amplificateur', 'ampw.all': 'Tous les amplis', 'ampw.pick': 'Ampli affiché par le widget',
|
||||||
'ampw.showHint': 'Amplificateur · cliquer pour afficher', 'ampw.hideHint': 'Amplificateur — affiché · cliquer pour masquer',
|
'ampw.showHint': 'Amplificateur · cliquer pour afficher', 'ampw.hideHint': 'Amplificateur — affiché · cliquer pour masquer',
|
||||||
@@ -1037,7 +1037,7 @@ const fr: Dict = {
|
|||||||
'aud.preroll': 'Pré-enregistrement (secondes)', 'aud.format': 'Format de fichier', 'aud.wav': 'WAV (sans perte, plus volumineux)', 'aud.mp3': 'MP3 (compressé, léger)',
|
'aud.preroll': 'Pré-enregistrement (secondes)', 'aud.format': 'Format de fichier', 'aud.wav': 'WAV (sans perte, plus volumineux)', 'aud.mp3': 'MP3 (compressé, léger)',
|
||||||
'aud.fromLevel': 'Niveau depuis la radio', 'aud.txLevel': 'Niveau du voice keyer', 'aud.txLevelHint': "Niveau des messages enregistrés envoyés à la radio. Augmentez-le si votre voice keyer est bien plus faible que votre micro ; Lire fait entendre ce même niveau. Si la radio n'émet presque rien, c'est que sa source de modulation est restée le micro de façade : sur un FTDX10, réglez MENU → SSB MOD SOURCE sur REAR (l'entrée USB).", 'aud.micLevel': 'Niveau micro', 'aud.qsoPlayLevel': 'Niveau de relecture QSO', 'aud.levelHint': 'Si votre voix est plus forte que la station, baissez le niveau micro.',
|
'aud.fromLevel': 'Niveau depuis la radio', 'aud.txLevel': 'Niveau du voice keyer', 'aud.txLevelHint': "Niveau des messages enregistrés envoyés à la radio. Augmentez-le si votre voice keyer est bien plus faible que votre micro ; Lire fait entendre ce même niveau. Si la radio n'émet presque rien, c'est que sa source de modulation est restée le micro de façade : sur un FTDX10, réglez MENU → SSB MOD SOURCE sur REAR (l'entrée USB).", 'aud.micLevel': 'Niveau micro', 'aud.qsoPlayLevel': 'Niveau de relecture QSO', 'aud.levelHint': 'Si votre voix est plus forte que la station, baissez le niveau micro.',
|
||||||
'aud.autoSend': "Envoyer automatiquement l'enregistrement à la station par e-mail lorsque j'enregistre un QSO",
|
'aud.autoSend': "Envoyer automatiquement l'enregistrement à la station par e-mail lorsque j'enregistre un QSO",
|
||||||
'aud.dvkTitle': 'Messages du manipulateur vocal (F1–F6)', 'aud.pttMethod': 'Méthode PTT', 'aud.pttNone': 'Aucune (VOX)', 'aud.pttCat': 'CAT (la liaison radio)', 'aud.pttRts': 'RTS série', 'aud.pttDtr': 'DTR série',
|
'aud.dvkTitle': 'Messages du manipulateur vocal (F1–F12)', 'aud.deleteMsg': 'Supprimer ce message', 'aud.pttMethod': 'Méthode PTT', 'aud.pttNone': 'Aucune (VOX)', 'aud.pttCat': 'CAT (la liaison radio)', 'aud.pttRts': 'RTS série', 'aud.pttDtr': 'DTR série',
|
||||||
'aud.testPtt': 'Tester le PTT', 'aud.pttPort': 'Port COM du PTT', 'aud.pickPort': 'Choisir un port COM', 'aud.selectPort': '— choisir —', 'aud.refresh': 'Actualiser',
|
'aud.testPtt': 'Tester le PTT', 'aud.pttPort': 'Port COM du PTT', 'aud.pickPort': 'Choisir un port COM', 'aud.selectPort': '— choisir —', 'aud.refresh': 'Actualiser',
|
||||||
'aud.msgPlaceholder': 'Libellé du message {n} (CQ, report, 73…)', 'aud.holdRec': '● Maintenir pour enreg.', 'aud.recordingNow': '● Enregistrement…', 'aud.play': '▶ Lire', 'aud.stop': '■ Arrêter',
|
'aud.msgPlaceholder': 'Libellé du message {n} (CQ, report, 73…)', 'aud.holdRec': '● Maintenir pour enreg.', 'aud.recordingNow': '● Enregistrement…', 'aud.play': '▶ Lire', 'aud.stop': '■ Arrêter',
|
||||||
'aud.errPttTest': 'Test PTT : ', 'aud.errRecord': 'Enregistrement : ', 'aud.errSave': 'Sauvegarde : ', 'aud.errPlay': 'Lecture : ',
|
'aud.errPttTest': 'Test PTT : ', 'aud.errRecord': 'Enregistrement : ', 'aud.errSave': 'Sauvegarde : ', 'aud.errPlay': 'Lecture : ',
|
||||||
|
|||||||
@@ -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.27.2';
|
export const APP_VERSION = '0.27.3';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
Vendored
+2
@@ -160,6 +160,8 @@ export function CreateDatabase(arg1:string):Promise<void>;
|
|||||||
|
|
||||||
export function DVKCancelRecord():Promise<void>;
|
export function DVKCancelRecord():Promise<void>;
|
||||||
|
|
||||||
|
export function DVKDelete(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function DVKPlay(arg1:number):Promise<void>;
|
export function DVKPlay(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function DVKPreview(arg1:number):Promise<void>;
|
export function DVKPreview(arg1:number):Promise<void>;
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ export function DVKCancelRecord() {
|
|||||||
return window['go']['main']['App']['DVKCancelRecord']();
|
return window['go']['main']['App']['DVKCancelRecord']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DVKDelete(arg1) {
|
||||||
|
return window['go']['main']['App']['DVKDelete'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function DVKPlay(arg1) {
|
export function DVKPlay(arg1) {
|
||||||
return window['go']['main']['App']['DVKPlay'](arg1);
|
return window['go']['main']['App']['DVKPlay'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -82,6 +82,7 @@ type Client struct {
|
|||||||
mu sync.Mutex // serialises the connection: one question at a time
|
mu sync.Mutex // serialises the connection: one question at a time
|
||||||
conn io.ReadWriteCloser
|
conn io.ReadWriteCloser
|
||||||
rd *bufio.Reader
|
rd *bufio.Reader
|
||||||
|
skipTP bool // ^TP went unanswered once — a KPA500, no ATU; never ask again
|
||||||
|
|
||||||
statusMu sync.RWMutex
|
statusMu sync.RWMutex
|
||||||
status Status
|
status Status
|
||||||
@@ -183,6 +184,13 @@ func (c *Client) connectLocked() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot open %s: %w", c.cfg.ComPort, err)
|
return fmt.Errorf("cannot open %s: %w", c.cfg.ComPort, err)
|
||||||
}
|
}
|
||||||
|
// The KPA500 is POWER-CONTROLLED by these lines: the Elecraft utility
|
||||||
|
// switches the amplifier on by raising them. Held asserted, once, and
|
||||||
|
// never touched again — reconnect cycles that toggled them were
|
||||||
|
// switching a KPA500 OFF twenty seconds after its operator pressed
|
||||||
|
// nothing but Standby.
|
||||||
|
_ = p.SetDTR(true)
|
||||||
|
_ = p.SetRTS(true)
|
||||||
_ = p.SetReadTimeout(ioTimeout)
|
_ = p.SetReadTimeout(ioTimeout)
|
||||||
c.conn = p
|
c.conn = p
|
||||||
}
|
}
|
||||||
@@ -214,7 +222,13 @@ func (c *Client) ask(cmd string) (string, error) {
|
|||||||
// the frame.
|
// the frame.
|
||||||
line, err := c.rd.ReadString(';')
|
line, err := c.rd.ReadString(';')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.dropLocked()
|
// NOT dropped. A command this model simply does not know (^TP is the
|
||||||
|
// KPA1500's ATU — a KPA500 never answers it) is silence, not a dead
|
||||||
|
// link, and dropping here tore the connection down on every slow poll
|
||||||
|
// cycle: two seconds of stalled commands, a reconnect, and a DTR
|
||||||
|
// toggle the amplifier read as the off switch. Nothing arrived, so
|
||||||
|
// nothing is left to desynchronise the next exchange. Write errors —
|
||||||
|
// the genuinely dead link — still drop, above.
|
||||||
return "", fmt.Errorf("no answer to %s: %w", cmd, err)
|
return "", fmt.Errorf("no answer to %s: %w", cmd, err)
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(line), nil
|
return strings.TrimSpace(line), nil
|
||||||
@@ -364,12 +378,21 @@ func (c *Client) pollOnce(n uint64) {
|
|||||||
c.statusMu.Unlock()
|
c.statusMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.skipTP {
|
||||||
|
return
|
||||||
|
}
|
||||||
if reply, err := c.ask("^TP;"); err == nil {
|
if reply, err := c.ask("^TP;"); err == nil {
|
||||||
if v, err := parseInt(reply, "^TP"); err == nil {
|
if v, err := parseInt(reply, "^TP"); err == nil {
|
||||||
c.statusMu.Lock()
|
c.statusMu.Lock()
|
||||||
c.status.Tuning = v == 1
|
c.status.Tuning = v == 1
|
||||||
c.statusMu.Unlock()
|
c.statusMu.Unlock()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// One silence is the model's answer for good: a KPA500 has no ATU and
|
||||||
|
// will never answer ^TP — asking again every cycle cost a two-second
|
||||||
|
// stall each time.
|
||||||
|
c.skipTP = true
|
||||||
|
applog.Printf("kpa: ^TP unanswered — no ATU on this model, not asking again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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.27.2"
|
appVersion = "0.27.3"
|
||||||
|
|
||||||
// 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