feat: serial CW keyer — add invert (active-LOW) polarity option
Some CW interfaces key on the opposite line level (transmit at rest / key backwards). New "Invert keying (active-LOW)" toggle flips the asserted level for both the CW and PTT lines, and the idle state drives both lines to their inactive level accordingly so an active-low rig isn't left keyed at rest. Default off = N1MM convention (line HIGH = active).
This commit is contained in:
@@ -218,6 +218,7 @@ const (
|
|||||||
keyWKEscClears = "winkeyer.esc_clears_call" // ESC also clears the callsign
|
keyWKEscClears = "winkeyer.esc_clears_call" // ESC also clears the callsign
|
||||||
keyWKSendOnType = "winkeyer.send_on_type" // key characters live as typed
|
keyWKSendOnType = "winkeyer.send_on_type" // key characters live as typed
|
||||||
keyWKCWLine = "winkeyer.cw_key_line" // serial engine: "dtr" (CW) / "rts" (PTT) or swapped
|
keyWKCWLine = "winkeyer.cw_key_line" // serial engine: "dtr" (CW) / "rts" (PTT) or swapped
|
||||||
|
keyWKCWInvert = "winkeyer.cw_invert" // serial engine: invert line polarity (active-LOW)
|
||||||
|
|
||||||
keyClusterAutoConnect = "cluster.auto_connect" // open every enabled server at app start
|
keyClusterAutoConnect = "cluster.auto_connect" // open every enabled server at app start
|
||||||
|
|
||||||
@@ -13053,7 +13054,7 @@ func (a *App) GetWinkeyerSettings() (WinkeyerSettings, error) {
|
|||||||
keyWKEnabled, keyWKPort, keyWKBaud, keyWKWPM, keyWKWeight, keyWKLeadIn,
|
keyWKEnabled, keyWKPort, keyWKBaud, keyWKWPM, keyWKWeight, keyWKLeadIn,
|
||||||
keyWKTail, keyWKRatio, keyWKFarnsworth, keyWKSidetone, keyWKMode,
|
keyWKTail, keyWKRatio, keyWKFarnsworth, keyWKSidetone, keyWKMode,
|
||||||
keyWKSwap, keyWKAutoSpace, keyWKUsePTT, keyWKSerialEcho, keyWKMacros,
|
keyWKSwap, keyWKAutoSpace, keyWKUsePTT, keyWKSerialEcho, keyWKMacros,
|
||||||
keyWKEngine, keyWKEscClears, keyWKSendOnType, keyWKCWLine)
|
keyWKEngine, keyWKEscClears, keyWKSendOnType, keyWKCWLine, keyWKCWInvert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
@@ -13063,6 +13064,7 @@ func (a *App) GetWinkeyerSettings() (WinkeyerSettings, error) {
|
|||||||
if v := m[keyWKCWLine]; v != "" {
|
if v := m[keyWKCWLine]; v != "" {
|
||||||
out.CWKeyLine = v
|
out.CWKeyLine = v
|
||||||
}
|
}
|
||||||
|
out.CWInvert = m[keyWKCWInvert] == "1"
|
||||||
if v := m[keyWKEscClears]; v != "" {
|
if v := m[keyWKEscClears]; v != "" {
|
||||||
out.EscClearsCall = v == "1"
|
out.EscClearsCall = v == "1"
|
||||||
}
|
}
|
||||||
@@ -13130,6 +13132,7 @@ func (a *App) SaveWinkeyerSettings(s WinkeyerSettings) error {
|
|||||||
keyWKEscClears: boolStr(s.EscClearsCall),
|
keyWKEscClears: boolStr(s.EscClearsCall),
|
||||||
keyWKSendOnType: boolStr(s.SendOnType),
|
keyWKSendOnType: boolStr(s.SendOnType),
|
||||||
keyWKCWLine: strings.TrimSpace(s.CWKeyLine),
|
keyWKCWLine: strings.TrimSpace(s.CWKeyLine),
|
||||||
|
keyWKCWInvert: boolStr(s.CWInvert),
|
||||||
} {
|
} {
|
||||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+2
-2
@@ -3,13 +3,13 @@
|
|||||||
"version": "0.20.11",
|
"version": "0.20.11",
|
||||||
"date": "2026-07-23",
|
"date": "2026-07-23",
|
||||||
"en": [
|
"en": [
|
||||||
"New CW keyer engine: 'Serial port (DTR=CW / RTS=PTT)'. OpsLog can now key CW by toggling a serial control line itself — the hardware-keying method N1MM/WSJT use with a Yaesu SCU-17 and generic keying interfaces — without a K1EL WinKeyer. Settings → CW Keyer → Keyer engine: pick 'Serial port', choose the keying COM port and whether CW is on DTR (PTT on RTS) or swapped. Speed, Farnsworth, lead-in/tail and PTT keying all apply; macros, auto-CQ and <LOGQSO> work exactly as with the WinKeyer.",
|
"New CW keyer engine: 'Serial port (DTR=CW / RTS=PTT)'. OpsLog can now key CW by toggling a serial control line itself — the hardware-keying method N1MM/WSJT use with a Yaesu SCU-17 and generic keying interfaces — without a K1EL WinKeyer. Settings → CW Keyer → Keyer engine: pick 'Serial port', choose the keying COM port and whether CW is on DTR (PTT on RTS) or swapped. Speed, Farnsworth, lead-in/tail and PTT keying all apply; macros, auto-CQ and <LOGQSO> work exactly as with the WinKeyer. An 'Invert keying (active-LOW)' option is there for interfaces that key backwards / transmit at rest.",
|
||||||
"Fixed the spectrum scope never displaying on the IC-7300: its CI-V waveform frames actually carry the same leading main-scope selector byte as the dual-scope IC-7610/9700, but OpsLog assumed the 7300 omitted it — so it read the frame sequence number from the wrong byte (always 0), discarded every frame, and drew nothing. The frame layout is now detected from the data itself, so the 7300 (and other single-scope Icoms) render correctly, in both center and fixed modes. The IC-7610/9700 are unaffected.",
|
"Fixed the spectrum scope never displaying on the IC-7300: its CI-V waveform frames actually carry the same leading main-scope selector byte as the dual-scope IC-7610/9700, but OpsLog assumed the 7300 omitted it — so it read the frame sequence number from the wrong byte (always 0), discarded every frame, and drew nothing. The frame layout is now detected from the data itself, so the 7300 (and other single-scope Icoms) render correctly, in both center and fixed modes. The IC-7610/9700 are unaffected.",
|
||||||
"IC-7300 scope: the center/fixed mode and edge-frequency commands now include the selector byte the rig requires, so switching the scope between center-on-VFO and fixed span from OpsLog is accepted instead of being rejected.",
|
"IC-7300 scope: the center/fixed mode and edge-frequency commands now include the selector byte the rig requires, so switching the scope between center-on-VFO and fixed span from OpsLog is accepted instead of being rejected.",
|
||||||
"Closing OpsLog no longer switches off the scope display on the radio itself: turning the scope off used to send both 'stop CI-V data' and 'display off', so quitting blanked a local IC-7300's own scope screen. It now only stops the CI-V data stream on disable and never touches the radio's display (the display-on command is sent solely when enabling)."
|
"Closing OpsLog no longer switches off the scope display on the radio itself: turning the scope off used to send both 'stop CI-V data' and 'display off', so quitting blanked a local IC-7300's own scope screen. It now only stops the CI-V data stream on disable and never touches the radio's display (the display-on command is sent solely when enabling)."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Nouveau moteur de keyer CW : « Port série (DTR=CW / RTS=PTT) ». OpsLog peut désormais manipuler la CW en basculant lui-même une ligne de contrôle série — la méthode de keying matériel qu'utilisent N1MM/WSJT avec un Yaesu SCU-17 et les interfaces génériques — sans WinKeyer K1EL. Réglages → Keyer CW → Moteur : choisis « Port série », le port COM de keying et si la CW est sur DTR (PTT sur RTS) ou l'inverse. Vitesse, Farnsworth, lead-in/tail et PTT s'appliquent ; les macros, l'auto-CQ et <LOGQSO> fonctionnent exactement comme avec le WinKeyer.",
|
"Nouveau moteur de keyer CW : « Port série (DTR=CW / RTS=PTT) ». OpsLog peut désormais manipuler la CW en basculant lui-même une ligne de contrôle série — la méthode de keying matériel qu'utilisent N1MM/WSJT avec un Yaesu SCU-17 et les interfaces génériques — sans WinKeyer K1EL. Réglages → Keyer CW → Moteur : choisis « Port série », le port COM de keying et si la CW est sur DTR (PTT sur RTS) ou l'inverse. Vitesse, Farnsworth, lead-in/tail et PTT s'appliquent ; les macros, l'auto-CQ et <LOGQSO> fonctionnent exactement comme avec le WinKeyer. Une option « Inverser le keying (actif-BAS) » est prévue pour les interfaces qui manipulent à l'envers / émettent au repos.",
|
||||||
"Correction du scope spectral qui ne s'affichait jamais sur l'IC-7300 : ses trames de forme d'onde CI-V portent en réalité le même octet sélecteur de scope en tête que les IC-7610/9700 (dual-scope), mais OpsLog supposait que le 7300 ne l'envoyait pas — il lisait donc le numéro de séquence de la trame sur le mauvais octet (toujours 0), jetait toutes les trames et ne dessinait rien. La disposition des trames est maintenant détectée à partir des données elles-mêmes, donc le 7300 (et les autres Icom mono-scope) s'affichent correctement, en mode centre comme en fixe. Les IC-7610/9700 ne sont pas affectés.",
|
"Correction du scope spectral qui ne s'affichait jamais sur l'IC-7300 : ses trames de forme d'onde CI-V portent en réalité le même octet sélecteur de scope en tête que les IC-7610/9700 (dual-scope), mais OpsLog supposait que le 7300 ne l'envoyait pas — il lisait donc le numéro de séquence de la trame sur le mauvais octet (toujours 0), jetait toutes les trames et ne dessinait rien. La disposition des trames est maintenant détectée à partir des données elles-mêmes, donc le 7300 (et les autres Icom mono-scope) s'affichent correctement, en mode centre comme en fixe. Les IC-7610/9700 ne sont pas affectés.",
|
||||||
"Scope IC-7300 : les commandes de mode centre/fixe et de fréquences de bords incluent maintenant l'octet sélecteur requis par la radio, donc basculer le scope entre centre-sur-VFO et span fixe depuis OpsLog est accepté au lieu d'être rejeté.",
|
"Scope IC-7300 : les commandes de mode centre/fixe et de fréquences de bords incluent maintenant l'octet sélecteur requis par la radio, donc basculer le scope entre centre-sur-VFO et span fixe depuis OpsLog est accepté au lieu d'être rejeté.",
|
||||||
"Fermer OpsLog n'éteint plus le scope sur la radio elle-même : couper le scope envoyait à la fois « stop données CI-V » et « affichage OFF », donc quitter éteignait l'écran scope d'un IC-7300 local. Désormais seule la diffusion des données CI-V est coupée à la désactivation, l'affichage de la radio n'est jamais touché (la commande d'allumage n'est envoyée qu'à l'activation)."
|
"Fermer OpsLog n'éteint plus le scope sur la radio elle-même : couper le scope envoyait à la fois « stop données CI-V » et « affichage OFF », donc quitter éteignait l'écran scope d'un IC-7300 local. Désormais seule la diffusion des données CI-V est coupée à la désactivation, l'affichage de la radio n'est jamais touché (la commande d'allumage n'est envoyée qu'à l'activation)."
|
||||||
|
|||||||
@@ -1074,13 +1074,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
port: string; baud: number; wpm: number; weight: number;
|
port: string; baud: number; wpm: number; weight: number;
|
||||||
lead_in_ms: number; tail_ms: number; ratio: number; farnsworth: number;
|
lead_in_ms: number; tail_ms: number; ratio: number; farnsworth: number;
|
||||||
sidetone_hz: number; mode: string; swap: boolean; autospace: boolean;
|
sidetone_hz: number; mode: string; swap: boolean; autospace: boolean;
|
||||||
use_ptt: boolean; serial_echo: boolean; cw_key_line: string; macros: WKMac[];
|
use_ptt: boolean; serial_echo: boolean; cw_key_line: string; cw_invert: boolean; macros: WKMac[];
|
||||||
};
|
};
|
||||||
const [wk, setWk] = useState<WKSettings>({
|
const [wk, setWk] = useState<WKSettings>({
|
||||||
enabled: false, engine: 'winkeyer', esc_clears_call: true,
|
enabled: false, engine: 'winkeyer', esc_clears_call: true,
|
||||||
port: '', baud: 1200, wpm: 25, weight: 50, lead_in_ms: 10,
|
port: '', baud: 1200, wpm: 25, weight: 50, lead_in_ms: 10,
|
||||||
tail_ms: 50, ratio: 50, farnsworth: 0, sidetone_hz: 600, mode: 'iambic_b',
|
tail_ms: 50, ratio: 50, farnsworth: 0, sidetone_hz: 600, mode: 'iambic_b',
|
||||||
swap: false, autospace: true, use_ptt: false, serial_echo: true, cw_key_line: 'dtr', macros: [],
|
swap: false, autospace: true, use_ptt: false, serial_echo: true, cw_key_line: 'dtr', cw_invert: false, macros: [],
|
||||||
});
|
});
|
||||||
const [wkPorts, setWkPorts] = useState<string[]>([]);
|
const [wkPorts, setWkPorts] = useState<string[]>([]);
|
||||||
const setWkField = (patch: Partial<WKSettings>) => setWk((s) => ({ ...s, ...patch }));
|
const setWkField = (patch: Partial<WKSettings>) => setWk((s) => ({ ...s, ...patch }));
|
||||||
@@ -3131,6 +3131,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> Key PTT line
|
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> Key PTT line
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<Checkbox checked={wk.cw_invert} onCheckedChange={(c) => setWkField({ cw_invert: !!c })} />
|
||||||
|
Invert keying (active-LOW) — tick this only if the rig transmits at rest / keys backwards
|
||||||
|
</label>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ var morseTable = map[rune]string{
|
|||||||
type serialKeyer struct {
|
type serialKeyer struct {
|
||||||
port serial.Port
|
port serial.Port
|
||||||
keyDTR bool // true: CW on DTR, PTT on RTS (N1MM default). false: swapped.
|
keyDTR bool // true: CW on DTR, PTT on RTS (N1MM default). false: swapped.
|
||||||
|
invert bool // true: active-LOW — asserting a line means driving it LOW
|
||||||
usePTT bool
|
usePTT bool
|
||||||
leadMs int
|
leadMs int
|
||||||
tailMs int
|
tailMs int
|
||||||
@@ -58,6 +59,7 @@ func newSerialKeyer(port serial.Port, cfg Config, onBusy func(bool)) *serialKeye
|
|||||||
k := &serialKeyer{
|
k := &serialKeyer{
|
||||||
port: port,
|
port: port,
|
||||||
keyDTR: !strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts"), // default DTR=CW
|
keyDTR: !strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts"), // default DTR=CW
|
||||||
|
invert: cfg.CWInvert,
|
||||||
usePTT: cfg.UsePTT,
|
usePTT: cfg.UsePTT,
|
||||||
leadMs: cfg.LeadInMs,
|
leadMs: cfg.LeadInMs,
|
||||||
tailMs: cfg.TailMs,
|
tailMs: cfg.TailMs,
|
||||||
@@ -69,17 +71,21 @@ func newSerialKeyer(port serial.Port, cfg Config, onBusy func(bool)) *serialKeye
|
|||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
k.allLow() // start idle: key up, PTT off
|
k.idle() // start inactive: key up, PTT off
|
||||||
go k.run()
|
go k.run()
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// level maps a logical "active" state to the physical line level, honouring the
|
||||||
|
// invert (active-LOW) option: normally active = HIGH, inverted active = LOW.
|
||||||
|
func (k *serialKeyer) level(active bool) bool { return active != k.invert }
|
||||||
|
|
||||||
// setKey raises/lowers the CW key line; setPTT the PTT line (only when enabled).
|
// setKey raises/lowers the CW key line; setPTT the PTT line (only when enabled).
|
||||||
func (k *serialKeyer) setKey(down bool) {
|
func (k *serialKeyer) setKey(down bool) {
|
||||||
if k.keyDTR {
|
if k.keyDTR {
|
||||||
_ = k.port.SetDTR(down)
|
_ = k.port.SetDTR(k.level(down))
|
||||||
} else {
|
} else {
|
||||||
_ = k.port.SetRTS(down)
|
_ = k.port.SetRTS(k.level(down))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (k *serialKeyer) setPTT(on bool) {
|
func (k *serialKeyer) setPTT(on bool) {
|
||||||
@@ -87,12 +93,18 @@ func (k *serialKeyer) setPTT(on bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if k.keyDTR {
|
if k.keyDTR {
|
||||||
_ = k.port.SetRTS(on)
|
_ = k.port.SetRTS(k.level(on))
|
||||||
} else {
|
} else {
|
||||||
_ = k.port.SetDTR(on)
|
_ = k.port.SetDTR(k.level(on))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (k *serialKeyer) allLow() { _ = k.port.SetDTR(false); _ = k.port.SetRTS(false) }
|
|
||||||
|
// idle drives BOTH lines to their inactive level (key up, PTT off), respecting
|
||||||
|
// the invert option — so an active-LOW interface isn't left keyed at rest.
|
||||||
|
func (k *serialKeyer) idle() {
|
||||||
|
_ = k.port.SetDTR(k.level(false))
|
||||||
|
_ = k.port.SetRTS(k.level(false))
|
||||||
|
}
|
||||||
|
|
||||||
// SetSpeed / Send / Stop mirror the WinKeyer manager's control surface.
|
// SetSpeed / Send / Stop mirror the WinKeyer manager's control surface.
|
||||||
func (k *serialKeyer) SetSpeed(wpm int) {
|
func (k *serialKeyer) SetSpeed(wpm int) {
|
||||||
@@ -131,7 +143,7 @@ func (k *serialKeyer) Close() {
|
|||||||
|
|
||||||
func (k *serialKeyer) run() {
|
func (k *serialKeyer) run() {
|
||||||
defer close(k.done)
|
defer close(k.done)
|
||||||
defer k.allLow()
|
defer k.idle()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-k.stop:
|
case <-k.stop:
|
||||||
@@ -149,7 +161,7 @@ func (k *serialKeyer) run() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-k.stop:
|
case <-k.stop:
|
||||||
k.allLow()
|
k.idle()
|
||||||
k.onBusy(false)
|
k.onBusy(false)
|
||||||
return
|
return
|
||||||
case s2 := <-k.in:
|
case s2 := <-k.in:
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type Config struct {
|
|||||||
// apply; the paddle/sidetone/ratio fields are WinKeyer-only.
|
// apply; the paddle/sidetone/ratio fields are WinKeyer-only.
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
CWKeyLine string `json:"cw_key_line"` // serial: "dtr" (CW on DTR, PTT on RTS — default) | "rts"
|
CWKeyLine string `json:"cw_key_line"` // serial: "dtr" (CW on DTR, PTT on RTS — default) | "rts"
|
||||||
|
CWInvert bool `json:"cw_invert"` // serial: invert line polarity (active-LOW) for both key and PTT
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) normalised() Config {
|
func (c Config) normalised() Config {
|
||||||
|
|||||||
Reference in New Issue
Block a user