Compare commits

...
4 Commits
Author SHA1 Message Date
rouggy 5d526d29db chore: release v0.27.16 2026-09-06 23:29:46 +02:00
rouggy 09c4358626 chore(changelog): the Yaesu work opens 0.27.16
The eight-digit rigs, the typed watering hole and the RTTY sideband all
landed after the 0.27.15 release commit, so they were sitting in a block
that had already shipped. 0.27.15 says what was released again.
2026-09-06 23:29:31 +02:00
rouggy 9ce7cf3b69 feat: a typed watering hole carries its mode; Yaesu RTTY sideband
His log settles the 28.074 case: "SetCATFrequency 28.074 MHz" and the
state still reads mode=USB — nothing sent a mode, so the rig simply
stayed where it was. A spot click has always carried one; a frequency
typed by hand carried none. It now uses the same table and the same
tolerance as a spot (±3 kHz of a known FT8/FT4/JS8 frequency), and only
towards the digital modes: tuning AWAY from one leaves the mode alone,
because there the frequency says nothing about what the operator means.

RTTY on Yaesu is a choice the log cannot make: ADIF records "RTTY" and
the rig has MD06 (RTTY-L) and MD09 (RTTY-U). The older lower sideband
stays the default and a station whose FSK controller wants the upper one
says so once in Settings → CAT.
2026-09-06 23:01:08 +02:00
rouggy 3745d23339 feat(yaesu): eight-digit rigs — the width comes from the radio
An FTDX3000 rejects every FA command: the FTDX10 family writes a
frequency in nine digits and everything before it — FTDX3000, FTDX5000,
FTDX1200, FT-2000, FT-950, FT-450 — writes eight, answering the longer
form with "?;". The rig would not follow and nothing said why.

The width is LEARNED from the rig's own replies rather than tabulated: it
announces the format in every answer to FA;, so it comes from the radio
in front of the operator instead of from a model list that is always one
release behind — and a Yaesu this backend has never heard of is right on
the first read. Nine until the first reply lands, which is what the
modern rigs use and what this backend was written against.

The five older ID codes are named too, so the console says FTDX3000
rather than a bare number.
2026-09-06 21:11:19 +02:00
11 changed files with 203 additions and 14 deletions
+10 -2
View File
@@ -157,6 +157,7 @@ const (
// the rule since is that nothing a backend does may be steered by another
// backend's setting.
keyCATYaesuLowLines = "cat.yaesu.low_dtr_rts" // deassert DTR/RTS on connect
keyCATYaesuRTTYUSB = "cat.yaesu.rtty_usb" // set RTTY on USB rather than the older LSB
keyCATKenwoodLowLines = "cat.kenwood.low_dtr_rts" // deassert DTR/RTS on connect
keyCATKenwoodDataMode = "cat.kenwood.data_mode" // data modes → "usb" | "data" (MD6) | "keep"
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
@@ -510,7 +511,11 @@ type CATSettings struct {
// Per-backend: deassert DTR and RTS after opening the CAT port, for
// interfaces that read either line as PTT. Off by default: lowering them
// stops some USB-serial interfaces transmitting at all.
YaesuLowLines bool `json:"yaesu_low_lines"`
YaesuLowLines bool `json:"yaesu_low_lines"`
// YaesuRTTYUSB: ADIF says "RTTY" and Yaesu has both sidebands, so the rig
// cannot be driven from the logged mode alone. Off = RTTY-L, the older
// convention.
YaesuRTTYUSB bool `json:"yaesu_rtty_usb"`
KenwoodLowLines bool `json:"kenwood_low_lines"`
IcomPort string `json:"icom_port"` // Icom USB CI-V serial port (e.g. COM5)
IcomBaud int `json:"icom_baud"` // Icom CI-V baud (default 115200)
@@ -8317,7 +8322,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if a.settings == nil {
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, 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, keyCATDigiUSB)
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDVKDax, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATYaesuRTTYUSB, 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, keyCATDigiUSB)
if err != nil {
return CATSettings{}, err
}
@@ -8343,6 +8348,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
KenwoodLink: kenwoodLinkOr(m[keyCATKenwoodLink], m[keyCATKenwoodHost]),
KenwoodBaud: 9600,
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
YaesuRTTYUSB: m[keyCATYaesuRTTYUSB] == "1",
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
KenwoodDataMode: m[keyCATKenwoodDataMode],
IcomPort: m[keyCATIcomPort],
@@ -8556,6 +8562,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
keyCATKenwoodLink: kenwoodLinkOr(s.KenwoodLink, s.KenwoodHost),
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
keyCATYaesuLowLines: b01(s.YaesuLowLines),
keyCATYaesuRTTYUSB: b01(s.YaesuRTTYUSB),
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
keyCATKenwoodDataMode: strings.ToLower(strings.TrimSpace(s.KenwoodDataMode)),
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
@@ -16283,6 +16290,7 @@ func (a *App) reloadCAT() {
// (a rig file that hides the VFO, a Freq property meaning A on one model
// and B on another); talking to the radio directly removes it.
yz := cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault)
yz.SetRTTYUpper(s.YaesuRTTYUSB)
yz.SetLowerLines(s.YaesuLowLines)
a.cat.Start(yz)
case "kenwood", "elecraft":
+14
View File
@@ -1,4 +1,18 @@
[
{
"version": "0.27.16",
"date": "",
"en": [
"[NEW] Typing a digital watering hole sets the mode with it. A spot click has always carried one; a frequency typed by hand carried none, so the rig stayed in SSB on 28.074 while the operator waited for decodes. Same table and same tolerance as a spot (±3 kHz of a known FT8/FT4/JS8 frequency), only towards the digital modes: tuning away from one leaves the mode alone, because there the frequency says nothing about what you mean to do.",
"Yaesu CAT now drives the older radios. The FTDX10, FT-991A, FT-891 and FT-710 write a frequency in nine digits; everything before them — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — writes eight and answers a nine-digit command with a rejection, which is what an FTDX3000 owner saw: every FA refused and a radio that would not follow. The width is taken from the rigs own reply rather than from a table of models, so a set is in the format that radio speaks — including models this backend has never heard of.",
"Yaesu: RTTY can be set on USB (Settings → CAT). ADIF records only “RTTY” and the rig has both sidebands, so the log cannot answer for it — the older RTTY-L stays the default, and a station whose FSK controller wants the upper one says so once."
],
"fr": [
"[NEW] Taper une fréquence dappel numérique règle le mode avec elle. Un clic sur un spot en portait un depuis toujours ; une fréquence tapée à la main nen portait aucun, si bien que le poste restait en SSB sur 28.074 pendant quon attendait les décodages. Même table et même tolérance quun spot (±3 kHz dune fréquence FT8/FT4/JS8 connue), et seulement vers les modes numériques : en sen éloignant le mode nest pas touché, car là la fréquence ne dit rien de ce quon veut faire.",
"Le CAT Yaesu pilote désormais les postes plus anciens. FTDX10, FT-991A, FT-891 et FT-710 écrivent une fréquence sur neuf chiffres ; tout ce qui précède — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — l’écrit sur huit et rejette une commande à neuf chiffres. Cest ce que voyait un possesseur de FTDX3000 : chaque FA refusée et une radio qui ne suivait pas. Le format est pris dans la réponse du poste plutôt que dans une table de modèles : lenvoi part donc dans la langue de cette radio-là, y compris pour des modèles que ce backend ne connaît pas.",
"Yaesu : le RTTY peut être placé en USB (Réglages → CAT). LADIF nenregistre que « RTTY » et le poste a les deux bandes latérales : le log ne peut pas répondre à sa place. Le RTTY-L ancien reste par défaut, et une station dont linterface FSK veut la supérieure le dit une fois."
]
},
{
"version": "0.27.15",
"date": "",
+24 -1
View File
@@ -5926,7 +5926,29 @@ export default function App() {
const mhz = parseFloat(mhzStr);
if (!Number.isFinite(mhz) || mhz < 0.1 || mhz > 3000) return;
noteManualEdit();
SetCATFrequency(Math.round(mhz * 1_000_000)).catch(() => {});
const hz = Math.round(mhz * 1_000_000);
SetCATFrequency(hz).catch(() => {});
tuneModeForWateringHole(hz);
};
// 28.074 IS FT8, and typing it says so.
//
// A spot click has always carried a mode; a frequency typed by hand carried
// none, so the rig stayed in whatever it was — an FTDX3000 landing on 28.074
// in USB while the operator waited for decodes. Nobody tunes a watering hole
// to listen to it in SSB, and this is the same table and the same tolerance a
// spot click is judged with (±3 kHz of a known digital frequency).
//
// Only when it actually changes something, and only towards the digital
// modes: tuning away from 28.074 leaves the mode alone, because there the
// frequency says nothing about what the operator means to do.
const tuneModeForWateringHole = (hz: number) => {
const m = inferDigitalMode(hz);
if (!m || m === mode) return;
setMode(m);
applyModePreset(m);
if (catState.enabled && catState.connected && !locks.mode) SetCATMode(m).catch(() => {});
ConfigureDecoderMode(m).catch(() => {});
};
// Carry out what was typed in the call field. Bands go through the ordinary
// band change, so the antennas, the power table and the outbound integrations
@@ -5944,6 +5966,7 @@ export default function App() {
const b = bandForMHz(hz / 1_000_000);
if (b) setBand(b);
if (catState.enabled && catState.connected) SetCATFrequency(hz).catch(() => {});
tuneModeForWateringHole(hz);
showToast((hz / 1_000_000).toFixed(3) + ' MHz');
};
+12 -1
View File
@@ -1628,7 +1628,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
const [icomCustom, setIcomCustom] = useState(false);
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,
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: '',
yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, yaesu_rtty_usb: 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,
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,
@@ -3532,6 +3532,17 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
</label>
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
</div>
{/* ADIF records "RTTY" and nothing more, and the rig has two
sidebands to put it on so this is the operator's answer, not
something the log can supply. */}
<div className="col-span-2 space-y-1">
<label className="flex items-center gap-2 text-xs cursor-pointer">
<Checkbox checked={!!(catCfg as any).yaesu_rtty_usb}
onCheckedChange={(c) => setCatCfg((s) => ({ ...s, yaesu_rtty_usb: !!c } as any))} />
{t('cat.yaesuRttyUsb')}
</label>
<span className="text-xs text-muted-foreground">{t('cat.yaesuRttyUsbHint')}</span>
</div>
</>
)}
{['icom', 'xiegu', 'kenwood', 'elecraft'].includes(catCfg.backend) && (
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
// Single source of truth for the app version shown in the UI (header + About).
// Bump this on a release (the release script updates it alongside telemetry.go).
export const APP_VERSION = '0.27.15';
export const APP_VERSION = '0.27.16';
// Author / credits, shown in Help -> About.
export const APP_AUTHOR = 'F4BPO';
+2
View File
@@ -2379,6 +2379,7 @@ export namespace main {
kenwood_baud: number;
kenwood_data_mode: string;
yaesu_low_lines: boolean;
yaesu_rtty_usb: boolean;
kenwood_low_lines: boolean;
icom_port: string;
icom_baud: number;
@@ -2432,6 +2433,7 @@ export namespace main {
this.kenwood_baud = source["kenwood_baud"];
this.kenwood_data_mode = source["kenwood_data_mode"];
this.yaesu_low_lines = source["yaesu_low_lines"];
this.yaesu_rtty_usb = source["yaesu_rtty_usb"];
this.kenwood_low_lines = source["kenwood_low_lines"];
this.icom_port = source["icom_port"];
this.icom_baud = source["icom_baud"];
+79 -5
View File
@@ -59,6 +59,13 @@ var yaesuModels = map[string]string{
"0650": "FT-891",
"0670": "FT-DX3000",
"0460": "FT-450D",
// The eight-digit family. Named for the console; the frequency format is
// learned from the rig either way (see learnFreqWidth).
"0251": "FT-2000",
"0310": "FT-950",
"0583": "FTDX1200",
"0462": "FTDX3000",
"0101": "FTDX5000",
}
// yaesuModeToADIF maps the MD digit to an ADIF mode. The DATA and RTTY variants
@@ -100,6 +107,11 @@ type Yaesu struct {
// rxVFOCmd is "FR" when the rig reports its receive VFO that way, else empty
// and VS is used — see ReadState.
rxVFOCmd string
// freqDigits is how many digits this rig writes a frequency in, LEARNED from
// its own replies. See learnFreqWidth.
freqDigits int
// rttyUpper picks RTTY-U over RTTY-L — see SetRTTYUpper.
rttyUpper bool
curFreq int64
curRXFreq int64
@@ -130,6 +142,18 @@ func NewYaesu(portName string, baud int, digital string) *Yaesu {
return &Yaesu{portName: strings.TrimSpace(portName), baud: baud, digital: digital, curVFO: "A"}
}
// SetRTTYUpper chooses which sideband RTTY is set on.
//
// Yaesu has both — MD06 is RTTY-L, MD09 is RTTY-U — and ADIF has neither: it
// says "RTTY" and stops there, so the rig cannot be driven from the logged mode
// alone. LSB is the older convention and stays the default; an operator whose
// FSK controller or decoder wants the other one says so once here.
func (y *Yaesu) SetRTTYUpper(v bool) {
y.mu.Lock()
defer y.mu.Unlock()
y.rttyUpper = v
}
// SetLowerLines chooses whether DTR and RTS are deasserted on connect. Set
// before Connect.
func (y *Yaesu) SetLowerLines(v bool) {
@@ -275,6 +299,7 @@ func (y *Yaesu) ReadState() (RigState, error) {
if err != nil {
return RigState{}, err // the rig stopped answering — let the Manager reconnect
}
y.learnFreqWidth(faRaw, "FA")
freqA, ok := parseYaesuFreq(faRaw, "FA")
if !ok {
return RigState{}, fmt.Errorf("yaesu: unparsable FA reply %q", faRaw)
@@ -355,7 +380,7 @@ func (y *Yaesu) SetFrequency(hz int64) error {
if y.curVFO == "B" {
cmd = "FB"
}
return y.write(fmt.Sprintf("%s%09d;", cmd, hz))
return y.write(fmt.Sprintf("%s%0*d;", cmd, y.freqWidth(), hz))
}
func (y *Yaesu) SetMode(mode string) error {
@@ -364,7 +389,7 @@ func (y *Yaesu) SetMode(mode string) error {
if y.port == nil {
return fmt.Errorf("yaesu: not connected")
}
d := yaesuModeDigit(mode, y.curFreq)
d := yaesuModeDigit(mode, y.curFreq, y.rttyUpper)
if d == 0 {
return fmt.Errorf("yaesu: no CAT mode for %q", mode)
}
@@ -481,7 +506,53 @@ func cmdPrefix(cmd string) string {
return c
}
// parseYaesuFreq reads "FA014074000;" into Hz.
// EIGHT DIGITS OR NINE — the rig says which, and it is not a matter of taste.
//
// The FTDX10, FT-991A, FT-891 and FT-710 write a frequency in nine digits;
// everything before them — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950,
// FT-450 — writes eight, and answers a nine-digit SET with "?;". An operator
// with an FTDX3000 saw exactly that: every FA command rejected, a radio that
// would not follow, and nothing to say why.
//
// The width is LEARNED rather than tabulated: the rig announces it in every
// reply to "FA;", so the answer comes from the radio in front of the operator
// instead of from a list of models that will always be one release behind. Nine
// until the first reply lands, which is what the modern rigs use and what this
// backend was written against.
const yaesuFreqDigitsDefault = 9
func (y *Yaesu) freqWidth() int {
if y.freqDigits >= 8 && y.freqDigits <= 11 {
return y.freqDigits
}
return yaesuFreqDigitsDefault
}
// learnFreqWidth takes the width from a frequency reply. Only a reply that
// parses as a frequency teaches anything — a "?;" or a stray frame says nothing
// about the format, and a width learned from one would be worse than the
// default.
func (y *Yaesu) learnFreqWidth(reply, prefix string) {
r := strings.TrimSpace(reply)
if !strings.HasPrefix(r, prefix) {
return
}
digits := strings.TrimSuffix(strings.TrimPrefix(r, prefix), ";")
if len(digits) < 8 || len(digits) > 11 {
return
}
for _, c := range digits {
if c < '0' || c > '9' {
return
}
}
if y.freqDigits != len(digits) {
debugLog.Printf("yaesu: this rig writes frequencies in %d digits — commands will match", len(digits))
y.freqDigits = len(digits)
}
}
// parseYaesuFreq reads "FA014074000;" (or "FA14074000;") into Hz.
func parseYaesuFreq(reply, prefix string) (int64, bool) {
r := strings.TrimSpace(reply)
if !strings.HasPrefix(r, prefix) {
@@ -551,7 +622,7 @@ func resolveYaesuVFOs(freqA, freqB int64, vfo string, split bool) (tx, rx int64,
// yaesuModeDigit maps an ADIF mode to the MD digit. SSB has no single digit —
// the sideband follows the worldwide convention (LSB below 10 MHz, USB above),
// which is why the current frequency is part of the decision.
func yaesuModeDigit(mode string, freqHz int64) byte {
func yaesuModeDigit(mode string, freqHz int64, rttyUpper bool) byte {
switch strings.ToUpper(strings.TrimSpace(mode)) {
case "SSB":
if freqHz > 0 && freqHz < 10_000_000 {
@@ -569,7 +640,10 @@ func yaesuModeDigit(mode string, freqHz int64) byte {
case "AM":
return '5'
case "RTTY":
return '6'
if rttyUpper {
return '9' // RTTY-U
}
return '6' // RTTY-L, the older convention
case "":
return 0
default:
+38
View File
@@ -0,0 +1,38 @@
package cat
import "testing"
// The FTDX10 family writes a frequency in nine digits; everything before it —
// FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — writes eight and
// answers a nine-digit SET with "?;". Reported from an FTDX3000: every FA
// command rejected, a radio that would not follow.
//
// The width is taken from the rig's own reply, so a model this backend has
// never heard of is right on the first read.
func TestYaesuFrequencyWidthIsLearnedFromTheRig(t *testing.T) {
y := &Yaesu{}
if got := y.freqWidth(); got != 9 {
t.Errorf("before any reply the width is %d, want the modern 9", got)
}
y.learnFreqWidth("FA14074000;", "FA") // an FTDX3000
if got := y.freqWidth(); got != 8 {
t.Errorf("width %d after an eight-digit reply, want 8", got)
}
y.learnFreqWidth("FA014074000;", "FA") // and an FTDX10 on the next session
if got := y.freqWidth(); got != 9 {
t.Errorf("width %d after a nine-digit reply, want 9", got)
}
// Nothing that is not a frequency teaches anything: a rejection, a stray
// frame or a reply from another command would otherwise set the format for
// every command that follows.
for _, junk := range []string{"?;", "FA;", "FB014074000;", "FA1407400X;", "FA1234567;", "FA123456789012;"} {
before := y.freqWidth()
y.learnFreqWidth(junk, "FA")
if after := y.freqWidth(); after != before {
t.Errorf("%q changed the width from %d to %d", junk, before, after)
}
}
}
+20 -1
View File
@@ -123,7 +123,7 @@ func TestYaesuModeDigit(t *testing.T) {
{"", 14074000, 0}, // nothing to set
}
for _, c := range cases {
if got := yaesuModeDigit(c.mode, c.hz); got != c.want {
if got := yaesuModeDigit(c.mode, c.hz, false); got != c.want {
t.Errorf("yaesuModeDigit(%q, %d) = %q, want %q", c.mode, c.hz, got, c.want)
}
}
@@ -406,3 +406,22 @@ func TestYaesuAntennaCommand(t *testing.T) {
}
}
}
// ADIF says "RTTY" and stops there, but Yaesu has both sidebands and the rig
// has to be told one. LSB is the older convention and the default; the other is
// a station's own choice, made once.
func TestYaesuRTTYSideband(t *testing.T) {
if got := yaesuModeDigit("RTTY", 14_080_000, false); got != '6' {
t.Errorf("RTTY = %q, want RTTY-L", got)
}
if got := yaesuModeDigit("RTTY", 14_080_000, true); got != '9' {
t.Errorf("RTTY (upper) = %q, want RTTY-U", got)
}
// The switch is about RTTY and nothing else.
if got := yaesuModeDigit("FT8", 28_074_000, true); got != 'C' {
t.Errorf("FT8 = %q, want DATA-U", got)
}
if got := yaesuModeDigit("CW", 14_030_000, true); got != '3' {
t.Errorf("CW = %q, want CW-U", got)
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ import (
const (
// appVersion is stamped on every heartbeat (and could feed the About box).
appVersion = "0.27.15"
appVersion = "0.27.16"
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
// to https://us.i.posthog.com for a US project.