Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d526d29db | ||
|
|
09c4358626 | ||
|
|
9ce7cf3b69 | ||
|
|
3745d23339 |
@@ -157,6 +157,7 @@ const (
|
|||||||
// the rule since is that nothing a backend does may be steered by another
|
// the rule since is that nothing a backend does may be steered by another
|
||||||
// backend's setting.
|
// backend's setting.
|
||||||
keyCATYaesuLowLines = "cat.yaesu.low_dtr_rts" // deassert DTR/RTS on connect
|
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
|
keyCATKenwoodLowLines = "cat.kenwood.low_dtr_rts" // deassert DTR/RTS on connect
|
||||||
keyCATKenwoodDataMode = "cat.kenwood.data_mode" // data modes → "usb" | "data" (MD6) | "keep"
|
keyCATKenwoodDataMode = "cat.kenwood.data_mode" // data modes → "usb" | "data" (MD6) | "keep"
|
||||||
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
|
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
|
||||||
@@ -511,6 +512,10 @@ type CATSettings struct {
|
|||||||
// interfaces that read either line as PTT. Off by default: lowering them
|
// interfaces that read either line as PTT. Off by default: lowering them
|
||||||
// stops some USB-serial interfaces transmitting at all.
|
// 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"`
|
KenwoodLowLines bool `json:"kenwood_low_lines"`
|
||||||
IcomPort string `json:"icom_port"` // Icom USB CI-V serial port (e.g. COM5)
|
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)
|
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 {
|
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, 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 {
|
if err != nil {
|
||||||
return CATSettings{}, err
|
return CATSettings{}, err
|
||||||
}
|
}
|
||||||
@@ -8343,6 +8348,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
|||||||
KenwoodLink: kenwoodLinkOr(m[keyCATKenwoodLink], m[keyCATKenwoodHost]),
|
KenwoodLink: kenwoodLinkOr(m[keyCATKenwoodLink], m[keyCATKenwoodHost]),
|
||||||
KenwoodBaud: 9600,
|
KenwoodBaud: 9600,
|
||||||
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
|
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
|
||||||
|
YaesuRTTYUSB: m[keyCATYaesuRTTYUSB] == "1",
|
||||||
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
|
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
|
||||||
KenwoodDataMode: m[keyCATKenwoodDataMode],
|
KenwoodDataMode: m[keyCATKenwoodDataMode],
|
||||||
IcomPort: m[keyCATIcomPort],
|
IcomPort: m[keyCATIcomPort],
|
||||||
@@ -8556,6 +8562,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
|||||||
keyCATKenwoodLink: kenwoodLinkOr(s.KenwoodLink, 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),
|
||||||
|
keyCATYaesuRTTYUSB: b01(s.YaesuRTTYUSB),
|
||||||
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
|
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
|
||||||
keyCATKenwoodDataMode: strings.ToLower(strings.TrimSpace(s.KenwoodDataMode)),
|
keyCATKenwoodDataMode: strings.ToLower(strings.TrimSpace(s.KenwoodDataMode)),
|
||||||
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
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
|
// (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.
|
// and B on another); talking to the radio directly removes it.
|
||||||
yz := cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault)
|
yz := cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault)
|
||||||
|
yz.SetRTTYUpper(s.YaesuRTTYUSB)
|
||||||
yz.SetLowerLines(s.YaesuLowLines)
|
yz.SetLowerLines(s.YaesuLowLines)
|
||||||
a.cat.Start(yz)
|
a.cat.Start(yz)
|
||||||
case "kenwood", "elecraft":
|
case "kenwood", "elecraft":
|
||||||
|
|||||||
@@ -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 rig’s 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 d’appel 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 n’en portait aucun, si bien que le poste restait en SSB sur 28.074 pendant qu’on attendait les décodages. Même table et même tolérance qu’un spot (±3 kHz d’une fréquence FT8/FT4/JS8 connue), et seulement vers les modes numériques : en s’en éloignant le mode n’est pas touché, car là la fréquence ne dit rien de ce qu’on 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. C’est 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 : l’envoi 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). L’ADIF n’enregistre 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 l’interface FSK veut la supérieure le dit une fois."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.27.15",
|
"version": "0.27.15",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
+24
-1
@@ -5926,7 +5926,29 @@ export default function App() {
|
|||||||
const mhz = parseFloat(mhzStr);
|
const mhz = parseFloat(mhzStr);
|
||||||
if (!Number.isFinite(mhz) || mhz < 0.1 || mhz > 3000) return;
|
if (!Number.isFinite(mhz) || mhz < 0.1 || mhz > 3000) return;
|
||||||
noteManualEdit();
|
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
|
// 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
|
// 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);
|
const b = bandForMHz(hz / 1_000_000);
|
||||||
if (b) setBand(b);
|
if (b) setBand(b);
|
||||||
if (catState.enabled && catState.connected) SetCATFrequency(hz).catch(() => {});
|
if (catState.enabled && catState.connected) SetCATFrequency(hz).catch(() => {});
|
||||||
|
tuneModeForWateringHole(hz);
|
||||||
showToast((hz / 1_000_000).toFixed(3) + ' MHz');
|
showToast((hz / 1_000_000).toFixed(3) + ' MHz');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1628,7 +1628,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
const [icomCustom, setIcomCustom] = useState(false);
|
const [icomCustom, setIcomCustom] = useState(false);
|
||||||
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_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,
|
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,
|
||||||
@@ -3532,6 +3532,17 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
</label>
|
</label>
|
||||||
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
||||||
</div>
|
</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) && (
|
{['icom', 'xiegu', 'kenwood', 'elecraft'].includes(catCfg.backend) && (
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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.15';
|
export const APP_VERSION = '0.27.16';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
@@ -2379,6 +2379,7 @@ export namespace main {
|
|||||||
kenwood_baud: number;
|
kenwood_baud: number;
|
||||||
kenwood_data_mode: string;
|
kenwood_data_mode: string;
|
||||||
yaesu_low_lines: boolean;
|
yaesu_low_lines: boolean;
|
||||||
|
yaesu_rtty_usb: boolean;
|
||||||
kenwood_low_lines: boolean;
|
kenwood_low_lines: boolean;
|
||||||
icom_port: string;
|
icom_port: string;
|
||||||
icom_baud: number;
|
icom_baud: number;
|
||||||
@@ -2432,6 +2433,7 @@ export namespace main {
|
|||||||
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"];
|
||||||
|
this.yaesu_rtty_usb = source["yaesu_rtty_usb"];
|
||||||
this.kenwood_low_lines = source["kenwood_low_lines"];
|
this.kenwood_low_lines = source["kenwood_low_lines"];
|
||||||
this.icom_port = source["icom_port"];
|
this.icom_port = source["icom_port"];
|
||||||
this.icom_baud = source["icom_baud"];
|
this.icom_baud = source["icom_baud"];
|
||||||
|
|||||||
+79
-5
@@ -59,6 +59,13 @@ var yaesuModels = map[string]string{
|
|||||||
"0650": "FT-891",
|
"0650": "FT-891",
|
||||||
"0670": "FT-DX3000",
|
"0670": "FT-DX3000",
|
||||||
"0460": "FT-450D",
|
"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
|
// 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
|
// rxVFOCmd is "FR" when the rig reports its receive VFO that way, else empty
|
||||||
// and VS is used — see ReadState.
|
// and VS is used — see ReadState.
|
||||||
rxVFOCmd string
|
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
|
curFreq int64
|
||||||
curRXFreq 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"}
|
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
|
// SetLowerLines chooses whether DTR and RTS are deasserted on connect. Set
|
||||||
// before Connect.
|
// before Connect.
|
||||||
func (y *Yaesu) SetLowerLines(v bool) {
|
func (y *Yaesu) SetLowerLines(v bool) {
|
||||||
@@ -275,6 +299,7 @@ func (y *Yaesu) ReadState() (RigState, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return RigState{}, err // the rig stopped answering — let the Manager reconnect
|
return RigState{}, err // the rig stopped answering — let the Manager reconnect
|
||||||
}
|
}
|
||||||
|
y.learnFreqWidth(faRaw, "FA")
|
||||||
freqA, ok := parseYaesuFreq(faRaw, "FA")
|
freqA, ok := parseYaesuFreq(faRaw, "FA")
|
||||||
if !ok {
|
if !ok {
|
||||||
return RigState{}, fmt.Errorf("yaesu: unparsable FA reply %q", faRaw)
|
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" {
|
if y.curVFO == "B" {
|
||||||
cmd = "FB"
|
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 {
|
func (y *Yaesu) SetMode(mode string) error {
|
||||||
@@ -364,7 +389,7 @@ func (y *Yaesu) SetMode(mode string) error {
|
|||||||
if y.port == nil {
|
if y.port == nil {
|
||||||
return fmt.Errorf("yaesu: not connected")
|
return fmt.Errorf("yaesu: not connected")
|
||||||
}
|
}
|
||||||
d := yaesuModeDigit(mode, y.curFreq)
|
d := yaesuModeDigit(mode, y.curFreq, y.rttyUpper)
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
return fmt.Errorf("yaesu: no CAT mode for %q", mode)
|
return fmt.Errorf("yaesu: no CAT mode for %q", mode)
|
||||||
}
|
}
|
||||||
@@ -481,7 +506,53 @@ func cmdPrefix(cmd string) string {
|
|||||||
return c
|
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) {
|
func parseYaesuFreq(reply, prefix string) (int64, bool) {
|
||||||
r := strings.TrimSpace(reply)
|
r := strings.TrimSpace(reply)
|
||||||
if !strings.HasPrefix(r, prefix) {
|
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 —
|
// 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),
|
// the sideband follows the worldwide convention (LSB below 10 MHz, USB above),
|
||||||
// which is why the current frequency is part of the decision.
|
// 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)) {
|
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
||||||
case "SSB":
|
case "SSB":
|
||||||
if freqHz > 0 && freqHz < 10_000_000 {
|
if freqHz > 0 && freqHz < 10_000_000 {
|
||||||
@@ -569,7 +640,10 @@ func yaesuModeDigit(mode string, freqHz int64) byte {
|
|||||||
case "AM":
|
case "AM":
|
||||||
return '5'
|
return '5'
|
||||||
case "RTTY":
|
case "RTTY":
|
||||||
return '6'
|
if rttyUpper {
|
||||||
|
return '9' // RTTY-U
|
||||||
|
}
|
||||||
|
return '6' // RTTY-L, the older convention
|
||||||
case "":
|
case "":
|
||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -123,7 +123,7 @@ func TestYaesuModeDigit(t *testing.T) {
|
|||||||
{"", 14074000, 0}, // nothing to set
|
{"", 14074000, 0}, // nothing to set
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
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)
|
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
@@ -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.15"
|
appVersion = "0.27.16"
|
||||||
|
|
||||||
// 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