feat(icom): PTT and offset split at the operator's hand

The MOX and SPLIT controls lived at the bottom of the Transmit card, below
the fold on most layouts — a console whose PTT needs scrolling is not a
console. A strip under the mode row now carries the PTT and Split as
OFF/+1k/+5k/+10k, DXpedition style, on a new SetIcomSplitOffset that takes
the chosen TX offset (0 keeps the old CW/SSB convention). The mode-alignment
from the previous fix rides along on every engage.
This commit is contained in:
2026-08-30 02:15:16 +02:00
parent 392b93f089
commit f98e95fe9e
7 changed files with 59 additions and 9 deletions
+9
View File
@@ -14533,6 +14533,15 @@ func (a *App) IcomSetSplit(on bool) error {
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetIcomSplit(on) }) return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetIcomSplit(on) })
} }
// IcomSetSplitOffset turns split on with a chosen TX offset (Hz) — the
// console's +1/+5/+10 kHz buttons.
func (a *App) IcomSetSplitOffset(hz int64) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetIcomSplitOffset(true, hz) })
}
func (a *App) IcomSetAntenna(n int) error { func (a *App) IcomSetAntenna(n int) error {
if a.cat == nil { if a.cat == nil {
return fmt.Errorf("cat not initialized") return fmt.Errorf("cat not initialized")
+4 -2
View File
@@ -28,7 +28,8 @@
"Icom network audio: “Talk to radio” now works over the LAN too — live microphone straight to the rig, PTT included. With RX audio and a headset, OpsLog is a complete remote station: hear, talk, key CW and log over one network link.", "Icom network audio: “Talk to radio” now works over the LAN too — live microphone straight to the rig, PTT included. With RX audio and a headset, OpsLog is a complete remote station: hear, talk, key CW and log over one network link.",
"Icom console: the SUB display shows the sub receivers frequency at all times (it was blank until split), and engaging split copies the mode to the TX VFO so it matches the main.", "Icom console: the SUB display shows the sub receivers frequency at all times (it was blank until split), and engaging split copies the mode to the TX VFO so it matches the main.",
"Icom network audio: the stream now opens regardless of the speaker choice — speakers-off (or a failed output device) was silently disabling the whole stream: no audio session, no recordings, no voice keyer, despite the RX-audio option being ticked.", "Icom network audio: the stream now opens regardless of the speaker choice — speakers-off (or a failed output device) was silently disabling the whole stream: no audio session, no recordings, no voice keyer, despite the RX-audio option being ticked.",
"Icom network: a radio in standby keeps its session and the consoles ON button — the quiet-CI-V recovery was tearing the session down every 15 s, so a radio that was off when OpsLog started could never be powered on." "Icom network: a radio in standby keeps its session and the consoles ON button — the quiet-CI-V recovery was tearing the session down every 15 s, so a radio that was off when OpsLog started could never be powered on.",
"Icom console: PTT and Split move up under the dial — one PTT button, and Split as OFF/+1k/+5k/+10k, DXpedition style. Engaging split also aligns the TX VFOs mode with the main."
], ],
"fr": [ "fr": [
"Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à laffichage de la radio (il lisait environ deux points S trop bas).", "Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à laffichage de la radio (il lisait environ deux points S trop bas).",
@@ -56,7 +57,8 @@
"Audio réseau Icom : « Talk to radio » fonctionne aussi par le LAN — micro en direct vers la radio, PTT compris. Avec le RX audio et un casque, OpsLog devient une station remote complète : écouter, parler, manipuler la CW et loguer sur un seul lien réseau.", "Audio réseau Icom : « Talk to radio » fonctionne aussi par le LAN — micro en direct vers la radio, PTT compris. Avec le RX audio et un casque, OpsLog devient une station remote complète : écouter, parler, manipuler la CW et loguer sur un seul lien réseau.",
"Console Icom : laffichage SUB montre la fréquence du sub receiver en permanence (il restait vide hors split), et activer le split copie le mode sur le VFO TX pour quil suive le main.", "Console Icom : laffichage SUB montre la fréquence du sub receiver en permanence (il restait vide hors split), et activer le split copie le mode sur le VFO TX pour quil suive le main.",
"Audio réseau Icom : le flux souvre désormais indépendamment du choix d’écoute — enceintes coupées (ou périphérique de sortie en échec) désactivait silencieusement tout le flux : pas de session audio, ni enregistrements, ni voice keyer, malgré la case RX audio cochée.", "Audio réseau Icom : le flux souvre désormais indépendamment du choix d’écoute — enceintes coupées (ou périphérique de sortie en échec) désactivait silencieusement tout le flux : pas de session audio, ni enregistrements, ni voice keyer, malgré la case RX audio cochée.",
"Réseau Icom : une radio en veille garde sa session et le bouton ON de la console — la récupération du CI-V muet détruisait la session toutes les 15 s, donc une radio éteinte au lancement dOpsLog ne pouvait jamais être allumée." "Réseau Icom : une radio en veille garde sa session et le bouton ON de la console — la récupération du CI-V muet détruisait la session toutes les 15 s, donc une radio éteinte au lancement dOpsLog ne pouvait jamais être allumée.",
"Console Icom : le PTT et le Split remontent sous le cadran — un bouton PTT, et le Split en OFF/+1k/+5k/+10k, façon DXpedition. Activer le split aligne aussi le mode du VFO TX sur le main."
] ]
}, },
{ {
+24 -1
View File
@@ -5,7 +5,7 @@ import {
IcomSetAFGain, IcomSetRFGain, IcomSetNB, IcomSetNBLevel, IcomSetNR, IcomSetNRLevel, IcomSetAFGain, IcomSetRFGain, IcomSetNB, IcomSetNBLevel, IcomSetNR, IcomSetNRLevel,
IcomSetANF, IcomSetAPF, IcomSetAGC, IcomSetPreamp, IcomSetAtt, IcomSetFilter, IcomSetANF, IcomSetAPF, IcomSetAGC, IcomSetPreamp, IcomSetAtt, IcomSetFilter,
AudioMonitorActive, AudioStartMonitor, AudioStopMonitor, AudioMonitorActive, AudioStartMonitor, AudioStopMonitor,
IcomSetRFPower, IcomSetMicGain, IcomSetSplit, IcomTune, IcomSetPTT, IcomSetRFPower, IcomSetMicGain, IcomSetSplit, IcomSetSplitOffset, IcomTune, IcomSetPTT,
IcomSetScope, IcomScopeData, IcomSetScopeMode, IcomSetScopeEdges, GetCATState, SetCATFrequency, SetCATMode, IcomSetScope, IcomScopeData, IcomSetScopeMode, IcomSetScopeEdges, GetCATState, SetCATFrequency, SetCATMode,
IcomSetRIT, IcomSetRITOn, IcomSetXITOn, IcomSetRIT, IcomSetRITOn, IcomSetXITOn,
IcomSetAntenna, IcomSetPBTInner, IcomSetPBTOuter, IcomSetManualNotch, IcomSetNotchPos, IcomSetAntenna, IcomSetPBTInner, IcomSetPBTOuter, IcomSetManualNotch, IcomSetNotchPos,
@@ -786,6 +786,29 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
); );
})} })}
</div> </div>
{/* PTT + split, at the operator's hand under the dial — the Transmit
card lower down kept these below the fold, and a console whose PTT
needs scrolling is not a console. */}
<div className="flex items-center gap-1.5 border-t border-border/60 px-2 py-1.5">
<button type="button" onClick={toggleMox}
className={cn('flex-1 px-3 py-1.5 rounded-md text-xs font-black tracking-wider border transition-colors',
tx ? 'bg-destructive border-destructive text-destructive-foreground animate-pulse' : 'bg-card text-foreground border-border hover:bg-muted')}>
{tx ? 'TX ON' : 'PTT'}
</button>
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground pl-1">Split</span>
<button type="button" onClick={() => set({ split: false }, () => IcomSetSplit(false))}
className={cn('px-2 py-1.5 rounded-md text-[11px] font-bold border transition-colors',
!st.split ? 'bg-primary text-primary-foreground border-primary' : 'bg-card text-muted-foreground border-border hover:bg-muted')}>
OFF
</button>
{[1, 5, 10].map((k) => (
<button key={k} type="button" onClick={() => set({ split: true }, () => IcomSetSplitOffset(k * 1000))}
className={cn('px-2 py-1.5 rounded-md text-[11px] font-bold border transition-colors',
st.split ? 'bg-card text-foreground border-border hover:bg-muted' : 'bg-card text-muted-foreground border-border hover:bg-muted')}>
+{k}k
</button>
))}
</div>
</div> </div>
{/* Live meters — always visible: S (RX, click → RST), Po in watts, SWR. */} {/* Live meters — always visible: S (RX, click → RST), Po in watts, SWR. */}
+2
View File
@@ -704,6 +704,8 @@ export function IcomSetScopeMode(arg1:boolean):Promise<void>;
export function IcomSetSplit(arg1:boolean):Promise<void>; export function IcomSetSplit(arg1:boolean):Promise<void>;
export function IcomSetSplitOffset(arg1:number):Promise<void>;
export function IcomSetSquelch(arg1:number):Promise<void>; export function IcomSetSquelch(arg1:number):Promise<void>;
export function IcomSetVOX(arg1:boolean):Promise<void>; export function IcomSetVOX(arg1:boolean):Promise<void>;
+4
View File
@@ -1346,6 +1346,10 @@ export function IcomSetSplit(arg1) {
return window['go']['main']['App']['IcomSetSplit'](arg1); return window['go']['main']['App']['IcomSetSplit'](arg1);
} }
export function IcomSetSplitOffset(arg1) {
return window['go']['main']['App']['IcomSetSplitOffset'](arg1);
}
export function IcomSetSquelch(arg1) { export function IcomSetSquelch(arg1) {
return window['go']['main']['App']['IcomSetSquelch'](arg1); return window['go']['main']['App']['IcomSetSquelch'](arg1);
} }
+1
View File
@@ -640,6 +640,7 @@ type IcomController interface {
SetRFPower(int) error SetRFPower(int) error
SetMicGain(int) error SetMicGain(int) error
SetIcomSplit(bool) error SetIcomSplit(bool) error
SetIcomSplitOffset(bool, int64) error
TuneATU() error TuneATU() error
SetScope(bool) error // enable/disable the spectrum-scope waveform stream SetScope(bool) error // enable/disable the spectrum-scope waveform stream
SetScopeMode(bool) error // true = fixed span, false = center-on-VFO SetScopeMode(bool) error // true = fixed span, false = center-on-VFO
+13 -4
View File
@@ -1860,10 +1860,16 @@ func (b *IcomSerial) SetMicGain(p int) error {
} }
func (b *IcomSerial) SetIcomSplit(on bool) error { func (b *IcomSerial) SetIcomSplit(on bool) error {
return b.SetIcomSplitOffset(on, 0)
}
// SetIcomSplitOffset enables split with a CHOSEN TX offset in Hz (0 = the
// usual convention: +1 kHz on CW, +5 kHz otherwise). The console offers
// +1/+5/+10 directly, DXpedition style.
func (b *IcomSerial) SetIcomSplitOffset(on bool, offsetHz int64) error {
if on { if on {
// Enable split with the usual "work him up" TX offset: +1 kHz on CW, // Set the unselected (TX) VFO to RX+offset first, then turn split on.
// +5 kHz otherwise (SSB). Set the unselected (TX) VFO to RX+offset first, // 0x25 0x01 + BCD sets the unselected VFO's frequency.
// then turn split on. 0x25 0x01 + BCD sets the unselected VFO's frequency.
rx := b.curFreq rx := b.curFreq
if rx <= 0 { if rx <= 0 {
if hz, err := b.readFreq(); err == nil { if hz, err := b.readFreq(); err == nil {
@@ -1871,10 +1877,13 @@ func (b *IcomSerial) SetIcomSplit(on bool) error {
} }
} }
if rx > 0 { if rx > 0 {
offset := int64(5000) offset := offsetHz
if offset <= 0 {
offset = 5000
if b.curModeByte == civ.ModeCW || b.curModeByte == civ.ModeCWR { if b.curModeByte == civ.ModeCW || b.curModeByte == civ.ModeCWR {
offset = 1000 offset = 1000
} }
}
_ = b.exec(append([]byte{civ.CmdVfoFreq, civ.SubVfoUnselected}, civ.FreqToBCD(rx+offset)...)...) _ = b.exec(append([]byte{civ.CmdVfoFreq, civ.SubVfoUnselected}, civ.FreqToBCD(rx+offset)...)...)
} }
// And the MODE crosses with it: a split where the TX VFO is still on // And the MODE crosses with it: a split where the TX VFO is still on