feat(cat): three transports for Kenwood/Elecraft, chosen not inferred

USB, RS-232-to-Ethernet, and the radio's own network protocol are three
different things, so they are three entries in one dropdown — and the
transport is now a stored setting rather than something deduced from
which field happened to be filled in. The old rule (a host wins whenever
it is not empty) is invisible from the settings page: someone who typed a
host months ago and later set a COM port had a radio that never answered
and nothing on screen to say why.

The third entry is named and refused, with the reason. It is a session
with its own framing and login, not the CAT byte stream over a socket, so
it has to be written per radio and against one — and a K4 owner reading
this list should be told that, not left wondering whether the empty
address field was the problem.

The upgrade reads an older install from what it has: a configured host
means the bridge, which is what the previous code used it for. Pinned by
a test, including the case that matters after the fact — choosing USB
with a stale host still stored gives the COM port.
This commit is contained in:
2026-08-25 17:46:14 +02:00
parent 2f2fced15c
commit bd540ef18b
7 changed files with 129 additions and 44 deletions
+41 -26
View File
@@ -132,6 +132,13 @@ const (
keyCATYaesuPort = "cat.yaesu.port" // Yaesu CAT serial port (e.g. COM4) keyCATYaesuPort = "cat.yaesu.port" // Yaesu CAT serial port (e.g. COM4)
keyCATYaesuBaud = "cat.yaesu.baud" // Yaesu CAT baud (FTDX10/101 default 38400) keyCATYaesuBaud = "cat.yaesu.baud" // Yaesu CAT baud (FTDX10/101 default 38400)
keyCATKenwoodHost = "cat.kenwood.host" // Kenwood CAT over a network serial bridge (ser2net), "host:port" keyCATKenwoodHost = "cat.kenwood.host" // Kenwood CAT over a network serial bridge (ser2net), "host:port"
// keyCATKenwoodLink says HOW the radio is reached: "usb" (a COM port),
// "bridge" (the same CAT bytes over TCP — ser2net, an Ethernet-serial
// adapter), or "native" (the radio's own network protocol). Kept apart from
// the address because the three are different transports, and inferring the
// choice from whether a field happened to be filled in is how an operator
// ends up with a host typed in and a radio that never answers.
keyCATKenwoodLink = "cat.kenwood.link"
keyCATKenwoodPort = "cat.kenwood.port" // Kenwood CAT serial port (TS-590/890/2000, Elecraft) keyCATKenwoodPort = "cat.kenwood.port" // Kenwood CAT serial port (TS-590/890/2000, Elecraft)
keyCATKenwoodBaud = "cat.kenwood.baud" // Kenwood CAT baud (TS-590 default 9600, TS-890 115200) keyCATKenwoodBaud = "cat.kenwood.baud" // Kenwood CAT baud (TS-590 default 9600, TS-890 115200)
// One key PER BACKEND, deliberately not shared. A Xiegu fix that reached // One key PER BACKEND, deliberately not shared. A Xiegu fix that reached
@@ -457,6 +464,9 @@ type CATSettings struct {
KenwoodHost string `json:"kenwood_host"` // "host:port" of a serial-over-network bridge (ser2net, Ethernet-serial KenwoodHost string `json:"kenwood_host"` // "host:port" of a serial-over-network bridge (ser2net, Ethernet-serial
// adapter). NOT the radios own RJ45, which speaks Kenwoods KNS/ARCP. // adapter). NOT the radios own RJ45, which speaks Kenwoods KNS/ARCP.
KenwoodPort string `json:"kenwood_port"` // Kenwood CAT serial port (TS-590/890/2000, Elecraft) KenwoodPort string `json:"kenwood_port"` // Kenwood CAT serial port (TS-590/890/2000, Elecraft)
// KenwoodLink: "usb" | "bridge" | "native". Empty means usb, unless a host
// was already configured by an older build — see GetCATSettings.
KenwoodLink string `json:"kenwood_link"`
KenwoodBaud int `json:"kenwood_baud"` // Kenwood CAT baud (TS-590 default 9600) KenwoodBaud int `json:"kenwood_baud"` // Kenwood CAT baud (TS-590 default 9600)
// What a DATA/digital mode (FT8, PSK…) sets on the rig: "usb" (default), "data" // What a DATA/digital mode (FT8, PSK…) sets on the rig: "usb" (default), "data"
// (MD6 — Elecraft K3/K4 DATA mode) or "keep" (leave the rig's mode untouched, // (MD6 — Elecraft K3/K4 DATA mode) or "keep" (leave the rig's mode untouched,
@@ -8044,7 +8054,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if a.settings == nil { if a.settings == nil {
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized") return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
} }
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDVKDax, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATKenwoodDataMode, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPttHotkeyEnabled, keyCATPttHotkey, keyCATPttHotkeyToggle, keyCATPollMs, keyCATDelayMs, keyCATOffsetOn, keyCATOffsetHz, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort, keyCATShareProto, keyCATShareTCIPort) m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDVKDax, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATKenwoodLink, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATKenwoodDataMode, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPttHotkeyEnabled, keyCATPttHotkey, keyCATPttHotkeyToggle, keyCATPollMs, keyCATDelayMs, keyCATOffsetOn, keyCATOffsetHz, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort, keyCATShareProto, keyCATShareTCIPort)
if err != nil { if err != nil {
return CATSettings{}, err return CATSettings{}, err
} }
@@ -8065,6 +8075,9 @@ func (a *App) GetCATSettings() (CATSettings, error) {
YaesuBaud: 38400, YaesuBaud: 38400,
KenwoodPort: m[keyCATKenwoodPort], KenwoodPort: m[keyCATKenwoodPort],
KenwoodHost: m[keyCATKenwoodHost], KenwoodHost: m[keyCATKenwoodHost],
// An install that predates the setting is read from what it has: a host
// filled in meant the bridge, since that is what the old code preferred.
KenwoodLink: kenwoodLinkOr(m[keyCATKenwoodLink], m[keyCATKenwoodHost]),
KenwoodBaud: 9600, KenwoodBaud: 9600,
YaesuLowLines: m[keyCATYaesuLowLines] == "1", YaesuLowLines: m[keyCATYaesuLowLines] == "1",
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1", KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
@@ -8264,6 +8277,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
keyCATYaesuBaud: strconv.Itoa(s.YaesuBaud), keyCATYaesuBaud: strconv.Itoa(s.YaesuBaud),
keyCATKenwoodPort: strings.TrimSpace(s.KenwoodPort), keyCATKenwoodPort: strings.TrimSpace(s.KenwoodPort),
keyCATKenwoodHost: strings.TrimSpace(s.KenwoodHost), keyCATKenwoodHost: strings.TrimSpace(s.KenwoodHost),
keyCATKenwoodLink: kenwoodLinkOr(s.KenwoodLink, s.KenwoodHost),
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud), keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
keyCATYaesuLowLines: b01(s.YaesuLowLines), keyCATYaesuLowLines: b01(s.YaesuLowLines),
keyCATKenwoodLowLines: b01(s.KenwoodLowLines), keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
@@ -15199,37 +15213,38 @@ func (a *App) reloadCAT() {
yz := cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault) yz := cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault)
yz.SetLowerLines(s.YaesuLowLines) yz.SetLowerLines(s.YaesuLowLines)
a.cat.Start(yz) a.cat.Start(yz)
case "kenwood": case "kenwood", "elecraft":
// Native Kenwood CAT — TS-590/890/990/2000 and everything that speaks the // Native Kenwood CAT — TS-590/890/990/2000 and everything that speaks the
// same dialect (Elecraft K3/K4, and the "Kenwood" setting on other rigs). // same dialect, the Elecraft K3/K4 included: the K3 emulates the Kenwood
// One IF; frame carries frequency, mode, VFO and split, so the poll costs a // command set, so one transport serves both and the Elecraft flag only
// single round trip where OmniRig needed a rig file to describe each one. // turns on its specifics (digital modes → DATA A via MD6+DT0).
// A network address wins over the COM port when both are filled: it is the elecraft := s.Backend == "elecraft"
// more deliberate setting, and silently preferring the wire would leave an switch kenwoodLinkOr(s.KenwoodLink, s.KenwoodHost) {
// operator staring at a host they typed and a radio that never answers. case kenwoodLinkNative:
if h := strings.TrimSpace(s.KenwoodHost); h != "" { // The radio's OWN network protocol — a session, its own framing, its
kw := cat.NewKenwoodTCP(h, s.DigitalDefault) // own authentication. Nothing here speaks it: it is not the CAT byte
// stream with a socket in front, and pretending otherwise would open a
// connection that answers nothing and blame the radio for it.
applog.Printf("cat: %s over the radio's own network protocol is not implemented — use USB, or an RS-232-to-Ethernet bridge", s.Backend)
a.cat.Stop()
// Said on screen as well as in the log: a CAT panel that simply
// stays disconnected sends the operator hunting a cable.
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "cat:state", cat.RigState{
Error: "this radio's own network protocol is not supported yet — use USB, or an RS-232-to-Ethernet bridge",
})
}
return
case kenwoodLinkBridge:
kw := cat.NewKenwoodTCP(strings.TrimSpace(s.KenwoodHost), s.DigitalDefault)
kw.SetDataMode(s.KenwoodDataMode) kw.SetDataMode(s.KenwoodDataMode)
kw.SetElecraft(elecraft)
a.cat.Start(kw) a.cat.Start(kw)
} else { default:
kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault) kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault)
kw.SetLowerLines(s.KenwoodLowLines) kw.SetLowerLines(s.KenwoodLowLines)
kw.SetDataMode(s.KenwoodDataMode) kw.SetDataMode(s.KenwoodDataMode)
a.cat.Start(kw) kw.SetElecraft(elecraft)
}
case "elecraft":
// Elecraft K3/K4: the Kenwood-dialect client with the Elecraft specifics on
// (digital modes → DATA A via MD6+DT0). Reuses the Kenwood port/baud/host
// settings — the K3 emulates the Kenwood command set, so a separate transport
// would be a near-total duplicate.
if h := strings.TrimSpace(s.KenwoodHost); h != "" {
kw := cat.NewKenwoodTCP(h, s.DigitalDefault)
kw.SetElecraft(true)
a.cat.Start(kw)
} else {
kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault)
kw.SetLowerLines(s.KenwoodLowLines)
kw.SetElecraft(true)
a.cat.Start(kw) a.cat.Start(kw)
} }
case "icom": case "icom":
+34
View File
@@ -0,0 +1,34 @@
package main
// How a Kenwood-dialect radio is reached.
//
// Three transports, and they are genuinely different things rather than three
// spellings of one: a COM port, the same CAT bytes carried over TCP by a serial
// bridge, and the radio's own network protocol — which is a session with its own
// framing and authentication, and is not implemented here.
//
// Kept as an explicit setting rather than inferred from which field an operator
// happened to fill in. The old code preferred a network address whenever one was
// present, which is invisible from the settings page: someone who typed a host
// months ago, then set a COM port, had a radio that never answered and nothing
// on screen to explain it.
const (
kenwoodLinkUSB = "usb" // a COM port
kenwoodLinkBridge = "bridge" // RS-232 to Ethernet: the same CAT bytes over TCP
kenwoodLinkNative = "native" // the radio's own network protocol — not supported
)
// kenwoodLinkOr resolves the stored choice, falling back to what an older
// install can be read as: a configured host meant the bridge, because that is
// what the previous code used it for.
func kenwoodLinkOr(link, host string) string {
switch link {
case kenwoodLinkUSB, kenwoodLinkBridge, kenwoodLinkNative:
return link
}
if host != "" {
return kenwoodLinkBridge
}
return kenwoodLinkUSB
}
+33
View File
@@ -0,0 +1,33 @@
package main
import "testing"
// The transport is a stored choice now, but installs exist that predate it and
// hold only a host. Reading those as "bridge" is what keeps a working station
// working across the upgrade — the old code used a host for exactly that.
func TestTheLinkIsReadFromAnOlderInstall(t *testing.T) {
cases := []struct {
link, host, want string
}{
{"", "", kenwoodLinkUSB}, // nothing configured
{"", "192.168.1.50:4999", kenwoodLinkBridge}, // upgraded: host only
{kenwoodLinkUSB, "192.168.1.50:4999", kenwoodLinkUSB}, // chose USB, host left behind
{kenwoodLinkBridge, "", kenwoodLinkBridge},
{kenwoodLinkNative, "", kenwoodLinkNative},
{"nonsense", "", kenwoodLinkUSB}, // a value nobody wrote: fall back, don't guess
}
for _, c := range cases {
if got := kenwoodLinkOr(c.link, c.host); got != c.want {
t.Errorf("link=%q host=%q → %q, want %q", c.link, c.host, got, c.want)
}
}
}
// The one that matters after the fact: an operator who picks USB while an old
// host is still stored must get the COM port. The previous code preferred the
// host whenever it was non-empty, which is invisible from the settings page.
func TestChoosingUSBBeatsALeftoverHost(t *testing.T) {
if got := kenwoodLinkOr(kenwoodLinkUSB, "10.0.0.9:4999"); got != kenwoodLinkUSB {
t.Fatalf("a leftover host overrode the operator's choice: %q", got)
}
}
+2 -2
View File
@@ -10,7 +10,7 @@
"PowerGenius XL: the amplifier's real state is read at startup. Its status frame carries no 'operate' field — the state is in 'state' — so on the direct GSCP link the flag was never read at all and OpsLog opened claiming STANDBY on an amp that was in line, with the first press of the button then commanding the state it was already in. IDLE means in line, not keyed.", "PowerGenius XL: the amplifier's real state is read at startup. Its status frame carries no 'operate' field — the state is in 'state' — so on the direct GSCP link the flag was never read at all and OpsLog opened claiming STANDBY on an amp that was in line, with the first press of the button then commanding the state it was already in. IDLE means in line, not keyed.",
"Antenna Genius: an option to write the SELECTED antenna into MY_ANTENNA, under the name it carries on the switch, ahead of the band default from Operating conditions. Which of the two ports counts is decided by the antenna jack the radio is transmitting on (ANT1/ANT2 on a Flex), with the jack-to-port wiring set once in Preferences — neither device can report it. When the port cannot be told, the log keeps the band default rather than naming an antenna at random.", "Antenna Genius: an option to write the SELECTED antenna into MY_ANTENNA, under the name it carries on the switch, ahead of the band default from Operating conditions. Which of the two ports counts is decided by the antenna jack the radio is transmitting on (ANT1/ANT2 on a Flex), with the jack-to-port wiring set once in Preferences — neither device can report it. When the port cannot be told, the log keeps the band default rather than naming an antenna at random.",
"The awards tab beside the entry form (F3) now offers only the awards this station follows, and drops the ones switched off — the same list the Awards tab reads. It offered every award that existed, so a station chasing three of them picked references out of a list of twenty.", "The awards tab beside the entry form (F3) now offers only the awards this station follows, and drops the ones switched off — the same list the Awards tab reads. It offered every award that existed, so a station chasing three of them picked references out of a list of twenty.",
"Kenwood / Elecraft CAT: USB and network are now an explicit choice, and only the fields of the chosen one are shown. Both were offered at once with nothing to say that the network address wins whenever it is filled in and the example address named port 4532, which is Hamlib rigctld, a different protocol and the one OpsLog itself serves under Share CAT. Pointed at the wrong kind of port, the link now says so instead of blaming the baud rate." "Kenwood / Elecraft CAT: the connection is a choice of three — USB / serial, RS-232 to Ethernet (a serial bridge carrying the same CAT), or the radio's own network protocol, which is named and refused rather than silently absent. Only the fields of the chosen one are shown. Before, the COM port and the network address sat side by side with nothing to say that the address wins whenever it is filled in, and the example address named port 4532 Hamlib rigctld, a different protocol, and the one OpsLog itself serves under Share CAT."
], ],
"fr": [ "fr": [
"Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.", "Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.",
@@ -20,7 +20,7 @@
"Power Genius XL : l'état réel de l'amplificateur est lu au démarrage. Sa trame d'état ne contient pas de champ « operate » — l'état est dans « state » — si bien que sur la liaison GSCP directe l'indicateur n'était jamais lu : OpsLog s'ouvrait en annonçant STANDBY sur un ampli en ligne, et le premier appui commandait l'état dans lequel il se trouvait déjà. IDLE veut dire en ligne, pas en émission.", "Power Genius XL : l'état réel de l'amplificateur est lu au démarrage. Sa trame d'état ne contient pas de champ « operate » — l'état est dans « state » — si bien que sur la liaison GSCP directe l'indicateur n'était jamais lu : OpsLog s'ouvrait en annonçant STANDBY sur un ampli en ligne, et le premier appui commandait l'état dans lequel il se trouvait déjà. IDLE veut dire en ligne, pas en émission.",
"Antenna Genius : une option pour inscrire l'antenne SÉLECTIONNÉE dans MY_ANTENNA, sous le nom qu'elle porte sur le switch, avant l'antenne par défaut des conditions de trafic. C'est la prise d'antenne sur laquelle la radio émet (ANT1/ANT2 sur un Flex) qui décide du port retenu, le câblage prise→port se règlant une fois dans les préférences — aucun des deux appareils ne peut le dire. Quand le port ne peut pas être déterminé, le journal conserve l'antenne par défaut plutôt que d'en nommer une au hasard.", "Antenna Genius : une option pour inscrire l'antenne SÉLECTIONNÉE dans MY_ANTENNA, sous le nom qu'elle porte sur le switch, avant l'antenne par défaut des conditions de trafic. C'est la prise d'antenne sur laquelle la radio émet (ANT1/ANT2 sur un Flex) qui décide du port retenu, le câblage prise→port se règlant une fois dans les préférences — aucun des deux appareils ne peut le dire. Quand le port ne peut pas être déterminé, le journal conserve l'antenne par défaut plutôt que d'en nommer une au hasard.",
"L'onglet des diplômes à côté de la saisie (F3) ne propose plus que les diplômes suivis par la station, et écarte ceux qui sont désactivés — la même liste que l'onglet Diplômes. Il proposait tous les diplômes existants : une station qui en chasse trois choisissait ses références dans une liste de vingt.", "L'onglet des diplômes à côté de la saisie (F3) ne propose plus que les diplômes suivis par la station, et écarte ceux qui sont désactivés — la même liste que l'onglet Diplômes. Il proposait tous les diplômes existants : une station qui en chasse trois choisissait ses références dans une liste de vingt.",
"CAT Kenwood / Elecraft : USB et réseau sont désormais un choix explicite, et seuls les champs du mode retenu s'affichent. Les deux étaient proposés en même temps sans rien indiquer que l'adresse réseau l'emporte dès qu'elle est renseignée et l'exemple d'adresse citait le port 4532, celui de Hamlib rigctld, un autre protocole, et justement celui qu'OpsLog propose lui-même sous « Partager le CAT ». Pointé sur un port de mauvaise nature, le lien le dit maintenant au lieu d'accuser la vitesse série." "CAT Kenwood / Elecraft : la connexion devient un choix entre trois — USB / série, RS-232 vers Ethernet (un pont série qui transporte le même CAT), ou le protocole réseau propre à la radio, qui est nommé et refusé plutôt qu'absent en silence. Seuls les champs du mode retenu s'affichent. Auparavant, le port COM et l'adresse réseau coexistaient sans rien indiquer que l'adresse l'emporte dès qu'elle est renseignée, et l'exemple citait le port 4532 Hamlib rigctld, un autre protocole, et celui qu'OpsLog propose lui-même sous « Partager le CAT »."
] ]
}, },
{ {
+15 -14
View File
@@ -1514,7 +1514,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [modeDraft, setModeDraft] = useState(''); const [modeDraft, setModeDraft] = useState('');
const [catCfg, setCatCfg] = useState<CATSettings>({ const [catCfg, setCatCfg] = useState<CATSettings>({
enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120, flex_dvk_dax: false, enabled: false, backend: 'omnirig', omnirig_rig: 1, omnirig_vfo: '', flex_host: '', flex_port: 4992, flex_spots: false, flex_decode_spots: false, flex_decode_secs: 120, flex_dvk_dax: false,
yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, kenwood_low_lines: false, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', kenwood_data_mode: 'usb', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70, xiegu_ptt_line: '', yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, kenwood_low_lines: false, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', kenwood_link: 'usb', kenwood_data_mode: 'usb', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70, xiegu_ptt_line: '',
icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false, icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0, offset_on: false, offset_hz: 0, tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0, offset_on: false, offset_hz: 0,
digital_default: 'FT8', share_enabled: false, share_port: 4532, share_proto: 'rigctl', share_tci_port: 40001, digital_default: 'FT8', share_enabled: false, share_port: 4532, share_proto: 'rigctl', share_tci_port: 40001,
@@ -3187,19 +3187,20 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
the field is not empty, which is invisible from here. */} the field is not empty, which is invisible from here. */}
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('cat.kwLink')}</Label> <Label>{t('cat.kwLink')}</Label>
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs"> <Select value={(catCfg as any).kenwood_link || 'usb'}
{[['usb', t('cat.kwLinkUsb')], ['net', t('cat.kwLinkNet')]].map(([v, label]) => ( onValueChange={(v) => setCatCfg((s) => ({ ...s, kenwood_link: v } as any))}>
<button key={v} type="button" <SelectTrigger><SelectValue /></SelectTrigger>
onClick={() => setCatCfg((s) => ({ ...s, kenwood_host: v === 'usb' ? '' : (s.kenwood_host || '') }))} <SelectContent>
className={cn('px-3 py-1.5 font-medium', <SelectItem value="usb">{t('cat.kwLinkUsb')}</SelectItem>
((catCfg.kenwood_host || '').trim() !== '') === (v === 'net') <SelectItem value="bridge">{t('cat.kwLinkBridge')}</SelectItem>
? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}> <SelectItem value="native">{t('cat.kwLinkNative')}</SelectItem>
{label} </SelectContent>
</button> </Select>
))} {((catCfg as any).kenwood_link || 'usb') === 'native' && (
</div> <p className="text-xs text-warning">{t('cat.kwLinkNativeHint')}</p>
)}
</div> </div>
{(catCfg.kenwood_host || '').trim() === '' && (<> {(((catCfg as any).kenwood_link || 'usb') === 'usb') && (<>
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('cat.kenwoodPort')}</Label> <Label>{t('cat.kenwoodPort')}</Label>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -3222,7 +3223,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</SelectContent> </SelectContent>
</Select> </div> </Select> </div>
</>)} </>)}
{(catCfg.kenwood_host || '').trim() !== '' && ( {((catCfg as any).kenwood_link === 'bridge') && (
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('cat.kenwoodHost')}</Label> <Label>{t('cat.kenwoodHost')}</Label>
<Input <Input
File diff suppressed because one or more lines are too long
+2
View File
@@ -2152,6 +2152,7 @@ export namespace main {
yaesu_baud: number; yaesu_baud: number;
kenwood_host: string; kenwood_host: string;
kenwood_port: string; kenwood_port: string;
kenwood_link: string;
kenwood_baud: number; kenwood_baud: number;
kenwood_data_mode: string; kenwood_data_mode: string;
yaesu_low_lines: boolean; yaesu_low_lines: boolean;
@@ -2203,6 +2204,7 @@ export namespace main {
this.yaesu_baud = source["yaesu_baud"]; this.yaesu_baud = source["yaesu_baud"];
this.kenwood_host = source["kenwood_host"]; this.kenwood_host = source["kenwood_host"];
this.kenwood_port = source["kenwood_port"]; this.kenwood_port = source["kenwood_port"];
this.kenwood_link = source["kenwood_link"];
this.kenwood_baud = source["kenwood_baud"]; this.kenwood_baud = source["kenwood_baud"];
this.kenwood_data_mode = source["kenwood_data_mode"]; this.kenwood_data_mode = source["kenwood_data_mode"];
this.yaesu_low_lines = source["yaesu_low_lines"]; this.yaesu_low_lines = source["yaesu_low_lines"];