Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6cd6e999a | ||
|
|
296a4a55c0 | ||
|
|
acf73c73c7 | ||
|
|
5777c119cb | ||
|
|
85bf0da006 | ||
|
|
b6ea07e3a3 |
@@ -120,6 +120,12 @@ const (
|
|||||||
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"
|
||||||
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
|
||||||
|
// into the Yaesu and Kenwood backends is what broke them in 0.22.7/0.22.8;
|
||||||
|
// 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
|
||||||
|
keyCATKenwoodLowLines = "cat.kenwood.low_dtr_rts" // deassert DTR/RTS on connect
|
||||||
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)
|
||||||
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
|
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
|
||||||
keyCATIcomAddr = "cat.icom.addr" // Icom CI-V address, decimal (IC-7610 = 152 / 0x98)
|
keyCATIcomAddr = "cat.icom.addr" // Icom CI-V address, decimal (IC-7610 = 152 / 0x98)
|
||||||
@@ -378,6 +384,11 @@ type CATSettings struct {
|
|||||||
// adapter). NOT the radio’s own RJ45, which speaks Kenwood’s KNS/ARCP.
|
// adapter). NOT the radio’s own RJ45, which speaks Kenwood’s 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)
|
||||||
KenwoodBaud int `json:"kenwood_baud"` // Kenwood CAT baud (TS-590 default 9600)
|
KenwoodBaud int `json:"kenwood_baud"` // Kenwood CAT baud (TS-590 default 9600)
|
||||||
|
// 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"`
|
||||||
|
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)
|
||||||
IcomAddr int `json:"icom_addr"` // Icom CI-V address, decimal (IC-7610 = 152)
|
IcomAddr int `json:"icom_addr"` // Icom CI-V address, decimal (IC-7610 = 152)
|
||||||
@@ -3514,10 +3525,17 @@ func (a *App) GetAwards() ([]award.Result, error) {
|
|||||||
// GetAward computes progress for a single award by code (one whole-log scan,
|
// GetAward computes progress for a single award by code (one whole-log scan,
|
||||||
// matching only that award). This is what the awards UI calls when an award is
|
// matching only that award). This is what the awards UI calls when an award is
|
||||||
// selected, so opening the panel doesn't scan every award up front.
|
// selected, so opening the panel doesn't scan every award up front.
|
||||||
func (a *App) GetAward(code string) (award.Result, error) {
|
// GetAward computes one award. modeClass ("CW", "PHONE", "DIGI", or "" for all)
|
||||||
|
// narrows it to contacts of that class BEFORE the engine runs, so the bands, the
|
||||||
|
// counts and the confirmations all describe that mode.
|
||||||
|
//
|
||||||
|
// Filtering only the reference list was not enough: a reference kept because it
|
||||||
|
// has one CW contact still showed the band cells it earned on SSB, which is
|
||||||
|
// precisely what an operator chasing a CW award must not be told.
|
||||||
|
func (a *App) GetAward(code, modeClass string) (award.Result, error) {
|
||||||
for _, d := range a.awardDefs() {
|
for _, d := range a.awardDefs() {
|
||||||
if strings.EqualFold(d.Code, code) {
|
if strings.EqualFold(d.Code, code) {
|
||||||
results, err := a.computeAwards([]award.Def{d})
|
results, err := a.computeAwardsForMode([]award.Def{d}, modeClass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return award.Result{}, err
|
return award.Result{}, err
|
||||||
}
|
}
|
||||||
@@ -3533,6 +3551,13 @@ func (a *App) GetAward(code string) (award.Result, error) {
|
|||||||
// computeAwards runs the engine for the given award definitions over the whole
|
// computeAwards runs the engine for the given award definitions over the whole
|
||||||
// log and enriches dynamic awards (totals + worked-reference names).
|
// log and enriches dynamic awards (totals + worked-reference names).
|
||||||
func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
|
func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
|
||||||
|
return a.computeAwardsForMode(defs, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeAwardsForMode is computeAwards restricted to one mode class. The
|
||||||
|
// snapshot is shared and cached by log revision, so this costs a matching pass,
|
||||||
|
// not a database read.
|
||||||
|
func (a *App) computeAwardsForMode(defs []award.Def, modeClass string) ([]award.Result, error) {
|
||||||
if a.qso == nil {
|
if a.qso == nil {
|
||||||
return nil, fmt.Errorf("db not initialized")
|
return nil, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
@@ -3540,6 +3565,15 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if want := strings.ToUpper(strings.TrimSpace(modeClass)); want != "" && want != "ALL" {
|
||||||
|
kept := make([]qso.QSO, 0, len(all))
|
||||||
|
for _, q := range all {
|
||||||
|
if award.ModeClass(q.Mode) == want {
|
||||||
|
kept = append(kept, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all = kept
|
||||||
|
}
|
||||||
nameOf := func(field, ref string) string {
|
nameOf := func(field, ref string) string {
|
||||||
switch field {
|
switch field {
|
||||||
case "dxcc":
|
case "dxcc":
|
||||||
@@ -3590,7 +3624,11 @@ func (a *App) computeAwards(defs []award.Def) ([]award.Result, error) {
|
|||||||
// AwardCellQSOs returns the QSOs that contribute to one award reference,
|
// AwardCellQSOs returns the QSOs that contribute to one award reference,
|
||||||
// optionally on a single band (band="" = all bands). Powers the award-grid
|
// optionally on a single band (band="" = all bands). Powers the award-grid
|
||||||
// cell drill-down ("show me every Canada contact on 20m").
|
// cell drill-down ("show me every Canada contact on 20m").
|
||||||
func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) {
|
// AwardCellQSOs lists the contacts behind one cell. modeClass ("CW", "PHONE",
|
||||||
|
// "DIGI", or "" for all) applies the SAME filter as the reference list above it
|
||||||
|
// — without it, filtering the list to CW and then opening a reference showed the
|
||||||
|
// FT8 and SSB contacts that filter exists to hide.
|
||||||
|
func (a *App) AwardCellQSOs(code, ref, band, modeClass string) ([]qso.QSO, error) {
|
||||||
if a.qso == nil {
|
if a.qso == nil {
|
||||||
return nil, fmt.Errorf("db not initialized")
|
return nil, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
@@ -3608,12 +3646,16 @@ func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) {
|
|||||||
metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)]
|
metas := a.awardRefMetas([]award.Def{*def})[strings.ToUpper(def.Code)]
|
||||||
wantRef := strings.ToUpper(strings.TrimSpace(ref))
|
wantRef := strings.ToUpper(strings.TrimSpace(ref))
|
||||||
wantBand := strings.ToLower(strings.TrimSpace(band))
|
wantBand := strings.ToLower(strings.TrimSpace(band))
|
||||||
|
wantMode := strings.ToUpper(strings.TrimSpace(modeClass))
|
||||||
|
|
||||||
var out []qso.QSO
|
var out []qso.QSO
|
||||||
err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error {
|
err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error {
|
||||||
if wantBand != "" && strings.ToLower(strings.TrimSpace(q.Band)) != wantBand {
|
if wantBand != "" && strings.ToLower(strings.TrimSpace(q.Band)) != wantBand {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if wantMode != "" && wantMode != "ALL" && award.ModeClass(q.Mode) != wantMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
a.enrichQSOForAwards(&q)
|
a.enrichQSOForAwards(&q)
|
||||||
for _, c := range award.MatchQSO(*def, metas, &q) {
|
for _, c := range award.MatchQSO(*def, metas, &q) {
|
||||||
if strings.ToUpper(c) == wantRef {
|
if strings.ToUpper(c) == wantRef {
|
||||||
@@ -6788,7 +6830,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, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort)
|
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CATSettings{}, err
|
return CATSettings{}, err
|
||||||
}
|
}
|
||||||
@@ -6809,6 +6851,8 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
|||||||
KenwoodPort: m[keyCATKenwoodPort],
|
KenwoodPort: m[keyCATKenwoodPort],
|
||||||
KenwoodHost: m[keyCATKenwoodHost],
|
KenwoodHost: m[keyCATKenwoodHost],
|
||||||
KenwoodBaud: 9600,
|
KenwoodBaud: 9600,
|
||||||
|
YaesuLowLines: m[keyCATYaesuLowLines] == "1",
|
||||||
|
KenwoodLowLines: m[keyCATKenwoodLowLines] == "1",
|
||||||
IcomPort: m[keyCATIcomPort],
|
IcomPort: m[keyCATIcomPort],
|
||||||
IcomBaud: 115200,
|
IcomBaud: 115200,
|
||||||
IcomAddr: 0x98, // IC-7610 default
|
IcomAddr: 0x98, // IC-7610 default
|
||||||
@@ -6944,6 +6988,12 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
|||||||
if s.DigitalDefault == "" {
|
if s.DigitalDefault == "" {
|
||||||
s.DigitalDefault = "FT8"
|
s.DigitalDefault = "FT8"
|
||||||
}
|
}
|
||||||
|
b01 := func(v bool) string {
|
||||||
|
if v {
|
||||||
|
return "1"
|
||||||
|
}
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
shareEnabled := "0"
|
shareEnabled := "0"
|
||||||
if s.ShareEnabled {
|
if s.ShareEnabled {
|
||||||
shareEnabled = "1"
|
shareEnabled = "1"
|
||||||
@@ -6967,6 +7017,8 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
|||||||
keyCATKenwoodPort: strings.TrimSpace(s.KenwoodPort),
|
keyCATKenwoodPort: strings.TrimSpace(s.KenwoodPort),
|
||||||
keyCATKenwoodHost: strings.TrimSpace(s.KenwoodHost),
|
keyCATKenwoodHost: strings.TrimSpace(s.KenwoodHost),
|
||||||
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
|
keyCATKenwoodBaud: strconv.Itoa(s.KenwoodBaud),
|
||||||
|
keyCATYaesuLowLines: b01(s.YaesuLowLines),
|
||||||
|
keyCATKenwoodLowLines: b01(s.KenwoodLowLines),
|
||||||
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
||||||
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
|
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
|
||||||
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
|
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
|
||||||
@@ -12796,7 +12848,9 @@ func (a *App) reloadCAT() {
|
|||||||
// Yaesu fault reported so far came from OmniRig's interpretation layer
|
// Yaesu fault reported so far came from OmniRig's interpretation layer
|
||||||
// (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.
|
||||||
a.cat.Start(cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault))
|
yz := cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault)
|
||||||
|
yz.SetLowerLines(s.YaesuLowLines)
|
||||||
|
a.cat.Start(yz)
|
||||||
case "kenwood":
|
case "kenwood":
|
||||||
// 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 (Elecraft K3/K4, and the "Kenwood" setting on other rigs).
|
||||||
@@ -12808,7 +12862,9 @@ func (a *App) reloadCAT() {
|
|||||||
if h := strings.TrimSpace(s.KenwoodHost); h != "" {
|
if h := strings.TrimSpace(s.KenwoodHost); h != "" {
|
||||||
a.cat.Start(cat.NewKenwoodTCP(h, s.DigitalDefault))
|
a.cat.Start(cat.NewKenwoodTCP(h, s.DigitalDefault))
|
||||||
} else {
|
} else {
|
||||||
a.cat.Start(cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault))
|
kw := cat.NewKenwood(s.KenwoodPort, s.KenwoodBaud, s.DigitalDefault)
|
||||||
|
kw.SetLowerLines(s.KenwoodLowLines)
|
||||||
|
a.cat.Start(kw)
|
||||||
}
|
}
|
||||||
case "icom":
|
case "icom":
|
||||||
// Native Icom CI-V over the radio's USB serial port (local control).
|
// Native Icom CI-V over the radio's USB serial port (local control).
|
||||||
@@ -14734,8 +14790,14 @@ func (a *App) AmpOperate(id string, on bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AmpPower turns the given amp on/off (SPE and ACOM; the PGXL has no power
|
// AmpPower turns the given amp on/off (SPE and ACOM; the PGXL has no power
|
||||||
// command on its direct link).
|
// command on its direct link). Logged here because the UI buttons swallow the
|
||||||
func (a *App) AmpPower(id string, on bool) error {
|
// returned error — a failed power-on otherwise looks like a dead button.
|
||||||
|
func (a *App) AmpPower(id string, on bool) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
applog.Printf("amp %s: power %v failed: %v", id, on, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
inst := a.ampInstByID(id)
|
inst := a.ampInstByID(id)
|
||||||
if inst == nil {
|
if inst == nil {
|
||||||
return fmt.Errorf("amplifier not running — check Settings → Amplifier")
|
return fmt.Errorf("amplifier not running — check Settings → Amplifier")
|
||||||
|
|||||||
@@ -1,4 +1,30 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.22.9",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"Awards: the CW / Phone / Digital filter now applies to the whole award — bands, counts, confirmations and the contacts behind a cell — instead of only hiding rows from the reference list.",
|
||||||
|
"Yaesu and Kenwood: a new option lowers the DTR and RTS lines on connect, for interfaces that read either as PTT and keep the radio transmitting while OpsLog is open. Off by default — lowering them stops other interfaces transmitting at all.",
|
||||||
|
"SPE amplifiers: the ON and OFF buttons now switch the amplifier on and off over USB.",
|
||||||
|
"Quieter diagnostic log: no more FlexRadio meter subscriptions, per-packet WSJT-X/MSHV lines, or repeated Tuner Genius / PGXL / SPE status frames.",
|
||||||
|
"Station Control: the cards now line up on a grid — same width, and same height across a row — and the column selector goes up to 6.",
|
||||||
|
"New amplifier widget beside the keyers (flame icon): Operate, power ON/OFF, the SPE power level and live watts / SWR / temperature. With several amplifiers a dropdown on the icon picks the one to watch, or all of them.",
|
||||||
|
"Icom: the meters and front-panel readings are polled only while the Icom console is on screen. They were half of the CI-V traffic at all times, which matters when the link is shared — a microHAM Router splitting one CI-V port between OpsLog and WSJT-X, for instance.",
|
||||||
|
"Icom: the power-ON button sends a wake preamble sized for the CI-V speed. It was a fixed length that only suited 19200 baud, so a radio set faster — 115200 is the default — ignored the command.",
|
||||||
|
"The Tuner Genius power meter now shows the peak the device itself measured, instead of whatever instant the poll happened to catch — a kilowatt transmission could read a few hundred watts. Every power meter also clears the moment the carrier drops, instead of trailing several seconds behind it."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Diplômes : le filtre CW / Phonie / Numérique s’applique désormais à tout le diplôme — bandes, comptes, confirmations et QSO derrière une case — au lieu de masquer seulement des lignes de la liste des références.",
|
||||||
|
"Yaesu et Kenwood : une option abaisse les lignes DTR et RTS à la connexion, pour les interfaces qui les lisent comme PTT et laissent la radio en émission tant qu’OpsLog est ouvert. Désactivée par défaut — les abaisser empêche d’autres interfaces d’émettre.",
|
||||||
|
"Amplificateurs SPE : les boutons ON et OFF allument et éteignent désormais l’amplificateur par l’USB.",
|
||||||
|
"Journal de diagnostic allégé : plus d’abonnements aux mètres FlexRadio, de ligne par paquet WSJT-X/MSHV, ni de trames statut Tuner Genius / PGXL / SPE répétées.",
|
||||||
|
"Contrôle station : les cartes s’alignent désormais sur une grille — même largeur, et même hauteur sur une ligne — et le sélecteur de colonnes va jusqu’à 6.",
|
||||||
|
"Nouveau widget amplificateur à côté des manipulateurs (icône flamme) : Operate, marche/arrêt, le niveau de puissance SPE et les watts / ROS / température en direct. Avec plusieurs amplis, une liste déroulante sur l’icône choisit celui à surveiller, ou tous.",
|
||||||
|
"Icom : les mesures et les réglages de façade ne sont interrogés que lorsque la console Icom est affichée. Ils représentaient la moitié du trafic CI-V en permanence, ce qui compte quand la liaison est partagée — un Router microHAM qui répartit un port CI-V entre OpsLog et WSJT-X, par exemple.",
|
||||||
|
"Icom : le bouton de mise sous tension envoie un préambule de réveil dimensionné pour la vitesse CI-V. Sa longueur était fixe et ne convenait qu’à 19200 bauds, si bien qu’une radio réglée plus vite — 115200 par défaut — ignorait la commande.",
|
||||||
|
"Le mètre de puissance du Tuner Genius affiche désormais la crête mesurée par l’appareil lui-même, au lieu de l’instant capté par l’interrogation — une émission d’un kilowatt pouvait afficher quelques centaines de watts. Tous les mètres de puissance retombent aussi dès la fin de l’émission, au lieu de traîner plusieurs secondes."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.22.8",
|
"version": "0.22.8",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
+82
-4
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Gauge, Hash, Loader2, Lock,
|
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Flame, Gauge, Hash, Loader2, Lock,
|
||||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
|
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ import { IcomPanel } from '@/components/IcomPanel';
|
|||||||
import { YaesuPanel } from '@/components/YaesuPanel';
|
import { YaesuPanel } from '@/components/YaesuPanel';
|
||||||
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
||||||
import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel';
|
import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel';
|
||||||
|
import { AmpWidget } from '@/components/AmpWidget';
|
||||||
import { ScpPanel, type ScpResult } from '@/components/ScpPanel';
|
import { ScpPanel, type ScpResult } from '@/components/ScpPanel';
|
||||||
import { FilterBuilder, type QueryFilter } from '@/components/FilterBuilder';
|
import { FilterBuilder, type QueryFilter } from '@/components/FilterBuilder';
|
||||||
import { AwardsPanel } from '@/components/AwardsPanel';
|
import { AwardsPanel } from '@/components/AwardsPanel';
|
||||||
@@ -570,6 +571,16 @@ export default function App() {
|
|||||||
// PGXL's OPERATE state comes from the radio when it reports the amp.
|
// PGXL's OPERATE state comes from the radio when it reports the amp.
|
||||||
const [ampSts, setAmpSts] = useState<any[]>([]);
|
const [ampSts, setAmpSts] = useState<any[]>([]);
|
||||||
const [flexAmp, setFlexAmp] = useState<any>(null);
|
const [flexAmp, setFlexAmp] = useState<any>(null);
|
||||||
|
// Amplifier widget: whether it is shown, and which amp — "all", or one amp id.
|
||||||
|
// Several amps side by side make a wide widget, so an operator running two
|
||||||
|
// picks the one he watches while transmitting. Declared here because the poll
|
||||||
|
// below runs faster while the widget is open.
|
||||||
|
const [showAmpWidget, setShowAmpWidget] = useState(() => localStorage.getItem('opslog.showAmpWidget') !== '0');
|
||||||
|
const [ampWidgetSel, setAmpWidgetSel] = useState(() => localStorage.getItem('opslog.ampSel.widget') || 'all');
|
||||||
|
// Poll fast only while the amplifier widget is open: its meters must track TX
|
||||||
|
// the way the FlexRadio panel's do (400 ms), or the power reads seconds behind
|
||||||
|
// the transmission and looks like the app is struggling. The status-bar chips
|
||||||
|
// alone need nothing like that rate.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
const tick = () => Promise.all([
|
const tick = () => Promise.all([
|
||||||
@@ -577,9 +588,9 @@ export default function App() {
|
|||||||
GetFlexState().catch(() => null),
|
GetFlexState().catch(() => null),
|
||||||
]).then(([l, fx]: any[]) => { if (alive) { setAmpSts((l ?? []) as any[]); setFlexAmp(fx); } });
|
]).then(([l, fx]: any[]) => { if (alive) { setAmpSts((l ?? []) as any[]); setFlexAmp(fx); } });
|
||||||
tick();
|
tick();
|
||||||
const id = window.setInterval(tick, 2000);
|
const id = window.setInterval(tick, showAmpWidget ? 400 : 2000);
|
||||||
return () => { alive = false; window.clearInterval(id); };
|
return () => { alive = false; window.clearInterval(id); };
|
||||||
}, []);
|
}, [showAmpWidget]);
|
||||||
// Multi-op "who's on air" widget: every operator's live status from the shared
|
// Multi-op "who's on air" widget: every operator's live status from the shared
|
||||||
// MySQL logbook (freq/mode/version). Only polled on a MySQL logbook.
|
// MySQL logbook (freq/mode/version). Only polled on a MySQL logbook.
|
||||||
type LiveStation = { operator: string; station: string; freq_hz: number; band: string; mode: string; online: boolean; version: string; age_sec: number };
|
type LiveStation = { operator: string; station: string; freq_hz: number; band: string; mode: string; online: boolean; version: string; age_sec: number };
|
||||||
@@ -4849,6 +4860,60 @@ export default function App() {
|
|||||||
{showTuner && tgStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />}
|
{showTuner && tgStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{/* Amplifier widget. With one amp the icon is a plain toggle; with
|
||||||
|
several it also carries a dropdown to choose which one the widget
|
||||||
|
shows (or All), since two amps stacked make a tall widget. */}
|
||||||
|
{ampSts.length > 0 && (
|
||||||
|
<div className="relative inline-flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { const v = !showAmpWidget; setShowAmpWidget(v); writeUiPref('opslog.showAmpWidget', v ? '1' : '0'); }}
|
||||||
|
title={showAmpWidget ? t('ampw.hideHint') : t('ampw.showHint')}
|
||||||
|
className={cn(
|
||||||
|
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||||
|
ampSts.length > 1 && 'rounded-r-none border-r-0',
|
||||||
|
showAmpWidget ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||||
|
: 'border-border text-muted-foreground hover:bg-muted',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Flame className="size-4" />
|
||||||
|
{showAmpWidget && ampSts.some((a: any) => a.spe?.connected || a.acom?.connected || a.pgxl?.connected || flexAmp?.amp_available) && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{ampSts.length > 1 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button type="button" title={t('ampw.pick')}
|
||||||
|
className={cn('inline-flex items-center justify-center h-7 w-4 rounded-md rounded-l-none border transition-colors',
|
||||||
|
showAmpWidget ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||||
|
: 'border-border text-muted-foreground hover:bg-muted')}>
|
||||||
|
<ChevronDown className="size-3" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="min-w-44">
|
||||||
|
{[{ id: 'all', name: t('ampw.all') }, ...ampSts.map((a: any) => ({ id: a.id, name: a.name || a.id }))].map((o) => (
|
||||||
|
<DropdownMenuItem key={o.id}
|
||||||
|
onSelect={() => {
|
||||||
|
setAmpWidgetSel(o.id);
|
||||||
|
writeUiPref('opslog.ampSel.widget', o.id);
|
||||||
|
// Picking an amp means "show me this one" — opening the
|
||||||
|
// widget too saves a second click on a hidden one.
|
||||||
|
if (!showAmpWidget) { setShowAmpWidget(true); writeUiPref('opslog.showAmpWidget', '1'); }
|
||||||
|
}}>
|
||||||
|
<span className="flex items-center gap-2 min-w-0">
|
||||||
|
{ampWidgetSel === o.id
|
||||||
|
? <Check className="size-3.5 shrink-0 text-primary" />
|
||||||
|
: <span className="size-3.5 shrink-0" />}
|
||||||
|
<span className="truncate">{o.name}</span>
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{scpEnabled && (
|
{scpEnabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -5365,7 +5430,7 @@ export default function App() {
|
|||||||
{/* Reserved free space to the right. The WinKeyer CW keyer and/or the
|
{/* Reserved free space to the right. The WinKeyer CW keyer and/or the
|
||||||
Digital Voice Keyer take this slot when enabled (Log4OM-style);
|
Digital Voice Keyer take this slot when enabled (Log4OM-style);
|
||||||
otherwise it shows the QRZ profile photo. */}
|
otherwise it shows the QRZ profile photo. */}
|
||||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showScp && scpEnabled) || (showLiveStations && dbConn?.backend === 'mysql')) && (
|
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showAmpWidget && ampSts.length > 0) || (showScp && scpEnabled) || (showLiveStations && dbConn?.backend === 'mysql')) && (
|
||||||
// relative + absolute inner (like the F1-F5 panel): a taller widget (e.g.
|
// relative + absolute inner (like the F1-F5 panel): a taller widget (e.g.
|
||||||
// the DVK with Auto CQ) can't grow the row — the row height stays set by
|
// the DVK with Auto CQ) can't grow the row — the row height stays set by
|
||||||
// the entry strip and each widget fills that height, scrolling inside.
|
// the entry strip and each widget fills that height, scrolling inside.
|
||||||
@@ -5452,6 +5517,19 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showAmpWidget && ampSts.length > 0 && (
|
||||||
|
// One column per amplifier shown, so two amps stand side by side
|
||||||
|
// rather than making the widget twice as tall as the dock row.
|
||||||
|
<div className="shrink-0 min-h-0"
|
||||||
|
style={{ width: `${Math.min(ampWidgetSel === 'all' ? ampSts.length : 1, 3) * 250 + 20}px` }}>
|
||||||
|
<AmpWidget
|
||||||
|
amps={ampSts}
|
||||||
|
flex={flexAmp}
|
||||||
|
sel={ampWidgetSel}
|
||||||
|
onClose={() => { setShowAmpWidget(false); writeUiPref('opslog.showAmpWidget', '0'); }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{showTuner && tgEnabled && (
|
{showTuner && tgEnabled && (
|
||||||
<div className="w-[230px] shrink-0 min-h-0">
|
<div className="w-[230px] shrink-0 min-h-0">
|
||||||
<TunerGeniusPanel
|
<TunerGeniusPanel
|
||||||
|
|||||||
@@ -51,11 +51,14 @@ type Amp = { id: string; name: string; type?: string; spe?: any; acom?: any; pgx
|
|||||||
|
|
||||||
export function AmpCard({ amp, flex, t }: { amp: Amp; flex: any; t: (k: string, v?: any) => string }) {
|
export function AmpCard({ amp, flex, t }: { amp: Amp; flex: any; t: (k: string, v?: any) => string }) {
|
||||||
// Peak-hold so the jittery VITA-49 meters read steadily (own ref per card).
|
// Peak-hold so the jittery VITA-49 meters read steadily (own ref per card).
|
||||||
|
// One second: the same window as the docked widget, so the two never show
|
||||||
|
// different figures for the same amplifier, and neither trails the end of a
|
||||||
|
// transmission long enough to look like lag.
|
||||||
const peak = useRef<Record<string, { v: number; t: number }>>({});
|
const peak = useRef<Record<string, { v: number; t: number }>>({});
|
||||||
const peakHold = (key: string, val: number) => {
|
const peakHold = (key: string, val: number) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const p = peak.current[key];
|
const p = peak.current[key];
|
||||||
if (!p || val >= p.v || now - p.t > 2000) { peak.current[key] = { v: val, t: now }; return val; }
|
if (!p || val >= p.v || now - p.t > 1000) { peak.current[key] = { v: val, t: now }; return val; }
|
||||||
return p.v;
|
return p.v;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,9 +76,12 @@ export function AmpCard({ amp, flex, t }: { amp: Amp; flex: any; t: (k: string,
|
|||||||
spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
||||||
{spe.operate ? 'OPERATE' : 'STANDBY'}
|
{spe.operate ? 'OPERATE' : 'STANDBY'}
|
||||||
</button>
|
</button>
|
||||||
|
{/* Power ON pulses RTS then DTR — it must stay clickable while the amp
|
||||||
|
is off (no status = not "connected"), and serial only. */}
|
||||||
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
|
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
|
||||||
<button type="button" disabled={!spe.connected}
|
<button type="button" disabled={!(spe.connected || spe.transport === 'serial')}
|
||||||
onClick={() => AmpPower(amp.id, true).catch(() => {})}
|
onClick={() => AmpPower(amp.id, true).catch(() => {})}
|
||||||
|
title={spe.transport === 'serial' ? 'Power on (RTS then DTR pulse)' : 'Power-on needs the serial RTS/DTR lines — not available over a network bridge'}
|
||||||
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
|
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
|
||||||
<button type="button" disabled={!spe.connected}
|
<button type="button" disabled={!spe.connected}
|
||||||
onClick={() => AmpPower(amp.id, false).catch(() => {})}
|
onClick={() => AmpPower(amp.id, false).catch(() => {})}
|
||||||
@@ -177,9 +183,6 @@ export function AmpCard({ amp, flex, t }: { amp: Amp; flex: any; t: (k: string,
|
|||||||
operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
||||||
{operate ? 'OPERATE' : 'STANDBY'}
|
{operate ? 'OPERATE' : 'STANDBY'}
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{operate ? t('flxp.ampInLine') : t('flxp.ampBypassed')}
|
|
||||||
</span>
|
|
||||||
{(pg.host || pg.connected) && (
|
{(pg.host || pg.connected) && (
|
||||||
<label className="flex items-center gap-1.5 text-xs" title={pg.connected ? t('flxp.pgConnected') : (pg.last_error || t('flxp.pgOffline'))}>
|
<label className="flex items-center gap-1.5 text-xs" title={pg.connected ? t('flxp.pgConnected') : (pg.last_error || t('flxp.pgOffline'))}>
|
||||||
<span className={cn('size-1.5 rounded-full', pg.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-danger')} />
|
<span className={cn('size-1.5 rounded-full', pg.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-danger')} />
|
||||||
@@ -208,6 +211,11 @@ export function AmpCard({ amp, flex, t }: { amp: Amp; flex: any; t: (k: string,
|
|||||||
const amps = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP')
|
const amps = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP')
|
||||||
&& !/^(RL|DRV)$/i.test((m.name || '').trim()));
|
&& !/^(RL|DRV)$/i.test((m.name || '').trim()));
|
||||||
if (amps.length === 0) return null;
|
if (amps.length === 0) return null;
|
||||||
|
// Power comes from the radio's meter stream and nothing else. The
|
||||||
|
// amplifier also reports a "peakfwd", and using it was a mistake twice
|
||||||
|
// over: it is a latched maximum that is never reset, and it survives in
|
||||||
|
// the last-known status after the amp disconnects — so it claimed
|
||||||
|
// 1350 W from an old transmission while the radio was putting out 10.
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2 pt-2 border-t border-border/50">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2 pt-2 border-t border-border/50">
|
||||||
{amps.map((m) => {
|
{amps.map((m) => {
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { Flame, X, Power } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { MeterBar } from '@/components/MeterBar';
|
||||||
|
import { AmpOperate, AmpPower, AmpPowerLevel, AmpFanMode, FlexAmpOperate } from '../../wailsjs/go/main/App';
|
||||||
|
|
||||||
|
// AmpWidget — the docked amplifier widget, the compact counterpart of AmpCard
|
||||||
|
// (which is the full-size card shown in the FlexRadio panel and Station
|
||||||
|
// Control). It covers all three families, since an operator running an SPE and
|
||||||
|
// a PowerGenius wants one widget, not two:
|
||||||
|
// • SPE Expert — OPERATE, power ON/OFF, L/M/H, live power / SWR / temp
|
||||||
|
// • ACOM — OPERATE, power ON/OFF, live power / SWR / temp
|
||||||
|
// • PowerGenius — OPERATE and meters ride on the FlexRadio, fan mode on GSCP
|
||||||
|
// With several amplifiers configured the caller passes a selection ("all" or an
|
||||||
|
// amp id) chosen from the toolbar icon's dropdown.
|
||||||
|
|
||||||
|
type Amp = { id: string; name: string; type?: string; spe?: any; acom?: any; pgxl?: any };
|
||||||
|
|
||||||
|
// Full-scale watts for the output bar, from the model string.
|
||||||
|
function maxW(model?: string, fallback = 1300): number {
|
||||||
|
const m = (model || '').toUpperCase();
|
||||||
|
if (m.includes('20K') || m.includes('2K')) return 2000;
|
||||||
|
if (m.includes('15K')) return 1500;
|
||||||
|
if (m.includes('13K')) return 1300;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function swrColour(swr?: number): string {
|
||||||
|
if (!swr || swr <= 0) return 'text-muted-foreground';
|
||||||
|
if (swr <= 1.5) return 'text-success';
|
||||||
|
if (swr <= 2.0) return 'text-warning';
|
||||||
|
return 'text-danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerMeter — the output-power LED bar that closes each column, straight under
|
||||||
|
// the controls (no filler above it: pushed to the bottom it left a band of dead
|
||||||
|
// space in the middle of the column). Uses the app-wide MeterBar, compact size,
|
||||||
|
// so it reads exactly like the meters on the rig panels.
|
||||||
|
function PowerMeter({ label, watts, maxWatts }: { label: string; watts: number; maxWatts: number }) {
|
||||||
|
return (
|
||||||
|
<MeterBar compact label={label} value={watts} unit="W" lo={0} hi={maxWatts}
|
||||||
|
display={`${Math.round(watts)} W`}
|
||||||
|
segColor={(f) => (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// usePeakHold keeps the highest reading seen for a short window, so a reading
|
||||||
|
// sampled between two syllables doesn't collapse the meter. `live` is the key
|
||||||
|
// part: pass false the moment the carrier drops and the held value is thrown
|
||||||
|
// away at once — holding it there made releasing the PTT look like a five
|
||||||
|
// second lag, which read as the whole app being slow.
|
||||||
|
function usePeakHold(windowMs = 1000) {
|
||||||
|
const peak = useRef<Record<string, { v: number; t: number }>>({});
|
||||||
|
return (key: string, val: number, live = true) => {
|
||||||
|
if (!live) { delete peak.current[key]; return val; }
|
||||||
|
const now = Date.now();
|
||||||
|
const p = peak.current[key];
|
||||||
|
if (!p || val >= p.v || now - p.t > windowMs) { peak.current[key] = { v: val, t: now }; return val; }
|
||||||
|
return p.v;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat — one small labelled figure (power, SWR, temperature).
|
||||||
|
function Stat({ label, value, className }: { label: string; value: string; className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-border bg-card/70 px-1.5 py-1 text-center">
|
||||||
|
<div className="text-[9px] uppercase tracking-wider text-muted-foreground leading-tight">{label}</div>
|
||||||
|
<div className={cn('text-sm font-bold font-mono leading-tight', className ?? 'text-foreground/80')}>{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperateButton — the STANDBY/OPERATE toggle, identical in behaviour across the
|
||||||
|
// three families (only where the state comes from differs).
|
||||||
|
function OperateButton({ operate, disabled, onClick, t }: {
|
||||||
|
operate: boolean; disabled: boolean; onClick: () => void; t: (k: string, v?: any) => string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button type="button" disabled={disabled} onClick={onClick}
|
||||||
|
className={cn('w-full inline-flex items-center justify-center gap-1.5 rounded-lg text-xs font-bold py-1.5 border transition-all active:scale-[0.98] disabled:opacity-40',
|
||||||
|
operate
|
||||||
|
? 'bg-gradient-to-b from-amber-400 to-amber-600 text-white border-amber-300/60 shadow-[0_0_9px_rgba(245,158,11,0.45)]'
|
||||||
|
: 'bg-card text-muted-foreground border-border hover:bg-muted hover:text-foreground')}>
|
||||||
|
<Power className="size-3.5" />
|
||||||
|
{operate ? t('ampw.operate') : t('ampw.standby')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmpBlock — one amplifier inside the widget.
|
||||||
|
function AmpBlock({ amp, flex, showName, t }: {
|
||||||
|
amp: Amp; flex: any; showName: boolean; t: (k: string, v?: any) => string;
|
||||||
|
}) {
|
||||||
|
const spe = amp.spe, acom = amp.acom;
|
||||||
|
const hold = usePeakHold();
|
||||||
|
|
||||||
|
if (spe || acom) {
|
||||||
|
const s = spe || acom;
|
||||||
|
// The amp reports zero watts on receive, so its TX flag clears the meter as
|
||||||
|
// soon as the operator lets go — and the radio's own flag, when we have one,
|
||||||
|
// gets there first (the amp is polled on its own slower cycle).
|
||||||
|
const txing = typeof flex?.transmitting === 'boolean' ? flex.transmitting : s.tx !== false;
|
||||||
|
const w = hold('w', Number(spe ? s.output_w : s.fwd_w) || 0, txing);
|
||||||
|
const swr = Number(spe ? s.swr_ant : s.swr) || 0;
|
||||||
|
const hi = spe ? maxW(s.model) : (Number(s.max_w) || 800);
|
||||||
|
// Power ON drives the remote-on control lines, so it stays available while
|
||||||
|
// the amplifier is off and reporting nothing — but only over a serial link.
|
||||||
|
const canPowerOn = spe ? (s.connected || s.transport === 'serial') : (s.port_open && s.transport === 'serial');
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col gap-1.5">
|
||||||
|
{showName && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={cn('size-1.5 rounded-full shrink-0', s.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-danger')} />
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider truncate">{amp.name || (spe ? 'SPE' : 'ACOM')}</span>
|
||||||
|
{s.connected && s.band && <span className="ml-auto text-[9px] text-muted-foreground shrink-0">{s.band}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<OperateButton operate={!!s.operate} disabled={!s.connected} t={t}
|
||||||
|
onClick={() => AmpOperate(amp.id, !s.operate).catch(() => {})} />
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<button type="button" disabled={!canPowerOn}
|
||||||
|
onClick={() => AmpPower(amp.id, true).catch(() => {})}
|
||||||
|
className="flex-1 rounded-lg border border-success/60 bg-card py-1 text-[11px] font-bold text-success hover:bg-success/15 disabled:opacity-30">
|
||||||
|
{t('ampw.on')}
|
||||||
|
</button>
|
||||||
|
<button type="button" disabled={!s.connected}
|
||||||
|
onClick={() => AmpPower(amp.id, false).catch(() => {})}
|
||||||
|
className="flex-1 rounded-lg border border-danger/60 bg-card py-1 text-[11px] font-bold text-danger hover:bg-danger/15 disabled:opacity-30">
|
||||||
|
{t('ampw.off')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Output level — SPE only; an ACOM has no L/M/H over CAT. */}
|
||||||
|
{spe && (
|
||||||
|
<div className="grid grid-cols-3 gap-1">
|
||||||
|
{(['L', 'M', 'H'] as const).map((lvl) => {
|
||||||
|
const active = (s.power_level || '').trim().toUpperCase() === lvl;
|
||||||
|
return (
|
||||||
|
<button key={lvl} type="button" disabled={!s.connected}
|
||||||
|
onClick={() => AmpPowerLevel(amp.id, lvl).catch(() => {})}
|
||||||
|
className={cn('rounded-md border py-0.5 text-[10px] font-bold transition-colors disabled:opacity-30',
|
||||||
|
active ? 'bg-primary text-primary-foreground border-primary' : 'bg-card text-muted-foreground border-border hover:bg-muted')}>
|
||||||
|
{t(`ampw.lvl${lvl}`)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{s.connected ? (
|
||||||
|
<div className="grid grid-cols-3 gap-1">
|
||||||
|
<Stat label={t('ampw.pwr')} value={`${Math.round(w)}`} />
|
||||||
|
<Stat label={t('ampw.swr')} value={swr > 0 ? swr.toFixed(1) : '—'} className={swrColour(swr)} />
|
||||||
|
<Stat label={t('ampw.temp')} value={`${Number(s.temp_c) || 0}°`} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-[10px] text-center text-muted-foreground italic py-1">{t('ampw.offline')}</div>
|
||||||
|
)}
|
||||||
|
{(s.warnings || s.alarms || s.err_text) && (
|
||||||
|
<div className="rounded-md border border-danger-border bg-danger-muted text-danger-muted-foreground text-[9px] px-1.5 py-0.5 text-center break-words">
|
||||||
|
{s.err_text || `${s.warnings || ''} ${s.alarms || ''}`.trim()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PowerMeter label={t('flxp.outputPower')} watts={s.connected ? w : 0} maxWatts={hi} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerGenius XL: OPERATE and the meters come from the radio when the Flex
|
||||||
|
// reports the amp; the fan mode rides on our own GSCP link.
|
||||||
|
const pg = amp.pgxl || {};
|
||||||
|
const viaFlex = !!flex?.amp_available;
|
||||||
|
const operate = viaFlex ? !!flex?.amp_operate : !!pg.operate;
|
||||||
|
const connected = !!pg.connected || viaFlex;
|
||||||
|
const fault = flex?.amp_fault;
|
||||||
|
const meters = ((flex?.meters as any[]) || []).filter((m) => (m.src || '').toUpperCase().includes('AMP'));
|
||||||
|
const meter = (re: RegExp) => meters.find((m) => re.test((m.name || '').trim()));
|
||||||
|
const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10);
|
||||||
|
const fwd = meter(/^fwd|pwr/i);
|
||||||
|
const flexW = fwd ? (/dbm/i.test(fwd.unit || '') ? dbmToW(fwd.value) : fwd.value) : 0;
|
||||||
|
// Whether there is power is the RADIO's transmit flag: it flips the instant
|
||||||
|
// the PTT does, where the amplifier's own status poll answers a second and a
|
||||||
|
// half late and holds TRANSMIT past the carrier.
|
||||||
|
//
|
||||||
|
// How MUCH is the live reading — the radio's meter stream, or the amp's own
|
||||||
|
// forward figure when there is no Flex. NOT the amp's "peakfwd": that is a
|
||||||
|
// latched maximum which is never reset AND survives in the last-known status
|
||||||
|
// after the amp disconnects, so it reported 1350 W from an old transmission
|
||||||
|
// while the radio was putting out 10 W into a disconnected amplifier.
|
||||||
|
const txing = typeof flex?.transmitting === 'boolean'
|
||||||
|
? flex.transmitting
|
||||||
|
: fwd ? flexW >= 1 : /TRANSMIT/i.test(pg.state || '');
|
||||||
|
const liveW = flexW > 0 ? flexW : (pg.connected ? Number(pg.fwd_w) || 0 : 0);
|
||||||
|
const fwdW = hold('fwd', txing ? liveW : 0, txing);
|
||||||
|
const id = meter(/^ID$|current/i);
|
||||||
|
// Live drain current, for the same reason: peak_id latches like peak_w.
|
||||||
|
const idA = id ? Number(id.value) : (txing ? Number(pg.id) || undefined : 0);
|
||||||
|
const temp = meter(/temp/i);
|
||||||
|
const tempC = pg.temperature > 0 ? pg.temperature : (temp ? temp.value : undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col gap-1.5">
|
||||||
|
{showName && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={cn('size-1.5 rounded-full shrink-0', connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-danger')} />
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider truncate">{amp.name || 'PGXL'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<OperateButton operate={operate} disabled={!connected} t={t}
|
||||||
|
onClick={() => (viaFlex ? FlexAmpOperate(!operate) : AmpOperate(amp.id, !operate)).catch(() => {})} />
|
||||||
|
{connected ? (
|
||||||
|
<div className="grid grid-cols-3 gap-1">
|
||||||
|
<Stat label={t('ampw.pwr')} value={`${Math.round(fwdW)}`} />
|
||||||
|
<Stat label={t('ampw.id')} value={idA != null ? idA.toFixed(1) : '—'} />
|
||||||
|
<Stat label={t('ampw.temp')} value={tempC != null ? `${Math.round(tempC)}°` : '—'} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-[10px] text-center text-muted-foreground italic py-1">{t('ampw.offline')}</div>
|
||||||
|
)}
|
||||||
|
{(pg.host || pg.connected) && (
|
||||||
|
<select
|
||||||
|
disabled={!pg.connected}
|
||||||
|
value={(pg.fan_mode || 'CONTEST').toUpperCase()}
|
||||||
|
onChange={(e) => AmpFanMode(amp.id, e.target.value).catch(() => {})}
|
||||||
|
title={t('ampw.fan')}
|
||||||
|
className="w-full h-7 rounded-md border border-border bg-card px-1.5 text-[10px] font-semibold outline-none focus:border-warning disabled:opacity-40">
|
||||||
|
<option value="STANDARD">{t('flxp.fanStandard')}</option>
|
||||||
|
<option value="CONTEST">{t('flxp.fanContest')}</option>
|
||||||
|
<option value="BROADCAST">{t('flxp.fanBroadcast')}</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{fault && fault !== 'NONE' && (
|
||||||
|
<div className="rounded-md border border-danger-border bg-danger-muted text-danger-muted-foreground text-[9px] px-1.5 py-0.5 text-center break-words">
|
||||||
|
{t('flxp.fault')}: {fault}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PowerMeter label={t('flxp.outputPower')} watts={connected ? fwdW : 0} maxWatts={2000} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AmpWidget({ amps, flex, sel, onClose }: {
|
||||||
|
amps: Amp[];
|
||||||
|
flex: any;
|
||||||
|
sel: string; // "all" or an amp id
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
// An id that no longer matches (the amp was removed in Settings) falls back to
|
||||||
|
// showing everything rather than an empty widget.
|
||||||
|
const shown = sel === 'all' ? amps : (amps.filter((a) => a.id === sel).length ? amps.filter((a) => a.id === sel) : amps);
|
||||||
|
const anyOnline = shown.some((a) => a.spe?.connected || a.acom?.connected || a.pgxl?.connected || flex?.amp_available);
|
||||||
|
const title = shown.length === 1 ? (shown[0].name || t('ampw.title')) : t('ampw.title');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col rounded-xl border border-border bg-gradient-to-b from-card to-muted/30 shadow-sm overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/40 shrink-0">
|
||||||
|
<Flame className={cn('size-4', anyOnline ? 'text-warning drop-shadow-[0_0_3px_rgba(245,158,11,0.55)]' : 'text-muted-foreground')} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-[0.18em] text-foreground/80 truncate">{title}</span>
|
||||||
|
<span className="flex-1" />
|
||||||
|
<button type="button" onClick={onClose} className="ml-1 text-muted-foreground hover:text-foreground transition-colors" title={t('ampw.close')}>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Several amplifiers sit SIDE BY SIDE, one column each: stacked, two amps
|
||||||
|
made a widget taller than the dock row and both ended up half hidden. */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto p-2.5">
|
||||||
|
{shown.length === 0 ? (
|
||||||
|
<div className="text-[11px] text-center text-muted-foreground italic py-2">{t('ampw.none')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-2.5 h-full" style={{ gridTemplateColumns: `repeat(${shown.length}, minmax(0, 1fr))` }}>
|
||||||
|
{shown.map((a, i) => (
|
||||||
|
<div key={a.id} className={cn(i > 0 && 'pl-2.5 border-l border-border/50')}>
|
||||||
|
<AmpBlock amp={a} flex={flex} showName={shown.length > 1} t={t} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -69,6 +69,9 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
|
const [awardList, setAwardList] = useState<AwardListItem[]>([]);
|
||||||
// Computed results are cached per award code — each award is scanned only the
|
// Computed results are cached per award code — each award is scanned only the
|
||||||
// first time it's selected (or when explicitly rescanned).
|
// first time it's selected (or when explicitly rescanned).
|
||||||
|
// Keyed by "CODE|MODECLASS": the same award computed for CW and for all modes
|
||||||
|
// are different results, and caching them under one key showed the previous
|
||||||
|
// mode’s numbers after switching.
|
||||||
const [byCode, setByCode] = useState<Record<string, AwardResult>>({});
|
const [byCode, setByCode] = useState<Record<string, AwardResult>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [err, setErr] = useState('');
|
const [err, setErr] = useState('');
|
||||||
@@ -119,13 +122,14 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
// Compute one award (cached). force=true bypasses the cache (Rescan).
|
// Compute one award (cached). force=true bypasses the cache (Rescan).
|
||||||
async function compute(code: string, force = false) {
|
async function compute(code: string, force = false) {
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
if (!force && byCode[code]) { setSelected(code); return; }
|
const key = `${code}|${modeFilter}`;
|
||||||
|
if (!force && byCode[key]) { setSelected(code); return; }
|
||||||
setSelected(code);
|
setSelected(code);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr('');
|
setErr('');
|
||||||
try {
|
try {
|
||||||
const r = (await GetAward(code)) as any as AwardResult;
|
const r = (await GetAward(code, modeFilter === 'all' ? '' : modeFilter)) as any as AwardResult;
|
||||||
setByCode((m) => ({ ...m, [code]: r }));
|
setByCode((m) => ({ ...m, [key]: r }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setErr(String(e?.message ?? e));
|
setErr(String(e?.message ?? e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -149,7 +153,10 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
}
|
}
|
||||||
useEffect(() => { loadList(); }, []);
|
useEffect(() => { loadList(); }, []);
|
||||||
|
|
||||||
const current = byCode[selected];
|
const current = byCode[`${selected}|${modeFilter}`];
|
||||||
|
// Recompute when the mode class changes: the bands, counts and confirmations
|
||||||
|
// all describe the selected mode, so they cannot be filtered client-side.
|
||||||
|
useEffect(() => { if (selected) compute(selected); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [modeFilter]);
|
||||||
|
|
||||||
// Bands relevant to the selected award, used by BOTH the grid and the stats
|
// Bands relevant to the selected award, used by BOTH the grid and the stats
|
||||||
// matrix so neither is padded with bands the award doesn't use. Rule: the
|
// matrix so neither is padded with bands the award doesn't use. Rule: the
|
||||||
@@ -292,7 +299,7 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{err && <div className="p-3 text-xs text-destructive">{err}</div>}
|
{err && <div className="p-3 text-xs text-destructive">{err}</div>}
|
||||||
{awardList.map((a) => {
|
{awardList.map((a) => {
|
||||||
const r = byCode[a.code];
|
const r = byCode[`${a.code}|${modeFilter}`];
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={a.code}
|
key={a.code}
|
||||||
@@ -539,7 +546,7 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cell && current && (
|
{cell && current && (
|
||||||
<CellQSOModal code={current.code} cell={cell} onClose={() => setCell(null)} />
|
<CellQSOModal code={current.code} cell={cell} modeClass={modeFilter === 'all' ? '' : modeFilter} onClose={() => setCell(null)} />
|
||||||
)}
|
)}
|
||||||
{showMissing && current && (
|
{showMissing && current && (
|
||||||
<MissingQSOModal code={current.code} name={current.name} onClose={() => setShowMissing(false)} onEditQSO={onEditQSO} />
|
<MissingQSOModal code={current.code} name={current.name} onClose={() => setShowMissing(false)} onEditQSO={onEditQSO} />
|
||||||
@@ -724,17 +731,17 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CellQSOModal lists the QSOs behind one award-grid cell (reference × band).
|
// CellQSOModal lists the QSOs behind one award-grid cell (reference × band).
|
||||||
function CellQSOModal({ code, cell, onClose }: { code: string; cell: { ref: string; band: string; name?: string }; onClose: () => void }) {
|
function CellQSOModal({ code, cell, modeClass, onClose }: { code: string; cell: { ref: string; band: string; name?: string }; modeClass: string; onClose: () => void }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [qsos, setQsos] = useState<any[]>([]);
|
const [qsos, setQsos] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
AwardCellQSOs(code, cell.ref, cell.band)
|
AwardCellQSOs(code, cell.ref, cell.band, modeClass)
|
||||||
.then((r) => setQsos((r ?? []) as any))
|
.then((r) => setQsos((r ?? []) as any))
|
||||||
.catch(() => setQsos([]))
|
.catch(() => setQsos([]))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [code, cell.ref, cell.band]);
|
}, [code, cell.ref, cell.band, modeClass]);
|
||||||
|
|
||||||
const fmt = (s: any) => { const d = new Date(s); return isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 16).replace('T', ' '); };
|
const fmt = (s: any) => { const d = new Date(s); return isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 16).replace('T', ' '); };
|
||||||
|
|
||||||
|
|||||||
@@ -323,13 +323,15 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [st.cw_speed]);
|
}, [st.cw_speed]);
|
||||||
// Peak-hold: keep the highest reading for ~2 s so the jittery VITA-49 meters
|
// Peak-hold: keep the highest reading for a second so the jittery VITA-49
|
||||||
// read steadily instead of jumping every poll.
|
// meters read steadily instead of jumping every poll. One second, not two —
|
||||||
|
// longer and the meters visibly trail the end of a transmission, which reads
|
||||||
|
// as the app lagging rather than as a peak being held.
|
||||||
const peak = useRef<Record<string, { v: number; t: number }>>({});
|
const peak = useRef<Record<string, { v: number; t: number }>>({});
|
||||||
const peakHold = (key: string, val: number) => {
|
const peakHold = (key: string, val: number, windowMs = 1000) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const p = peak.current[key];
|
const p = peak.current[key];
|
||||||
if (!p || val >= p.v || now - p.t > 2000) { peak.current[key] = { v: val, t: now }; return val; }
|
if (!p || val >= p.v || now - p.t > windowMs) { peak.current[key] = { v: val, t: now }; return val; }
|
||||||
return p.v;
|
return p.v;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -355,7 +357,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// PowerGenius XL direct connection (fan mode), independent of the Flex link.
|
// PowerGenius XL direct connection (fan mode), independent of the Flex link.
|
||||||
const [pg, setPg] = useState<{ connected: boolean; fan_mode?: string; host?: string; last_error?: string }>({ connected: false });
|
const [pg, setPg] = useState<{ connected: boolean; fan_mode?: string; host?: string; last_error?: string; state?: string; peak_w?: number }>({ connected: false });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
const tick = async () => { try { const s: any = await GetPGXLStatus(); if (alive) setPg(s || { connected: false }); } catch {} };
|
const tick = async () => { try { const s: any = await GetPGXLStatus(); if (alive) setPg(s || { connected: false }); } catch {} };
|
||||||
@@ -978,10 +980,12 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
||||||
{spe.operate ? 'OPERATE' : 'STANDBY'}
|
{spe.operate ? 'OPERATE' : 'STANDBY'}
|
||||||
</button>
|
</button>
|
||||||
{/* Power ON / OFF (best-guess keystrokes from the APG — verify on hw). */}
|
{/* Power ON pulses RTS then DTR — it must stay clickable while the amp
|
||||||
|
is off (no status = not "connected"), and serial only. */}
|
||||||
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
|
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
|
||||||
<button type="button" disabled={!spe.connected}
|
<button type="button" disabled={!(spe.connected || spe.transport === 'serial')}
|
||||||
onClick={() => AmpPower(selAmp.id, true).catch(() => {})}
|
onClick={() => AmpPower(selAmp.id, true).catch(() => {})}
|
||||||
|
title={spe.transport === 'serial' ? 'Power on (RTS then DTR pulse)' : 'Power-on needs the serial RTS/DTR lines — not available over a network bridge'}
|
||||||
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
|
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
|
||||||
<button type="button" disabled={!spe.connected}
|
<button type="button" disabled={!spe.connected}
|
||||||
onClick={() => AmpPower(selAmp.id, false).catch(() => {})}
|
onClick={() => AmpPower(selAmp.id, false).catch(() => {})}
|
||||||
@@ -1083,9 +1087,6 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
st.amp_operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
st.amp_operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
||||||
{st.amp_operate ? 'OPERATE' : 'STANDBY'}
|
{st.amp_operate ? 'OPERATE' : 'STANDBY'}
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{st.amp_operate ? t('flxp.ampInLine') : t('flxp.ampBypassed')}
|
|
||||||
</span>
|
|
||||||
{/* Fan mode — shown when the PowerGenius is configured (Settings →
|
{/* Fan mode — shown when the PowerGenius is configured (Settings →
|
||||||
PowerGenius). The dot shows the direct-connection state; the
|
PowerGenius). The dot shows the direct-connection state; the
|
||||||
selector is disabled until connected (hover it for the error). */}
|
selector is disabled until connected (hover it for the error). */}
|
||||||
@@ -1118,6 +1119,12 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
const amp = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP')
|
const amp = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP')
|
||||||
&& !/^(RL|DRV)$/i.test((m.name || '').trim()));
|
&& !/^(RL|DRV)$/i.test((m.name || '').trim()));
|
||||||
if (off || amp.length === 0) return null;
|
if (off || amp.length === 0) return null;
|
||||||
|
// Power comes from the radio's meter stream and nothing else. The
|
||||||
|
// amplifier also reports a "peakfwd", and using it was a mistake
|
||||||
|
// twice over: it is a latched maximum that is never reset, and it
|
||||||
|
// survives in the last-known status after the amp disconnects — so
|
||||||
|
// it claimed 1350 W from an old transmission while the radio was
|
||||||
|
// putting out 10.
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2 pt-2 border-t border-border/50">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2 pt-2 border-t border-border/50">
|
||||||
{amp.map((m) => {
|
{amp.map((m) => {
|
||||||
|
|||||||
@@ -1136,7 +1136,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,
|
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,
|
||||||
yaesu_port: '', yaesu_baud: 38400, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', 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: '', 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,
|
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
||||||
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
||||||
@@ -2580,6 +2580,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
</Select>
|
</Select>
|
||||||
<span className="text-xs text-muted-foreground">{t('cat.yaesuBaudHint')}</span>
|
<span className="text-xs text-muted-foreground">{t('cat.yaesuBaudHint')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||||
|
<Checkbox checked={!!catCfg.yaesu_low_lines} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, yaesu_low_lines: !!c }))} />
|
||||||
|
{t('cat.lowerLines')}
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{['icom', 'xiegu', 'kenwood'].includes(catCfg.backend) && (
|
{['icom', 'xiegu', 'kenwood'].includes(catCfg.backend) && (
|
||||||
@@ -2627,6 +2634,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">{t('cat.kenwoodHostHint')}</span>
|
<span className="text-xs text-muted-foreground">{t('cat.kenwoodHostHint')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||||
|
<Checkbox checked={!!catCfg.kenwood_low_lines} onCheckedChange={(c) => setCatCfg((s) => ({ ...s, kenwood_low_lines: !!c }))} />
|
||||||
|
{t('cat.lowerLines')}
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-muted-foreground">{t('cat.lowerLinesHint')}</span>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{catCfg.backend === 'icom' && (
|
{catCfg.backend === 'icom' && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { Plus, Pencil, Trash2, Power, PlugZap, Loader2, Check, X, Compass, Square, Antenna as AntennaIcon, ArrowDownToLine, Minus, RefreshCw, GripVertical } from 'lucide-react';
|
import { Plus, Pencil, Trash2, Power, PlugZap, Loader2, Check, X, Compass, Square, Antenna as AntennaIcon, ArrowDownToLine, Minus, RefreshCw, GripVertical } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -26,6 +26,13 @@ type Device = { id: string; type: string; name: string; host: string; user?: str
|
|||||||
type Relay = { number: number; label: string; on: boolean };
|
type Relay = { number: number; label: string; on: boolean };
|
||||||
type DevStatus = { id: string; name: string; type: string; connected: boolean; error?: string; relays: Relay[] };
|
type DevStatus = { id: string; name: string; type: string; connected: boolean; error?: string; relays: Relay[] };
|
||||||
|
|
||||||
|
// Dashboard geometry: a grid of EQUAL columns whose cards also share a common
|
||||||
|
// HEIGHT per row — every card stretches to the tallest one beside it. Masonry
|
||||||
|
// packed tighter but left the row edges ragged, which read as untidy on a wide
|
||||||
|
// screen; aligned rows are what an operator expects from a dashboard.
|
||||||
|
const CARD_MIN = 430; // narrowest a card may get before "Auto" drops a column
|
||||||
|
const GRID_GAP = 16; // px between cards, both axes
|
||||||
|
|
||||||
const RELAY_COUNT: Record<string, number> = { webswitch: 5, kmtronic: 8, denkovi: 8, usbrelay: 8 };
|
const RELAY_COUNT: Record<string, number> = { webswitch: 5, kmtronic: 8, denkovi: 8, usbrelay: 8 };
|
||||||
const TYPE_LABEL: Record<string, string> = { webswitch: 'WebSwitch 1216H', kmtronic: 'KMTronic 8-relay', denkovi: 'Denkovi USB (FT245)', usbrelay: 'Denkovi USB (serial)' };
|
const TYPE_LABEL: Record<string, string> = { webswitch: 'WebSwitch 1216H', kmtronic: 'KMTronic 8-relay', denkovi: 'Denkovi USB (FT245)', usbrelay: 'Denkovi USB (serial)' };
|
||||||
|
|
||||||
@@ -269,6 +276,23 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
|||||||
// Max columns per row (persisted). "Auto" fills the window; a number caps the
|
// Max columns per row (persisted). "Auto" fills the window; a number caps the
|
||||||
// row so cards wrap onto further lines even when there's horizontal room.
|
// row so cards wrap onto further lines even when there's horizontal room.
|
||||||
const [cols, setCols] = useState<string>(() => localStorage.getItem('opslog.stationCols') || 'auto');
|
const [cols, setCols] = useState<string>(() => localStorage.getItem('opslog.stationCols') || 'auto');
|
||||||
|
|
||||||
|
// How many columns the dashboard shows. "Auto" fits as many CARD_MIN-wide
|
||||||
|
// cards as the window allows; an explicit count is honoured as asked, even
|
||||||
|
// past that width — the operator can see his own screen.
|
||||||
|
const gridRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [colCount, setColCount] = useState(1);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (cols !== 'auto') { setColCount(Number(cols) || 1); return; }
|
||||||
|
const el = gridRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const measure = () => setColCount(Math.max(1, Math.floor((el.clientWidth + GRID_GAP) / (CARD_MIN + GRID_GAP))));
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, [cols]);
|
||||||
// Amplifiers (Settings → Amplifier): shown here so operators without a
|
// Amplifiers (Settings → Amplifier): shown here so operators without a
|
||||||
// FlexRadio panel still get the controls. EVERY configured amp gets its own
|
// FlexRadio panel still get the controls. EVERY configured amp gets its own
|
||||||
// card (identical to the Flex panel's) — no dropdown. Polled fast (1.5s) so the
|
// card (identical to the Flex panel's) — no dropdown. Polled fast (1.5s) so the
|
||||||
@@ -415,10 +439,10 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// `wide` cards span the whole dashboard instead of sitting in one masonry
|
// `wide` cards take two grid columns instead of one. The amplifier and tuner
|
||||||
// column. The amplifier and tuner carry meter rows and a channel selector that
|
// carry meter rows and a channel selector that are unreadable squeezed into a
|
||||||
// are unreadable squeezed into a ~430px column — they are the same cards the
|
// single ~430px column — they are the same cards the FlexRadio panel shows
|
||||||
// FlexRadio panel shows full-width, and they need that room here too.
|
// full-width, and they need that room here too.
|
||||||
const widgets: { id: string; node: React.ReactNode; wide?: boolean }[] = [];
|
const widgets: { id: string; node: React.ReactNode; wide?: boolean }[] = [];
|
||||||
if (rot.enabled) {
|
if (rot.enabled) {
|
||||||
widgets.push({ id: 'rotator', node: <RotatorWidget hd={rot} refetch={pollRot} centerLat={centerLat} centerLon={centerLon} bearing={bearing} t={t} /> });
|
widgets.push({ id: 'rotator', node: <RotatorWidget hd={rot} refetch={pollRot} centerLat={centerLat} centerLon={centerLon} bearing={bearing} t={t} /> });
|
||||||
@@ -446,7 +470,7 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
|||||||
{/* Max columns per row: Auto fills the window; 1/2/3/4 cap the row so a
|
{/* Max columns per row: Auto fills the window; 1/2/3/4 cap the row so a
|
||||||
card can sit on a further line even with horizontal room to spare. */}
|
card can sit on a further line even with horizontal room to spare. */}
|
||||||
<div className="flex items-center rounded-md border border-border overflow-hidden text-[11px]">
|
<div className="flex items-center rounded-md border border-border overflow-hidden text-[11px]">
|
||||||
{(['auto', '1', '2', '3', '4'] as const).map((c) => (
|
{(['auto', '1', '2', '3', '4', '5', '6'] as const).map((c) => (
|
||||||
<button key={c} type="button"
|
<button key={c} type="button"
|
||||||
onClick={() => { setCols(c); writeUiPref('opslog.stationCols', c); }}
|
onClick={() => { setCols(c); writeUiPref('opslog.stationCols', c); }}
|
||||||
className={cn('px-2 py-1 font-medium', cols === c ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
|
className={cn('px-2 py-1 font-medium', cols === c ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
|
||||||
@@ -466,22 +490,26 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Masonry dashboard: fixed-width cards flow into balanced CSS columns so
|
{/* Dashboard grid: equal columns, and every card stretches to the height of
|
||||||
they pack tightly by height (no ragged gaps under short cards). "Auto"
|
the tallest one on its row, so the rows line up instead of ending
|
||||||
fits as many ~430px columns as the window allows; a fixed count caps the
|
ragged. One column each, two for the amplifier/tuner whose meter rows
|
||||||
container width to that many columns. Each card has a grip handle (left
|
need the room. Each card has a grip handle (left rail) as the drag
|
||||||
rail) as the drag initiator (the body is full of buttons). */}
|
initiator, since the body is full of buttons. */}
|
||||||
<div style={{ columnWidth: '430px', columnGap: '1rem', columnFill: 'balance',
|
<div ref={gridRef} style={{
|
||||||
...(cols !== 'auto' ? { maxWidth: `${Number(cols) * 446}px` } : {}) }}>
|
display: 'grid',
|
||||||
|
gridTemplateColumns: `repeat(${colCount}, minmax(0, 1fr))`,
|
||||||
|
gap: `${GRID_GAP}px`,
|
||||||
|
}}>
|
||||||
{ordered.map((w) => (
|
{ordered.map((w) => (
|
||||||
// column-span:all lifts a wide card out of the columns and across the
|
<div key={w.id}
|
||||||
// full container, while keeping it in document order — so drag-reorder
|
// A wide card takes two columns — but never more than there are, or
|
||||||
// still works between wide and normal cards. Capped so it stays a card
|
// the grid would grow a phantom column on a narrow window.
|
||||||
// and not a banner on an ultra-wide window.
|
style={{ gridColumn: w.wide ? `span ${Math.min(2, colCount)}` : undefined }}
|
||||||
<div key={w.id} className="flex items-stretch break-inside-avoid mb-4"
|
|
||||||
style={w.wide ? { columnSpan: 'all', maxWidth: '900px' } : undefined}
|
|
||||||
onDragOver={(e) => { if (dragId.current) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }}
|
onDragOver={(e) => { if (dragId.current) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }}
|
||||||
onDrop={(e) => { if (dragId.current) { e.preventDefault(); onDrop(w.id); } }}>
|
onDrop={(e) => { if (dragId.current) { e.preventDefault(); onDrop(w.id); } }}>
|
||||||
|
{/* h-full down the chain is what makes the card fill the row height
|
||||||
|
rather than sit at its natural size in a taller cell. */}
|
||||||
|
<div className="flex items-stretch h-full">
|
||||||
<div draggable
|
<div draggable
|
||||||
onDragStart={(e) => { dragId.current = w.id; e.dataTransfer.effectAllowed = 'move'; }}
|
onDragStart={(e) => { dragId.current = w.id; e.dataTransfer.effectAllowed = 'move'; }}
|
||||||
onDragEnd={() => { dragId.current = null; }}
|
onDragEnd={() => { dragId.current = null; }}
|
||||||
@@ -490,7 +518,8 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
|||||||
dragId.current === w.id && 'opacity-60')}>
|
dragId.current === w.id && 'opacity-60')}>
|
||||||
<GripVertical className="size-4" />
|
<GripVertical className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">{w.node}</div>
|
<div className="flex-1 min-w-0 [&>*]:h-full">{w.node}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,13 +76,16 @@ export function TunerCard({ status, t }: { status: TGStatus; t: (k: string, v?:
|
|||||||
const peakHold = (key: string, val: number) => {
|
const peakHold = (key: string, val: number) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const p = peak.current[key];
|
const p = peak.current[key];
|
||||||
if (!p || val >= p.v || now - p.t > 2000) { peak.current[key] = { v: val, t: now }; return val; }
|
if (!p || val >= p.v || now - p.t > 1000) { peak.current[key] = { v: val, t: now }; return val; }
|
||||||
return p.v;
|
return p.v;
|
||||||
};
|
};
|
||||||
|
|
||||||
const connected = !!status.connected;
|
const connected = !!status.connected;
|
||||||
const rawVswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
|
const rawVswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
|
||||||
const rawFwdW = status.fwd_w && status.fwd_w >= 1 ? status.fwd_w : 0;
|
// Prefer the device's own peak detector over the sampled reading; the JS hold
|
||||||
|
// below still smooths what is left between polls.
|
||||||
|
const rawPeak = Math.max(status.peak_w ?? 0, status.fwd_w ?? 0);
|
||||||
|
const rawFwdW = rawPeak >= 1 ? rawPeak : 0;
|
||||||
const fwdW = peakHold('fwd', rawFwdW);
|
const fwdW = peakHold('fwd', rawFwdW);
|
||||||
|
|
||||||
// SWR needs RF to mean anything: sampled on receive the device reports a
|
// SWR needs RF to mean anything: sampled on receive the device reports a
|
||||||
@@ -93,13 +96,14 @@ export function TunerCard({ status, t }: { status: TGStatus; t: (k: string, v?:
|
|||||||
// That expiry is the point: the first version of this held the last TX value
|
// That expiry is the point: the first version of this held the last TX value
|
||||||
// with no timeout at all, so the meter sat frozen on the previous transmission
|
// with no timeout at all, so the meter sat frozen on the previous transmission
|
||||||
// indefinitely — reading 1.39:1 with the radio plainly in RX and 0 W forward.
|
// indefinitely — reading 1.39:1 with the radio plainly in RX and 0 W forward.
|
||||||
// Same 2 s window as the power peak above, so the two meters clear together.
|
// Same one-second window as the power peak above, so the two meters clear
|
||||||
|
// together.
|
||||||
const swrHold = useRef<{ v: number; t: number } | null>(null);
|
const swrHold = useRef<{ v: number; t: number } | null>(null);
|
||||||
if (rawFwdW >= 1 && rawVswr) swrHold.current = { v: rawVswr, t: Date.now() };
|
if (rawFwdW >= 1 && rawVswr) swrHold.current = { v: rawVswr, t: Date.now() };
|
||||||
const heldSwr = swrHold.current;
|
const heldSwr = swrHold.current;
|
||||||
const vswr = rawFwdW >= 1
|
const vswr = rawFwdW >= 1
|
||||||
? rawVswr
|
? rawVswr
|
||||||
: heldSwr && Date.now() - heldSwr.t < 2000
|
: heldSwr && Date.now() - heldSwr.t < 1000
|
||||||
? heldSwr.v
|
? heldSwr.v
|
||||||
: undefined;
|
: undefined;
|
||||||
const active = status.active ?? 1;
|
const active = status.active ?? 1;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type TGChannel = {
|
|||||||
};
|
};
|
||||||
export type TGStatus = {
|
export type TGStatus = {
|
||||||
connected: boolean; host?: string; last_error?: string;
|
connected: boolean; host?: string; last_error?: string;
|
||||||
fwd_dbm?: number; fwd_w?: number; swr_db?: number; vswr?: number; freq_mhz?: number;
|
fwd_dbm?: number; fwd_w?: number; peak_w?: number; swr_db?: number; vswr?: number; freq_mhz?: number;
|
||||||
operate?: boolean; bypass?: boolean; tuning?: boolean; active?: number;
|
operate?: boolean; bypass?: boolean; tuning?: boolean; active?: number;
|
||||||
antenna?: number; three_way?: boolean; message?: string;
|
antenna?: number; three_way?: boolean; message?: string;
|
||||||
a?: TGChannel; b?: TGChannel;
|
a?: TGChannel; b?: TGChannel;
|
||||||
@@ -69,6 +69,10 @@ export function TunerGeniusPanel({ status, onTune, onBypass, onOperate, onActiva
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const vswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
|
const vswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
|
||||||
const active = status.active ?? 1;
|
const active = status.active ?? 1;
|
||||||
|
// The device's own peak, not the instantaneous reading: a 400 ms poll lands
|
||||||
|
// between syllables as often as on a peak, so the plain figure showed a few
|
||||||
|
// hundred watts on a kilowatt transmission.
|
||||||
|
const fwdW = Math.max(status.peak_w ?? 0, status.fwd_w ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col rounded-xl border border-border bg-gradient-to-b from-card to-muted/30 shadow-sm overflow-hidden">
|
<div className="h-full flex flex-col rounded-xl border border-border bg-gradient-to-b from-card to-muted/30 shadow-sm overflow-hidden">
|
||||||
@@ -103,7 +107,7 @@ export function TunerGeniusPanel({ status, onTune, onBypass, onOperate, onActiva
|
|||||||
<div className="rounded-lg border border-border bg-card/70 px-2 py-1.5 text-center">
|
<div className="rounded-lg border border-border bg-card/70 px-2 py-1.5 text-center">
|
||||||
<div className="text-[9px] uppercase tracking-wider text-muted-foreground">{t('tgp.power')}</div>
|
<div className="text-[9px] uppercase tracking-wider text-muted-foreground">{t('tgp.power')}</div>
|
||||||
<div className="text-lg font-bold font-mono leading-tight text-foreground/80">
|
<div className="text-lg font-bold font-mono leading-tight text-foreground/80">
|
||||||
{status.fwd_w && status.fwd_w >= 1 ? `${Math.round(status.fwd_w)} W` : '—'}
|
{fwdW >= 1 ? `${Math.round(fwdW)} W` : '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ const en: Dict = {
|
|||||||
'extsvc.hint': 'Upload logged QSOs to online logbooks. Each service uploads automatically on a new QSO when enabled; timing is per-service (immediate, or a 1–2 min delay so a mis-logged QSO can still be fixed first).',
|
'extsvc.hint': 'Upload logged QSOs to online logbooks. Each service uploads automatically on a new QSO when enabled; timing is per-service (immediate, or a 1–2 min delay so a mis-logged QSO can still be fixed first).',
|
||||||
'hw.motorTxInhibit': 'Inhibit transmission while the antenna is moving', 'hw.motorTxInhibitHint': 'Blocks the FlexRadio from transmitting while the elements move (needs FlexRadio in API mode + this antenna enabled). No effect with other radios.', 'hw.motorAntenna': 'Ultrabeam / Steppir', 'hw.motorEnable': 'Enable antenna control', 'hw.motorType': 'Antenna type', 'hw.motorTransport': 'Connection', 'hw.motorTcp': 'Network (TCP)', 'hw.motorSerial': 'Serial (COM)', 'hw.motorCom': 'Serial port', 'hw.motorBaud': 'Baud', 'hw.steppirHint': 'SteppIR controllers are RS-232 serial (the DATA OUT DB9 port). Serial = a USB↔RS-232 (FTDI) adapter, shown as a COM port. Network = a serial-to-Ethernet bridge (as for the Ultrabeam).', 'hw.steppirRange': 'Tunable range', 'hw.steppirRangeHint': "The SteppIR's frequency coverage. On a band outside this range (e.g. 30 m on a 20 m–6 m SteppIR) OpsLog won't try to tune the antenna and won't inhibit transmission. Default 13–54 MHz (20 m–6 m); widen the low edge (e.g. 6) for a 40 m-equipped SteppIR.", 'hw.ultrabeam': 'Antenna (Ultrabeam)', 'hw.audioVoice': 'Audio devices & voice keyer',
|
'hw.motorTxInhibit': 'Inhibit transmission while the antenna is moving', 'hw.motorTxInhibitHint': 'Blocks the FlexRadio from transmitting while the elements move (needs FlexRadio in API mode + this antenna enabled). No effect with other radios.', 'hw.motorAntenna': 'Ultrabeam / Steppir', 'hw.motorEnable': 'Enable antenna control', 'hw.motorType': 'Antenna type', 'hw.motorTransport': 'Connection', 'hw.motorTcp': 'Network (TCP)', 'hw.motorSerial': 'Serial (COM)', 'hw.motorCom': 'Serial port', 'hw.motorBaud': 'Baud', 'hw.steppirHint': 'SteppIR controllers are RS-232 serial (the DATA OUT DB9 port). Serial = a USB↔RS-232 (FTDI) adapter, shown as a COM port. Network = a serial-to-Ethernet bridge (as for the Ultrabeam).', 'hw.steppirRange': 'Tunable range', 'hw.steppirRangeHint': "The SteppIR's frequency coverage. On a band outside this range (e.g. 30 m on a 20 m–6 m SteppIR) OpsLog won't try to tune the antenna and won't inhibit transmission. Default 13–54 MHz (20 m–6 m); widen the low edge (e.g. 6) for a 40 m-equipped SteppIR.", 'hw.ultrabeam': 'Antenna (Ultrabeam)', 'hw.audioVoice': 'Audio devices & voice keyer',
|
||||||
// CAT panel body
|
// CAT panel body
|
||||||
'cat.enable': 'Enable CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig', 'cat.optFlex': 'FlexRadio (API)', 'cat.share': 'Share CAT with other programs', 'cat.shareHint': 'A native CAT backend owns the radio serial port, so no other program can reach it. This lets WSJT-X, JTDX, MSHV or Log4OM talk to the rig THROUGH OpsLog: in the other program pick the rig model "Hamlib NET rigctl" and enter 127.0.0.1:4532. Works with every backend, not only the native ones.', 'cat.sharePort': 'Sharing port', 'cat.optXiegu': 'Xiegu (USB)', 'cat.xieguPort': 'Xiegu CAT port', 'cat.xieguBaudHint': 'Must match the radio menu (G90 default: 19200).', 'cat.xieguAddrHint': 'Factory CI-V address of the G90/X6100 family: 0x70.', 'cat.xieguPTTLine': 'How the rig is keyed', 'cat.xieguPTTCiv': 'CI-V command', 'cat.xieguPTTHint': 'A G90 does not transmit on the CI-V command: interfaces like the DE-19 key it on RTS or DTR. Pick the line yours uses \u2014 it is also what lets WSJT-X transmit through the shared CAT link.', 'cat.optYaesu': 'Yaesu (USB)', 'cat.optKenwood': 'Kenwood (USB, network)', 'cat.civTrace': 'Log the CAT protocol', 'cat.civTraceHint': 'Writes every CAT frame to and from the radio into the log \u2014 CI-V as hex, Kenwood as the text it exchanges. For reporting a rig that answers oddly \u2014 a button that does the wrong thing, a frequency that jumps. Session only: it is a diagnostic, and it makes the log large.', 'cat.kenwoodPort': 'Kenwood COM port', 'cat.kenwoodPortHint': 'The rig\u2019s CAT/USB serial port (TS-590, TS-890, TS-990, TS-2000, and Elecraft K3/K4 which speak the same dialect).', 'cat.kenwoodBaudHint': 'Must match MENU on the radio: a TS-590 leaves the factory at 9600, a TS-890 at 115200.', 'cat.kenwoodHost': 'Or over the network (host:port)', 'cat.kenwoodHostHint': 'A serial-over-network bridge \u2014 ser2net, an Ethernet-serial adapter, a Raspberry Pi at the radio. Filled in, it is used INSTEAD of the COM port above. This is not the radio\u2019s own RJ45 socket, which speaks Kenwood\u2019s KNS protocol and is not supported.', 'cat.yaesuPort': 'Yaesu CAT port', 'cat.yaesuPortHint': 'The rig CAT port — on an FTDX10/FTDX101 over USB this is the ENHANCED COM port, not the standard one.', 'cat.yaesuBaudHint': 'Must match the radio menu (FTDX10/FTDX101 default: 38400).', 'cat.optIcom': 'Icom (CI-V USB)', 'cat.optIcomNet': 'Icom (CI-V network)', 'cat.optTci': 'TCI', 'flxpw.title': 'FlexRadio \u2014 power per band and mode', 'flxpw.hint': 'TX power applied when the band or mode changes. Leave a box empty to leave the power alone.', 'flxpw.band': 'Band', 'flxb.title': 'FlexRadio \u2014 per-band antennas and power', 'flxb.hint': 'Antennas are applied when the band changes; power when the band or the mode changes. Leave a power box empty to leave it alone.', 'flxb.rxAnt': 'RX antenna', 'flxb.txAnt': 'TX antenna', 'flxb.noRadio': 'No antennas reported yet \u2014 connect the FlexRadio, then reopen this panel.', 'flxpw.phone': 'Phone', 'flxpw.digi': 'Digital', 'flxpw.noBands': 'No bands configured \u2014 add them in Lists \u2192 Bands.', 'flxpw.saved': 'Saved',
|
'cat.enable': 'Enable CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig', 'cat.optFlex': 'FlexRadio (API)', 'cat.share': 'Share CAT with other programs', 'cat.shareHint': 'A native CAT backend owns the radio serial port, so no other program can reach it. This lets WSJT-X, JTDX, MSHV or Log4OM talk to the rig THROUGH OpsLog: in the other program pick the rig model "Hamlib NET rigctl" and enter 127.0.0.1:4532. Works with every backend, not only the native ones.', 'cat.sharePort': 'Sharing port', 'cat.optXiegu': 'Xiegu (USB)', 'cat.xieguPort': 'Xiegu CAT port', 'cat.xieguBaudHint': 'Must match the radio menu (G90 default: 19200).', 'cat.xieguAddrHint': 'Factory CI-V address of the G90/X6100 family: 0x70.', 'cat.xieguPTTLine': 'How the rig is keyed', 'cat.xieguPTTCiv': 'CI-V command', 'cat.xieguPTTHint': 'A G90 does not transmit on the CI-V command: interfaces like the DE-19 key it on RTS or DTR. Pick the line yours uses \u2014 it is also what lets WSJT-X transmit through the shared CAT link.', 'cat.optYaesu': 'Yaesu (USB)', 'cat.optKenwood': 'Kenwood (USB, network)', 'cat.civTrace': 'Log the CAT protocol', 'cat.civTraceHint': 'Writes every CAT frame to and from the radio into the log \u2014 CI-V as hex, Kenwood as the text it exchanges. For reporting a rig that answers oddly \u2014 a button that does the wrong thing, a frequency that jumps. Session only: it is a diagnostic, and it makes the log large.', 'cat.kenwoodPort': 'Kenwood COM port', 'cat.kenwoodPortHint': 'The rig\u2019s CAT/USB serial port (TS-590, TS-890, TS-990, TS-2000, and Elecraft K3/K4 which speak the same dialect).', 'cat.kenwoodBaudHint': 'Must match MENU on the radio: a TS-590 leaves the factory at 9600, a TS-890 at 115200.', 'cat.kenwoodHost': 'Or over the network (host:port)', 'cat.kenwoodHostHint': 'A serial-over-network bridge \u2014 ser2net, an Ethernet-serial adapter, a Raspberry Pi at the radio. Filled in, it is used INSTEAD of the COM port above. This is not the radio\u2019s own RJ45 socket, which speaks Kenwood\u2019s KNS protocol and is not supported.', 'cat.yaesuPort': 'Yaesu CAT port', 'cat.yaesuPortHint': 'The rig CAT port — on an FTDX10/FTDX101 over USB this is the ENHANCED COM port, not the standard one.', 'cat.yaesuBaudHint': 'Must match the radio menu (FTDX10/FTDX101 default: 38400).', 'cat.lowerLines': 'Lower the DTR and RTS lines on connect', 'cat.lowerLinesHint': 'Windows raises both when the port opens, and some interfaces read either line as PTT — the radio then transmits for as long as OpsLog runs. Tick this only if that happens: other interfaces need RTS raised to transmit at all, and lowering it makes the rig go silent.', 'cat.optIcom': 'Icom (CI-V USB)', 'cat.optIcomNet': 'Icom (CI-V network)', 'cat.optTci': 'TCI', 'flxpw.title': 'FlexRadio \u2014 power per band and mode', 'flxpw.hint': 'TX power applied when the band or mode changes. Leave a box empty to leave the power alone.', 'flxpw.band': 'Band', 'flxb.title': 'FlexRadio \u2014 per-band antennas and power', 'flxb.hint': 'Antennas are applied when the band changes; power when the band or the mode changes. Leave a power box empty to leave it alone.', 'flxb.rxAnt': 'RX antenna', 'flxb.txAnt': 'TX antenna', 'flxb.noRadio': 'No antennas reported yet \u2014 connect the FlexRadio, then reopen this panel.', 'flxpw.phone': 'Phone', 'flxpw.digi': 'Digital', 'flxpw.noBands': 'No bands configured \u2014 add them in Lists \u2192 Bands.', 'flxpw.saved': 'Saved',
|
||||||
'cat.icomNetHost': 'Rig IP / hostname', 'cat.icomNetUser': 'Network user (ID)', 'cat.icomNetPass': 'Network password',
|
'cat.icomNetHost': 'Rig IP / hostname', 'cat.icomNetUser': 'Network user (ID)', 'cat.icomNetPass': 'Network password',
|
||||||
'cat.icomNetHint': "Connects to the rig's built-in LAN server directly — no RS-BA1 or Remote Utility needed (close them first). Use the Network User1 ID/Password set in the rig's Network menu. A rig in standby is powered on automatically.",
|
'cat.icomNetHint': "Connects to the rig's built-in LAN server directly — no RS-BA1 or Remote Utility needed (close them first). Use the Network User1 ID/Password set in the rig's Network menu. A rig in standby is powered on automatically.",
|
||||||
'cat.icomNetAudio': 'Stream RX audio over the network (experimental)',
|
'cat.icomNetAudio': 'Stream RX audio over the network (experimental)',
|
||||||
@@ -352,6 +352,12 @@ const en: Dict = {
|
|||||||
'wkp.autoCallHint': 'Click a CQ macro (one whose text contains CQ) to resend it on a loop — message, gap, repeat — until you send another macro (e.g. a report), press Stop, or hit ESC. Non-CQ macros send once.', 'wkp.autoCall': 'Auto-call', 'wkp.gap': 'gap', 'wkp.gapHint': 'Seconds to wait after the message before resending', 'wkp.loopHint': 'click a CQ macro to loop it', 'wkp.macroN': 'Macro {n}',
|
'wkp.autoCallHint': 'Click a CQ macro (one whose text contains CQ) to resend it on a loop — message, gap, repeat — until you send another macro (e.g. a report), press Stop, or hit ESC. Non-CQ macros send once.', 'wkp.autoCall': 'Auto-call', 'wkp.gap': 'gap', 'wkp.gapHint': 'Seconds to wait after the message before resending', 'wkp.loopHint': 'click a CQ macro to loop it', 'wkp.macroN': 'Macro {n}',
|
||||||
'dvkp.voiceKeyer': 'Voice keyer', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Repeat a CQ-labelled message on a timer until you stop it or play another slot', 'dvkp.gap': 'Gap', 'dvkp.notPhone': 'The voice keyer only transmits on a phone mode (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Disable voice keyer', 'dvkp.noMsgPre': 'No messages recorded yet. Open', 'dvkp.settingsPath': 'Settings → Audio devices & voice keyer', 'dvkp.noMsgPost': 'to record F1–F6.', 'dvkp.transmit': 'Transmit F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — empty', 'dvkp.message': 'message',
|
'dvkp.voiceKeyer': 'Voice keyer', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Repeat a CQ-labelled message on a timer until you stop it or play another slot', 'dvkp.gap': 'Gap', 'dvkp.notPhone': 'The voice keyer only transmits on a phone mode (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Disable voice keyer', 'dvkp.noMsgPre': 'No messages recorded yet. Open', 'dvkp.settingsPath': 'Settings → Audio devices & voice keyer', 'dvkp.noMsgPost': 'to record F1–F6.', 'dvkp.transmit': 'Transmit F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — empty', 'dvkp.message': 'message',
|
||||||
'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.', 'agp.filterOnHint': 'Showing antennas for {band} only — click to show all bands', 'agp.filterOffHint': 'Showing all antennas — click to show only the current band',
|
'agp.portDeselect': 'Port {letter} — click to deselect', 'agp.portSelect': 'Select on port {letter}', 'agp.online': 'online', 'agp.offline': 'offline', 'agp.close': 'Close', 'agp.connecting': 'Connecting…', 'agp.noAntennas': 'No antennas configured.', 'agp.filterOnHint': 'Showing antennas for {band} only — click to show all bands', 'agp.filterOffHint': 'Showing all antennas — click to show only the current band',
|
||||||
|
'ampw.title': 'Amplifier', 'ampw.all': 'All amplifiers', 'ampw.pick': 'Which amplifier the widget shows',
|
||||||
|
'ampw.showHint': 'Amplifier · click to show', 'ampw.hideHint': 'Amplifier — shown · click to hide',
|
||||||
|
'ampw.close': 'Close', 'ampw.offline': 'offline', 'ampw.none': 'No amplifier configured',
|
||||||
|
'ampw.operate': 'Operate', 'ampw.standby': 'Standby', 'ampw.on': 'ON', 'ampw.off': 'OFF',
|
||||||
|
'ampw.lvlL': 'Low', 'ampw.lvlM': 'Mid', 'ampw.lvlH': 'High',
|
||||||
|
'ampw.pwr': 'Watts', 'ampw.swr': 'SWR', 'ampw.temp': 'Temp', 'ampw.id': 'Id (A)', 'ampw.fan': 'Fan mode',
|
||||||
'tgp.online': 'online', 'tgp.offline': 'offline', 'tgp.close': 'Close', 'tgp.connecting': 'Connecting…', 'tgp.swr': 'SWR', 'tgp.power': 'Fwd power', 'tgp.tune': 'Tune', 'tgp.tuning': 'Tuning…', 'tgp.tuneHint': 'Start an automatic tuning cycle on the active channel — key the rig into a carrier so the tuner can measure SWR', 'tgp.bypass': 'Bypass', 'tgp.bypassHint': 'Toggle global bypass — route the antenna straight through, tuner out of line', 'tgp.operate': 'Operate', 'tgp.standby': 'Standby', 'tgp.operateHint': 'Toggle Operate / Standby',
|
'tgp.online': 'online', 'tgp.offline': 'offline', 'tgp.close': 'Close', 'tgp.connecting': 'Connecting…', 'tgp.swr': 'SWR', 'tgp.power': 'Fwd power', 'tgp.tune': 'Tune', 'tgp.tuning': 'Tuning…', 'tgp.tuneHint': 'Start an automatic tuning cycle on the active channel — key the rig into a carrier so the tuner can measure SWR', 'tgp.bypass': 'Bypass', 'tgp.bypassHint': 'Toggle global bypass — route the antenna straight through, tuner out of line', 'tgp.operate': 'Operate', 'tgp.standby': 'Standby', 'tgp.operateHint': 'Toggle Operate / Standby',
|
||||||
'tgp.title': 'Tuner Genius', 'tgp.chActive': 'Channel {letter} — active', 'tgp.chSelect': 'Make channel {letter} active', 'tgp.chActiveTag': 'active', 'tgp.ant': 'Ant', 'tgp.bypassed': 'Bypassed', 'tgp.inLine': 'In line',
|
'tgp.title': 'Tuner Genius', 'tgp.chActive': 'Channel {letter} — active', 'tgp.chSelect': 'Make channel {letter} active', 'tgp.chActiveTag': 'active', 'tgp.ant': 'Ant', 'tgp.bypassed': 'Bypassed', 'tgp.inLine': 'In line',
|
||||||
'flxp.ritHint': 'RIT — shifts your RECEIVE frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.', 'flxp.xitHint': 'XIT — shifts your TRANSMIT frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.',
|
'flxp.ritHint': 'RIT — shifts your RECEIVE frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.', 'flxp.xitHint': 'XIT — shifts your TRANSMIT frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.',
|
||||||
@@ -678,7 +684,7 @@ const fr: Dict = {
|
|||||||
'rot.enable': 'Activer le contrôle du rotator', 'rot.testOkRG': 'Connecté — le Rotator Genius a accepté la commande (rotation vers 0°).', 'rot.type': 'Type de rotator', 'rot.rotatorNum': 'Rotator n°', 'rot.rgHint': 'Parle directement à un Rotator Genius 4O3A en TCP (port 9006 par défaut) — sans PstRotator. Le n° choisit lequel des deux rotators du boîtier piloter.', 'rot.arcoHint': "Parle directement à tout contrôleur réglé sur Yaesu GS-232A — sans PstRotator. microHAM ARCO : régler Config → LAN → CONTROL PROTOCOL (ou USB CONTROL PROTOCOL) sur « Yaesu GS-232A » ; en USB la vitesse est sans importance. ERC (Easy Rotor Control, ERC Mini compris) : son émulation DOIT être réglée sur GS-232 — un ERC laissé en Hy-Gain DCU-1 parle un autre jeu de commandes et ne répondra pas — puis choisir son port COM et la même vitesse que dans la configuration de l'ERC.", 'rot.hint': "OpsLog envoie des commandes UDP à PstRotator. Active l'écouteur UDP de PstRotator (Setup → Communication → UDP) avant de tester.",
|
'rot.enable': 'Activer le contrôle du rotator', 'rot.testOkRG': 'Connecté — le Rotator Genius a accepté la commande (rotation vers 0°).', 'rot.type': 'Type de rotator', 'rot.rotatorNum': 'Rotator n°', 'rot.rgHint': 'Parle directement à un Rotator Genius 4O3A en TCP (port 9006 par défaut) — sans PstRotator. Le n° choisit lequel des deux rotators du boîtier piloter.', 'rot.arcoHint': "Parle directement à tout contrôleur réglé sur Yaesu GS-232A — sans PstRotator. microHAM ARCO : régler Config → LAN → CONTROL PROTOCOL (ou USB CONTROL PROTOCOL) sur « Yaesu GS-232A » ; en USB la vitesse est sans importance. ERC (Easy Rotor Control, ERC Mini compris) : son émulation DOIT être réglée sur GS-232 — un ERC laissé en Hy-Gain DCU-1 parle un autre jeu de commandes et ne répondra pas — puis choisir son port COM et la même vitesse que dans la configuration de l'ERC.", 'rot.hint': "OpsLog envoie des commandes UDP à PstRotator. Active l'écouteur UDP de PstRotator (Setup → Communication → UDP) avant de tester.",
|
||||||
'extsvc.hint': "Envoie les QSO enregistrés vers des carnets en ligne. Chaque service upload automatiquement à chaque nouveau QSO si activé ; le délai est propre à chaque service (immédiat, ou 1–2 min pour corriger un QSO mal saisi avant).",
|
'extsvc.hint': "Envoie les QSO enregistrés vers des carnets en ligne. Chaque service upload automatiquement à chaque nouveau QSO si activé ; le délai est propre à chaque service (immédiat, ou 1–2 min pour corriger un QSO mal saisi avant).",
|
||||||
'hw.motorTxInhibit': "Inhiber la transmission pendant que l'antenne bouge", 'hw.motorTxInhibitHint': "Empêche le FlexRadio d'émettre pendant que les éléments bougent (nécessite le FlexRadio en API + cette antenne activée). Sans effet avec les autres radios.", 'hw.motorAntenna': 'Antenne motorisée', 'hw.motorEnable': "Activer le contrôle de l'antenne", 'hw.motorType': "Type d'antenne", 'hw.motorTransport': 'Connexion', 'hw.motorTcp': 'Réseau (TCP)', 'hw.motorSerial': 'Série (COM)', 'hw.motorCom': 'Port série', 'hw.motorBaud': 'Débit', 'hw.steppirHint': "Les contrôleurs SteppIR sont en RS-232 série (port DB9 « DATA OUT »). Série = un adaptateur USB↔RS-232 (FTDI), vu comme un port COM. Réseau = un pont série-Ethernet (comme pour l'Ultrabeam).", 'hw.steppirRange': 'Plage accordable', 'hw.steppirRangeHint': "La couverture en fréquence de la SteppIR. Sur une bande hors de cette plage (p. ex. 30 m avec une SteppIR 20 m-6 m), OpsLog n'essaie pas d'accorder l'antenne et n'inhibe pas l'émission. Défaut 13-54 MHz (20 m-6 m) ; abaisse la borne basse (p. ex. 6) pour une SteppIR équipée 40 m.", 'hw.ultrabeam': 'Antenne (Ultrabeam)', 'hw.audioVoice': 'Périphériques audio & manipulateur vocal',
|
'hw.motorTxInhibit': "Inhiber la transmission pendant que l'antenne bouge", 'hw.motorTxInhibitHint': "Empêche le FlexRadio d'émettre pendant que les éléments bougent (nécessite le FlexRadio en API + cette antenne activée). Sans effet avec les autres radios.", 'hw.motorAntenna': 'Antenne motorisée', 'hw.motorEnable': "Activer le contrôle de l'antenne", 'hw.motorType': "Type d'antenne", 'hw.motorTransport': 'Connexion', 'hw.motorTcp': 'Réseau (TCP)', 'hw.motorSerial': 'Série (COM)', 'hw.motorCom': 'Port série', 'hw.motorBaud': 'Débit', 'hw.steppirHint': "Les contrôleurs SteppIR sont en RS-232 série (port DB9 « DATA OUT »). Série = un adaptateur USB↔RS-232 (FTDI), vu comme un port COM. Réseau = un pont série-Ethernet (comme pour l'Ultrabeam).", 'hw.steppirRange': 'Plage accordable', 'hw.steppirRangeHint': "La couverture en fréquence de la SteppIR. Sur une bande hors de cette plage (p. ex. 30 m avec une SteppIR 20 m-6 m), OpsLog n'essaie pas d'accorder l'antenne et n'inhibe pas l'émission. Défaut 13-54 MHz (20 m-6 m) ; abaisse la borne basse (p. ex. 6) pour une SteppIR équipée 40 m.", 'hw.ultrabeam': 'Antenne (Ultrabeam)', 'hw.audioVoice': 'Périphériques audio & manipulateur vocal',
|
||||||
'cat.enable': 'Activer le CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig', 'cat.optFlex': 'FlexRadio (API)', 'cat.share': 'Partager le CAT avec les autres logiciels', 'cat.shareHint': "Un backend CAT natif occupe le port série de la radio, aucun autre logiciel ne peut donc y accéder. Ceci permet à WSJT-X, JTDX, MSHV ou Log4OM de dialoguer avec la radio À TRAVERS OpsLog : dans l'autre logiciel, choisissez le modèle « Hamlib NET rigctl » et saisissez 127.0.0.1:4532. Fonctionne avec tous les backends, pas seulement les natifs.", 'cat.sharePort': 'Port de partage', 'cat.optXiegu': 'Xiegu (USB)', 'cat.xieguPort': 'Port CAT Xiegu', 'cat.xieguBaudHint': 'Doit correspondre au menu de la radio (G90 par défaut : 19200).', 'cat.xieguAddrHint': "Adresse CI-V d'usine de la famille G90/X6100 : 0x70.", 'cat.xieguPTTLine': 'Passage en \u00e9mission', 'cat.xieguPTTCiv': 'Commande CI-V', 'cat.xieguPTTHint': 'Un G90 ne passe pas en \u00e9mission sur la commande CI-V : les interfaces comme le DE-19 le pilotent par RTS ou DTR. Choisissez la ligne de la v\u00f4tre \u2014 c\u2019est aussi ce qui permet \u00e0 WSJT-X d\u2019\u00e9mettre via le partage CAT.', 'cat.optYaesu': 'Yaesu (USB)', 'cat.optKenwood': 'Kenwood (USB, réseau)', 'cat.civTrace': 'Journaliser le protocole CAT', 'cat.civTraceHint': '\u00c9crit dans le journal chaque trame CAT \u00e9chang\u00e9e avec la radio \u2014 CI-V en hexad\u00e9cimal, Kenwood en texte. Pour signaler une radio qui r\u00e9pond de travers \u2014 un bouton qui fait autre chose, une fr\u00e9quence qui saute. Valable pour la session seulement : c\u2019est un diagnostic, et le journal grossit vite.', 'cat.kenwoodPort': 'Port COM Kenwood', 'cat.kenwoodPortHint': 'Le port s\u00e9rie CAT/USB de la radio (TS-590, TS-890, TS-990, TS-2000, ainsi que les Elecraft K3/K4 qui parlent le m\u00eame dialecte).', 'cat.kenwoodBaudHint': 'Doit correspondre au MENU de la radio : un TS-590 sort d\u2019usine \u00e0 9600, un TS-890 \u00e0 115200.', 'cat.kenwoodHost': 'Ou par le r\u00e9seau (h\u00f4te:port)', 'cat.kenwoodHostHint': 'Un pont s\u00e9rie-r\u00e9seau \u2014 ser2net, un bo\u00eetier Ethernet-s\u00e9rie, un Raspberry Pi pr\u00e8s de la radio. S\u2019il est rempli, il est utilis\u00e9 \u00c0 LA PLACE du port COM ci-dessus. Il ne s\u2019agit pas de la prise RJ45 de la radio, qui parle le protocole KNS de Kenwood et n\u2019est pas prise en charge.', 'cat.yaesuPort': 'Port CAT Yaesu', 'cat.yaesuPortHint': "Le port CAT de la radio — sur un FTDX10/FTDX101 en USB c'est le port COM ENHANCED, pas le standard.", 'cat.yaesuBaudHint': 'Doit correspondre au menu de la radio (FTDX10/FTDX101 par défaut : 38400).', 'cat.optIcom': 'Icom (CI-V USB)', 'cat.optIcomNet': 'Icom (CI-V réseau)', 'cat.optTci': 'TCI', 'flxpw.title': 'FlexRadio \u2014 puissance par bande et par mode', 'flxpw.hint': 'Puissance d\u2019\u00e9mission appliqu\u00e9e au changement de bande ou de mode. Laissez une case vide pour ne pas toucher \u00e0 la puissance.', 'flxpw.band': 'Bande', 'flxb.title': 'FlexRadio \u2014 antennes et puissance par bande', 'flxb.hint': 'Les antennes sont appliqu\u00e9es au changement de bande ; la puissance au changement de bande ou de mode. Laissez une case de puissance vide pour ne pas y toucher.', 'flxb.rxAnt': 'Antenne RX', 'flxb.txAnt': 'Antenne TX', 'flxb.noRadio': 'Aucune antenne remont\u00e9e \u2014 connectez le FlexRadio, puis rouvrez ce panneau.', 'flxpw.phone': 'Phonie', 'flxpw.digi': 'Num\u00e9rique', 'flxpw.noBands': 'Aucune bande configur\u00e9e \u2014 ajoutez-les dans Listes \u2192 Bandes.', 'flxpw.saved': 'Enregistr\u00e9',
|
'cat.enable': 'Activer le CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig', 'cat.optFlex': 'FlexRadio (API)', 'cat.share': 'Partager le CAT avec les autres logiciels', 'cat.shareHint': "Un backend CAT natif occupe le port série de la radio, aucun autre logiciel ne peut donc y accéder. Ceci permet à WSJT-X, JTDX, MSHV ou Log4OM de dialoguer avec la radio À TRAVERS OpsLog : dans l'autre logiciel, choisissez le modèle « Hamlib NET rigctl » et saisissez 127.0.0.1:4532. Fonctionne avec tous les backends, pas seulement les natifs.", 'cat.sharePort': 'Port de partage', 'cat.optXiegu': 'Xiegu (USB)', 'cat.xieguPort': 'Port CAT Xiegu', 'cat.xieguBaudHint': 'Doit correspondre au menu de la radio (G90 par défaut : 19200).', 'cat.xieguAddrHint': "Adresse CI-V d'usine de la famille G90/X6100 : 0x70.", 'cat.xieguPTTLine': 'Passage en \u00e9mission', 'cat.xieguPTTCiv': 'Commande CI-V', 'cat.xieguPTTHint': 'Un G90 ne passe pas en \u00e9mission sur la commande CI-V : les interfaces comme le DE-19 le pilotent par RTS ou DTR. Choisissez la ligne de la v\u00f4tre \u2014 c\u2019est aussi ce qui permet \u00e0 WSJT-X d\u2019\u00e9mettre via le partage CAT.', 'cat.optYaesu': 'Yaesu (USB)', 'cat.optKenwood': 'Kenwood (USB, réseau)', 'cat.civTrace': 'Journaliser le protocole CAT', 'cat.civTraceHint': '\u00c9crit dans le journal chaque trame CAT \u00e9chang\u00e9e avec la radio \u2014 CI-V en hexad\u00e9cimal, Kenwood en texte. Pour signaler une radio qui r\u00e9pond de travers \u2014 un bouton qui fait autre chose, une fr\u00e9quence qui saute. Valable pour la session seulement : c\u2019est un diagnostic, et le journal grossit vite.', 'cat.kenwoodPort': 'Port COM Kenwood', 'cat.kenwoodPortHint': 'Le port s\u00e9rie CAT/USB de la radio (TS-590, TS-890, TS-990, TS-2000, ainsi que les Elecraft K3/K4 qui parlent le m\u00eame dialecte).', 'cat.kenwoodBaudHint': 'Doit correspondre au MENU de la radio : un TS-590 sort d\u2019usine \u00e0 9600, un TS-890 \u00e0 115200.', 'cat.kenwoodHost': 'Ou par le r\u00e9seau (h\u00f4te:port)', 'cat.kenwoodHostHint': 'Un pont s\u00e9rie-r\u00e9seau \u2014 ser2net, un bo\u00eetier Ethernet-s\u00e9rie, un Raspberry Pi pr\u00e8s de la radio. S\u2019il est rempli, il est utilis\u00e9 \u00c0 LA PLACE du port COM ci-dessus. Il ne s\u2019agit pas de la prise RJ45 de la radio, qui parle le protocole KNS de Kenwood et n\u2019est pas prise en charge.', 'cat.yaesuPort': 'Port CAT Yaesu', 'cat.yaesuPortHint': "Le port CAT de la radio — sur un FTDX10/FTDX101 en USB c'est le port COM ENHANCED, pas le standard.", 'cat.yaesuBaudHint': 'Doit correspondre au menu de la radio (FTDX10/FTDX101 par défaut : 38400).', 'cat.lowerLines': 'Abaisser les lignes DTR et RTS à la connexion', 'cat.lowerLinesHint': 'Windows les lève à l’ouverture du port, et certaines interfaces lisent l’une ou l’autre comme PTT — la radio émet alors tant qu’OpsLog tourne. À cocher seulement dans ce cas : d’autres interfaces ont besoin de RTS haut pour émettre, et l’abaisser rend la radio muette.', 'cat.optIcom': 'Icom (CI-V USB)', 'cat.optIcomNet': 'Icom (CI-V réseau)', 'cat.optTci': 'TCI', 'flxpw.title': 'FlexRadio \u2014 puissance par bande et par mode', 'flxpw.hint': 'Puissance d\u2019\u00e9mission appliqu\u00e9e au changement de bande ou de mode. Laissez une case vide pour ne pas toucher \u00e0 la puissance.', 'flxpw.band': 'Bande', 'flxb.title': 'FlexRadio \u2014 antennes et puissance par bande', 'flxb.hint': 'Les antennes sont appliqu\u00e9es au changement de bande ; la puissance au changement de bande ou de mode. Laissez une case de puissance vide pour ne pas y toucher.', 'flxb.rxAnt': 'Antenne RX', 'flxb.txAnt': 'Antenne TX', 'flxb.noRadio': 'Aucune antenne remont\u00e9e \u2014 connectez le FlexRadio, puis rouvrez ce panneau.', 'flxpw.phone': 'Phonie', 'flxpw.digi': 'Num\u00e9rique', 'flxpw.noBands': 'Aucune bande configur\u00e9e \u2014 ajoutez-les dans Listes \u2192 Bandes.', 'flxpw.saved': 'Enregistr\u00e9',
|
||||||
'cat.icomNetHost': 'IP / nom d\'hôte du poste', 'cat.icomNetUser': 'Utilisateur réseau (ID)', 'cat.icomNetPass': 'Mot de passe réseau',
|
'cat.icomNetHost': 'IP / nom d\'hôte du poste', 'cat.icomNetUser': 'Utilisateur réseau (ID)', 'cat.icomNetPass': 'Mot de passe réseau',
|
||||||
'cat.icomNetHint': "Se connecte directement au serveur LAN intégré du poste — sans RS-BA1 ni Remote Utility (ferme-les d'abord). Utilise l'ID/mot de passe Network User1 configurés dans le menu Network du poste. Un poste en veille est allumé automatiquement.",
|
'cat.icomNetHint': "Se connecte directement au serveur LAN intégré du poste — sans RS-BA1 ni Remote Utility (ferme-les d'abord). Utilise l'ID/mot de passe Network User1 configurés dans le menu Network du poste. Un poste en veille est allumé automatiquement.",
|
||||||
'cat.icomNetAudio': 'Diffuser l’audio RX par le réseau (expérimental)',
|
'cat.icomNetAudio': 'Diffuser l’audio RX par le réseau (expérimental)',
|
||||||
@@ -747,6 +753,12 @@ const fr: Dict = {
|
|||||||
'wkp.autoCallHint': "Clique une macro CQ (dont le texte contient CQ) pour la réémettre en boucle — message, pause, répétition — jusqu'à envoyer une autre macro (ex. un report), appuyer sur Stop ou ESC. Les macros non-CQ ne sont émises qu'une fois.", 'wkp.autoCall': 'Appel auto', 'wkp.gap': 'pause', 'wkp.gapHint': 'Secondes à attendre après le message avant de réémettre', 'wkp.loopHint': 'clique une macro CQ pour la boucler', 'wkp.macroN': 'Macro {n}',
|
'wkp.autoCallHint': "Clique une macro CQ (dont le texte contient CQ) pour la réémettre en boucle — message, pause, répétition — jusqu'à envoyer une autre macro (ex. un report), appuyer sur Stop ou ESC. Les macros non-CQ ne sont émises qu'une fois.", 'wkp.autoCall': 'Appel auto', 'wkp.gap': 'pause', 'wkp.gapHint': 'Secondes à attendre après le message avant de réémettre', 'wkp.loopHint': 'clique une macro CQ pour la boucler', 'wkp.macroN': 'Macro {n}',
|
||||||
'dvkp.voiceKeyer': 'Manipulateur vocal', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Répète un message libellé CQ à intervalle régulier jusqu\'à l\'arrêt ou la lecture d\'un autre slot', 'dvkp.gap': 'Intervalle', 'dvkp.notPhone': 'Le manipulateur vocal n\'émet qu\'en mode phonie (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Désactiver le manipulateur vocal', 'dvkp.noMsgPre': 'Aucun message enregistré. Ouvre', 'dvkp.settingsPath': 'Réglages → Périphériques audio & manipulateur vocal', 'dvkp.noMsgPost': 'pour enregistrer F1–F6.', 'dvkp.transmit': 'Émettre F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — vide', 'dvkp.message': 'message',
|
'dvkp.voiceKeyer': 'Manipulateur vocal', 'dvkp.autoCq': 'Auto CQ', 'dvkp.autoCqHint': 'Répète un message libellé CQ à intervalle régulier jusqu\'à l\'arrêt ou la lecture d\'un autre slot', 'dvkp.gap': 'Intervalle', 'dvkp.notPhone': 'Le manipulateur vocal n\'émet qu\'en mode phonie (SSB/AM/FM)', 'dvkp.stop': 'Stop', 'dvkp.disable': 'Désactiver le manipulateur vocal', 'dvkp.noMsgPre': 'Aucun message enregistré. Ouvre', 'dvkp.settingsPath': 'Réglages → Périphériques audio & manipulateur vocal', 'dvkp.noMsgPost': 'pour enregistrer F1–F6.', 'dvkp.transmit': 'Émettre F{slot}{label} ({dur}s)', 'dvkp.empty': 'F{slot} — vide', 'dvkp.message': 'message',
|
||||||
'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.', 'agp.filterOnHint': 'Antennes du {band} uniquement — clic pour afficher toutes les bandes', 'agp.filterOffHint': 'Toutes les antennes affichées — clic pour n’afficher que la bande courante',
|
'agp.portDeselect': 'Port {letter} — clic pour désélectionner', 'agp.portSelect': 'Sélectionner sur le port {letter}', 'agp.online': 'en ligne', 'agp.offline': 'hors ligne', 'agp.close': 'Fermer', 'agp.connecting': 'Connexion…', 'agp.noAntennas': 'Aucune antenne configurée.', 'agp.filterOnHint': 'Antennes du {band} uniquement — clic pour afficher toutes les bandes', 'agp.filterOffHint': 'Toutes les antennes affichées — clic pour n’afficher que la bande courante',
|
||||||
|
'ampw.title': 'Amplificateur', 'ampw.all': 'Tous les amplis', 'ampw.pick': 'Ampli affiché par le widget',
|
||||||
|
'ampw.showHint': 'Amplificateur · cliquer pour afficher', 'ampw.hideHint': 'Amplificateur — affiché · cliquer pour masquer',
|
||||||
|
'ampw.close': 'Fermer', 'ampw.offline': 'hors ligne', 'ampw.none': 'Aucun amplificateur configuré',
|
||||||
|
'ampw.operate': 'Operate', 'ampw.standby': 'Standby', 'ampw.on': 'ON', 'ampw.off': 'OFF',
|
||||||
|
'ampw.lvlL': 'Low', 'ampw.lvlM': 'Mid', 'ampw.lvlH': 'High',
|
||||||
|
'ampw.pwr': 'Watts', 'ampw.swr': 'ROS', 'ampw.temp': 'Temp', 'ampw.id': 'Id (A)', 'ampw.fan': 'Mode ventil.',
|
||||||
'tgp.online': 'en ligne', 'tgp.offline': 'hors ligne', 'tgp.close': 'Fermer', 'tgp.connecting': 'Connexion…', 'tgp.swr': 'ROS', 'tgp.power': 'Puiss. directe', 'tgp.tune': 'Accord', 'tgp.tuning': 'Accord…', 'tgp.tuneHint': "Lancer un cycle d'accord automatique sur le canal actif — passe la radio en porteuse pour que le coupleur mesure le ROS", 'tgp.bypass': 'Bypass', 'tgp.bypassHint': "Basculer le bypass global — antenne en direct, coupleur hors ligne", 'tgp.operate': 'Operate', 'tgp.standby': 'Standby', 'tgp.operateHint': 'Basculer Operate / Standby',
|
'tgp.online': 'en ligne', 'tgp.offline': 'hors ligne', 'tgp.close': 'Fermer', 'tgp.connecting': 'Connexion…', 'tgp.swr': 'ROS', 'tgp.power': 'Puiss. directe', 'tgp.tune': 'Accord', 'tgp.tuning': 'Accord…', 'tgp.tuneHint': "Lancer un cycle d'accord automatique sur le canal actif — passe la radio en porteuse pour que le coupleur mesure le ROS", 'tgp.bypass': 'Bypass', 'tgp.bypassHint': "Basculer le bypass global — antenne en direct, coupleur hors ligne", 'tgp.operate': 'Operate', 'tgp.standby': 'Standby', 'tgp.operateHint': 'Basculer Operate / Standby',
|
||||||
'tgp.title': 'Tuner Genius', 'tgp.chActive': 'Canal {letter} — actif', 'tgp.chSelect': 'Activer le canal {letter}', 'tgp.chActiveTag': 'actif', 'tgp.ant': 'Ant', 'tgp.bypassed': 'Bypass', 'tgp.inLine': 'En ligne',
|
'tgp.title': 'Tuner Genius', 'tgp.chActive': 'Canal {letter} — actif', 'tgp.chSelect': 'Activer le canal {letter}', 'tgp.chActiveTag': 'actif', 'tgp.ant': 'Ant', 'tgp.bypassed': 'Bypass', 'tgp.inLine': 'En ligne',
|
||||||
'flxp.ritHint': "RIT — décale uniquement ta fréquence de RÉCEPTION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.", 'flxp.xitHint': "XIT — décale uniquement ta fréquence d'ÉMISSION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.",
|
'flxp.ritHint': "RIT — décale uniquement ta fréquence de RÉCEPTION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.", 'flxp.xitHint': "XIT — décale uniquement ta fréquence d'ÉMISSION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const PORTABLE_KEYS = [
|
|||||||
'opslog.autofocusWB', // auto-focus Worked-before
|
'opslog.autofocusWB', // auto-focus Worked-before
|
||||||
'hamlog.filterPresets', // Filter Builder saved presets
|
'hamlog.filterPresets', // Filter Builder saved presets
|
||||||
'opslog.showRotor', // rotor compass shown next to the keyers
|
'opslog.showRotor', // rotor compass shown next to the keyers
|
||||||
|
'opslog.showAmpWidget', // amplifier widget shown next to the keyers
|
||||||
|
'opslog.ampSel.widget', // which amplifier that widget shows ("all" or an amp id)
|
||||||
'opslog.showBeamOnMap', // antenna beam lobe drawn on the Main map
|
'opslog.showBeamOnMap', // antenna beam lobe drawn on the Main map
|
||||||
'opslog.startEqualsEnd',// log TIME_ON = TIME_OFF (QSO time = completion time)
|
'opslog.startEqualsEnd',// log TIME_ON = TIME_OFF (QSO time = completion time)
|
||||||
'opslog.showQsoRate', // QSO-rate meter (10/60 min) shown in the header
|
'opslog.showQsoRate', // QSO-rate meter (10/60 min) shown in the header
|
||||||
|
|||||||
@@ -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.22.8';
|
export const APP_VERSION = '0.22.9';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
Vendored
+2
-2
@@ -75,7 +75,7 @@ export function AudioStopTX():Promise<void>;
|
|||||||
|
|
||||||
export function AudioTXActive():Promise<boolean>;
|
export function AudioTXActive():Promise<boolean>;
|
||||||
|
|
||||||
export function AwardCellQSOs(arg1:string,arg2:string,arg3:string):Promise<Array<qso.QSO>>;
|
export function AwardCellQSOs(arg1:string,arg2:string,arg3:string,arg4:string):Promise<Array<qso.QSO>>;
|
||||||
|
|
||||||
export function AwardFields():Promise<Array<string>>;
|
export function AwardFields():Promise<Array<string>>;
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ export function GetAudioSettings():Promise<main.AudioSettings>;
|
|||||||
|
|
||||||
export function GetAutostartPrograms():Promise<Array<main.AutostartProgram>>;
|
export function GetAutostartPrograms():Promise<Array<main.AutostartProgram>>;
|
||||||
|
|
||||||
export function GetAward(arg1:string):Promise<award.Result>;
|
export function GetAward(arg1:string,arg2:string):Promise<award.Result>;
|
||||||
|
|
||||||
export function GetAwardDefs():Promise<Array<award.Def>>;
|
export function GetAwardDefs():Promise<Array<award.Def>>;
|
||||||
|
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ export function AudioTXActive() {
|
|||||||
return window['go']['main']['App']['AudioTXActive']();
|
return window['go']['main']['App']['AudioTXActive']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AwardCellQSOs(arg1, arg2, arg3) {
|
export function AwardCellQSOs(arg1, arg2, arg3, arg4) {
|
||||||
return window['go']['main']['App']['AwardCellQSOs'](arg1, arg2, arg3);
|
return window['go']['main']['App']['AwardCellQSOs'](arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AwardFields() {
|
export function AwardFields() {
|
||||||
@@ -678,8 +678,8 @@ export function GetAutostartPrograms() {
|
|||||||
return window['go']['main']['App']['GetAutostartPrograms']();
|
return window['go']['main']['App']['GetAutostartPrograms']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetAward(arg1) {
|
export function GetAward(arg1, arg2) {
|
||||||
return window['go']['main']['App']['GetAward'](arg1);
|
return window['go']['main']['App']['GetAward'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetAwardDefs() {
|
export function GetAwardDefs() {
|
||||||
|
|||||||
@@ -1924,6 +1924,8 @@ export namespace main {
|
|||||||
kenwood_host: string;
|
kenwood_host: string;
|
||||||
kenwood_port: string;
|
kenwood_port: string;
|
||||||
kenwood_baud: number;
|
kenwood_baud: number;
|
||||||
|
yaesu_low_lines: boolean;
|
||||||
|
kenwood_low_lines: boolean;
|
||||||
icom_port: string;
|
icom_port: string;
|
||||||
icom_baud: number;
|
icom_baud: number;
|
||||||
icom_addr: number;
|
icom_addr: number;
|
||||||
@@ -1964,6 +1966,8 @@ export namespace main {
|
|||||||
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_baud = source["kenwood_baud"];
|
this.kenwood_baud = source["kenwood_baud"];
|
||||||
|
this.yaesu_low_lines = source["yaesu_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"];
|
||||||
this.icom_addr = source["icom_addr"];
|
this.icom_addr = source["icom_addr"];
|
||||||
@@ -3497,6 +3501,11 @@ export namespace powergenius {
|
|||||||
fan_mode?: string;
|
fan_mode?: string;
|
||||||
temperature: number;
|
temperature: number;
|
||||||
operate: boolean;
|
operate: boolean;
|
||||||
|
fwd_w: number;
|
||||||
|
peak_w: number;
|
||||||
|
vswr: number;
|
||||||
|
id: number;
|
||||||
|
peak_id: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Status(source);
|
return new Status(source);
|
||||||
@@ -3511,6 +3520,11 @@ export namespace powergenius {
|
|||||||
this.fan_mode = source["fan_mode"];
|
this.fan_mode = source["fan_mode"];
|
||||||
this.temperature = source["temperature"];
|
this.temperature = source["temperature"];
|
||||||
this.operate = source["operate"];
|
this.operate = source["operate"];
|
||||||
|
this.fwd_w = source["fwd_w"];
|
||||||
|
this.peak_w = source["peak_w"];
|
||||||
|
this.vswr = source["vswr"];
|
||||||
|
this.id = source["id"];
|
||||||
|
this.peak_id = source["peak_id"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4556,6 +4570,7 @@ export namespace spe {
|
|||||||
|
|
||||||
export class Status {
|
export class Status {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
transport: string;
|
||||||
last_error?: string;
|
last_error?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
operate: boolean;
|
operate: boolean;
|
||||||
@@ -4579,6 +4594,7 @@ export namespace spe {
|
|||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.connected = source["connected"];
|
this.connected = source["connected"];
|
||||||
|
this.transport = source["transport"];
|
||||||
this.last_error = source["last_error"];
|
this.last_error = source["last_error"];
|
||||||
this.model = source["model"];
|
this.model = source["model"];
|
||||||
this.operate = source["operate"];
|
this.operate = source["operate"];
|
||||||
@@ -4633,6 +4649,7 @@ export namespace tunergenius {
|
|||||||
last_error?: string;
|
last_error?: string;
|
||||||
fwd_dbm: number;
|
fwd_dbm: number;
|
||||||
fwd_w: number;
|
fwd_w: number;
|
||||||
|
peak_w: number;
|
||||||
swr_db: number;
|
swr_db: number;
|
||||||
vswr: number;
|
vswr: number;
|
||||||
operate: boolean;
|
operate: boolean;
|
||||||
@@ -4660,6 +4677,7 @@ export namespace tunergenius {
|
|||||||
this.last_error = source["last_error"];
|
this.last_error = source["last_error"];
|
||||||
this.fwd_dbm = source["fwd_dbm"];
|
this.fwd_dbm = source["fwd_dbm"];
|
||||||
this.fwd_w = source["fwd_w"];
|
this.fwd_w = source["fwd_w"];
|
||||||
|
this.peak_w = source["peak_w"];
|
||||||
this.swr_db = source["swr_db"];
|
this.swr_db = source["swr_db"];
|
||||||
this.vswr = source["vswr"];
|
this.vswr = source["vswr"];
|
||||||
this.operate = source["operate"];
|
this.operate = source["operate"];
|
||||||
|
|||||||
+7
-15
@@ -314,7 +314,12 @@ func (f *Flex) send(cmd string) int {
|
|||||||
debugLog.Printf("Flex: send %q failed: %v", cmd, err)
|
debugLog.Printf("Flex: send %q failed: %v", cmd, err)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
// Meter subscriptions are pure bookkeeping and a Flex announces meters one
|
||||||
|
// status line at a time — tracing each "sub meter N" wrote dozens of lines
|
||||||
|
// at every connect.
|
||||||
|
if !strings.HasPrefix(cmd, "sub meter ") {
|
||||||
debugLog.Printf("Flex: → %s", cmd)
|
debugLog.Printf("Flex: → %s", cmd)
|
||||||
|
}
|
||||||
return seq
|
return seq
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,17 +778,11 @@ func (f *Flex) handleStatus(payload string) {
|
|||||||
f.meterMeta[num] = old
|
f.meterMeta[num] = old
|
||||||
}
|
}
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
// One line for the whole batch, not one per meter: a Flex announces
|
// Not logged: the radio announces meters ONE per status line, so even
|
||||||
// dozens at connect and that was a wall of text every time.
|
// a batched line meant dozens of them at every connect.
|
||||||
var names []string
|
|
||||||
for _, id := range newIDs {
|
for _, id := range newIDs {
|
||||||
mi := f.meterMeta[id]
|
|
||||||
names = append(names, nonEmpty(mi.name, strconv.Itoa(id)))
|
|
||||||
f.subscribeMeter(id)
|
f.subscribeMeter(id)
|
||||||
}
|
}
|
||||||
if len(names) > 0 {
|
|
||||||
debugLog.Printf("Flex: subscribed to %d meter(s): %s", len(names), strings.Join(names, " "))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Spot status: "spot <index> …". Track the index so we can clear the
|
// Spot status: "spot <index> …". Track the index so we can clear the
|
||||||
// panadapter, and log it verbatim — a click on a panadapter spot pushes a
|
// panadapter, and log it verbatim — a click on a panadapter spot pushes a
|
||||||
@@ -2261,13 +2260,6 @@ func (f *Flex) subscribeMeter(id int) {
|
|||||||
f.send(fmt.Sprintf("sub meter %d", id))
|
f.send(fmt.Sprintf("sub meter %d", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func nonEmpty(s, def string) string {
|
|
||||||
if s == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFloatDefault(s string, def float64) float64 {
|
func parseFloatDefault(s string, def float64) float64 {
|
||||||
if v, err := strconv.ParseFloat(strings.TrimSpace(s), 64); err == nil {
|
if v, err := strconv.ParseFloat(strings.TrimSpace(s), 64); err == nil {
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -114,6 +114,15 @@ type IcomSerial struct {
|
|||||||
silentGrace time.Duration // current width of that tolerance (backs off, see ReadState)
|
silentGrace time.Duration // current width of that tolerance (backs off, see ReadState)
|
||||||
dspLoaded bool // readDSP has run since the rig became responsive (loads all
|
dspLoaded bool // readDSP has run since the rig became responsive (loads all
|
||||||
// the panel's set-once controls once the rig actually answers)
|
// the panel's set-once controls once the rig actually answers)
|
||||||
|
// When the console last asked for the DSP snapshot. The meters and the
|
||||||
|
// front-panel rotation are polled ONLY while something is displaying them:
|
||||||
|
// they are half of OpsLog's CI-V traffic, and on a shared bus — a microHAM
|
||||||
|
// Router splitting one CI-V link into two virtual ports, OpsLog on one and
|
||||||
|
// WSJT-X/MSHV on the other — every frame we don't need is a frame that can
|
||||||
|
// collide with the other program's.
|
||||||
|
dspWantMu sync.Mutex
|
||||||
|
dspWantAt time.Time
|
||||||
|
|
||||||
lastSetFreq int64 // last frequency commanded (spot click: freq then mode)
|
lastSetFreq int64 // last frequency commanded (spot click: freq then mode)
|
||||||
lastSetFreqAt time.Time
|
lastSetFreqAt time.Time
|
||||||
|
|
||||||
@@ -409,9 +418,18 @@ func (b *IcomSerial) ReadState() (RigState, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Live meters + TX state for the Icom panel (the rig doesn't push these).
|
// Live meters + TX state for the Icom panel (the rig doesn't push these).
|
||||||
|
// The meters are polled only while the console is actually on screen: they
|
||||||
|
// were three CI-V round trips per cycle, every cycle, whether or not anyone
|
||||||
|
// could see them. The previous readings are kept so the panel opens on the
|
||||||
|
// last known values rather than on zeros.
|
||||||
tx := b.readTX()
|
tx := b.readTX()
|
||||||
sm, _ := b.readMeter(civ.SubMeterS)
|
watched := b.panelWatched()
|
||||||
po, swr := 0, 0
|
b.dspMu.Lock()
|
||||||
|
sm, po, swr := b.dsp.SMeter, b.dsp.PowerMeter, b.dsp.SWRMeter
|
||||||
|
b.dspMu.Unlock()
|
||||||
|
if watched {
|
||||||
|
sm, _ = b.readMeter(civ.SubMeterS)
|
||||||
|
po, swr = 0, 0
|
||||||
if tx {
|
if tx {
|
||||||
if v, ok := b.readMeter(civ.SubMeterPo); ok {
|
if v, ok := b.readMeter(civ.SubMeterPo); ok {
|
||||||
po = v
|
po = v
|
||||||
@@ -420,6 +438,7 @@ func (b *IcomSerial) ReadState() (RigState, error) {
|
|||||||
swr = v
|
swr = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
b.dspMu.Lock()
|
b.dspMu.Lock()
|
||||||
b.dsp.Available = true
|
b.dsp.Available = true
|
||||||
b.dsp.Model = b.model
|
b.dsp.Model = b.model
|
||||||
@@ -434,12 +453,16 @@ func (b *IcomSerial) ReadState() (RigState, error) {
|
|||||||
// snapshot so the panel's antenna, sliders, RIT, notch, etc. reflect the rig
|
// snapshot so the panel's antenna, sliders, RIT, notch, etc. reflect the rig
|
||||||
// instead of sitting at their zero defaults. Runs once; ↻ Refresh re-reads on
|
// instead of sitting at their zero defaults. Runs once; ↻ Refresh re-reads on
|
||||||
// demand, and a reconnect re-arms it (Connect clears dspLoaded).
|
// demand, and a reconnect re-arms it (Connect clears dspLoaded).
|
||||||
|
// Both of these only make sense for the console — don't spend the bus on them
|
||||||
|
// while it is closed. The snapshot then loads the first time it is opened.
|
||||||
|
if watched {
|
||||||
if !b.dspLoaded {
|
if !b.dspLoaded {
|
||||||
b.readDSP()
|
b.readDSP()
|
||||||
b.dspLoaded = true
|
b.dspLoaded = true
|
||||||
} else {
|
} else {
|
||||||
b.refreshFrontPanel()
|
b.refreshFrontPanel()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,12 +555,25 @@ func (b *IcomSerial) SetPower(on bool) error {
|
|||||||
return fmt.Errorf("icom: not connected")
|
return fmt.Errorf("icom: not connected")
|
||||||
}
|
}
|
||||||
if on {
|
if on {
|
||||||
buf := make([]byte, 0, 32)
|
// The preamble has to last long enough IN TIME for the sleeping rig to
|
||||||
for i := 0; i < 25; i++ {
|
// notice it, so its length scales with the baud rate — Icom's own table
|
||||||
|
// asks for 7 bytes at 4800 baud and 150 at 115200, which is a fixed
|
||||||
|
// ~25 ms of carrier. A flat 25 bytes (what this sent before) is right at
|
||||||
|
// 19200 and far too short above it, so a rig configured for 115200 simply
|
||||||
|
// ignored the command.
|
||||||
|
n := b.baud / 768
|
||||||
|
if n < 7 {
|
||||||
|
n = 7
|
||||||
|
} else if n > 150 {
|
||||||
|
n = 150
|
||||||
|
}
|
||||||
|
buf := make([]byte, 0, n+8)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
buf = append(buf, 0xFE)
|
buf = append(buf, 0xFE)
|
||||||
}
|
}
|
||||||
buf = append(buf, 0xFE, 0xFE, b.rigAddr, civ.AddrController, civ.CmdPower, 0x01, 0xFD)
|
buf = append(buf, 0xFE, 0xFE, b.rigAddr, civ.AddrController, civ.CmdPower, 0x01, 0xFD)
|
||||||
_, err := b.port.Write(buf)
|
_, err := b.port.Write(buf)
|
||||||
|
applog.Printf("icom: power ON — %d-byte wake preamble at %d baud, addr 0x%02X (err=%v)", n, b.baud, b.rigAddr, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := b.port.Write(civ.Frame(b.rigAddr, civ.AddrController, civ.CmdPower, 0x00))
|
_, err := b.port.Write(civ.Frame(b.rigAddr, civ.AddrController, civ.CmdPower, 0x00))
|
||||||
@@ -1289,11 +1325,26 @@ func (b *IcomSerial) modeCode(mode string) (code byte, data bool, err error) {
|
|||||||
// ── IcomController: receive-DSP controls for the Icom tab ───────────────────
|
// ── IcomController: receive-DSP controls for the Icom tab ───────────────────
|
||||||
|
|
||||||
func (b *IcomSerial) IcomState() IcomTXState {
|
func (b *IcomSerial) IcomState() IcomTXState {
|
||||||
|
// Asking for the snapshot is what marks the console as being watched, which
|
||||||
|
// is what re-enables the meter polling in ReadState.
|
||||||
|
b.dspWantMu.Lock()
|
||||||
|
b.dspWantAt = time.Now()
|
||||||
|
b.dspWantMu.Unlock()
|
||||||
|
|
||||||
b.dspMu.Lock()
|
b.dspMu.Lock()
|
||||||
defer b.dspMu.Unlock()
|
defer b.dspMu.Unlock()
|
||||||
return b.dsp
|
return b.dsp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// panelWatched reports whether the Icom console asked for the DSP snapshot
|
||||||
|
// recently — i.e. whether anything is on screen to read the meters.
|
||||||
|
func (b *IcomSerial) panelWatched() bool {
|
||||||
|
b.dspWantMu.Lock()
|
||||||
|
at := b.dspWantAt
|
||||||
|
b.dspWantMu.Unlock()
|
||||||
|
return !at.IsZero() && time.Since(at) < 3*time.Second
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshIcom re-reads the whole DSP snapshot from the rig. Runs on the CAT
|
// RefreshIcom re-reads the whole DSP snapshot from the rig. Runs on the CAT
|
||||||
// goroutine (dispatched via IcomDo).
|
// goroutine (dispatched via IcomDo).
|
||||||
func (b *IcomSerial) RefreshIcom() error {
|
func (b *IcomSerial) RefreshIcom() error {
|
||||||
|
|||||||
+26
-12
@@ -88,6 +88,26 @@ type Kenwood struct {
|
|||||||
// Kept only to put it in the error message: "not answering" and "answering
|
// Kept only to put it in the error message: "not answering" and "answering
|
||||||
// something unreadable" are different faults with different fixes.
|
// something unreadable" are different faults with different fixes.
|
||||||
heard string
|
heard string
|
||||||
|
|
||||||
|
// lowerLines deasserts DTR and RTS after opening the port.
|
||||||
|
//
|
||||||
|
// Neither default is safe for everyone, which is why this is a setting and
|
||||||
|
// not a decision. Windows raises both lines on open: an interface that reads
|
||||||
|
// them as PTT then keys the rig for as long as OpsLog is running — reported
|
||||||
|
// as FT8 output power wandering, and gone the moment OpsLog was closed.
|
||||||
|
// Lowering them instead silences the radios whose USB-serial interface needs
|
||||||
|
// RTS asserted to transmit at all — a TS-990 that opened cleanly and answered
|
||||||
|
// nothing. Off by default: that is how this backend behaved for its whole
|
||||||
|
// life before the question came up.
|
||||||
|
lowerLines bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLowerLines chooses whether DTR and RTS are deasserted on connect. Set
|
||||||
|
// before Connect.
|
||||||
|
func (k *Kenwood) SetLowerLines(v bool) {
|
||||||
|
k.mu.Lock()
|
||||||
|
k.lowerLines = v
|
||||||
|
k.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// where names the link for a message, so an error does not read "COM @ 0 baud"
|
// where names the link for a message, so an error does not read "COM @ 0 baud"
|
||||||
@@ -572,18 +592,12 @@ func (k *Kenwood) openPort() (serial.Port, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// The modem lines are LEFT ALONE.
|
// The modem lines are only touched when the operator asks for it. See
|
||||||
//
|
// lowerLines: both defaults break somebody's station.
|
||||||
// They were briefly deasserted here, to stop an interface that reads them as
|
if k.lowerLines {
|
||||||
// PTT from keying the rig on connect. That silenced radios instead: a TS-990
|
_ = p.SetDTR(false)
|
||||||
// on COM3 opened fine and answered nothing, because a great many USB-serial
|
_ = p.SetRTS(false)
|
||||||
// interfaces will not transmit with RTS low — hardware flow control, or an
|
}
|
||||||
// output stage the line enables. "Opened but the rig sent nothing" was this,
|
|
||||||
// and it arrived as "CAT stopped working after the update".
|
|
||||||
//
|
|
||||||
// The fault that change was written for was a Xiegu G90 behind a DE-19, and
|
|
||||||
// that backend now has an explicit setting for which line keys it. A rig
|
|
||||||
// whose PTT is a CAT command has no business touching DTR or RTS at all.
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-6
@@ -82,6 +82,9 @@ var yaesuModeToADIF = map[byte]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Yaesu struct {
|
type Yaesu struct {
|
||||||
|
// lowerLines deasserts DTR and RTS on connect — see Kenwood.lowerLines for
|
||||||
|
// why neither default is safe for every station.
|
||||||
|
lowerLines bool
|
||||||
portName string
|
portName string
|
||||||
baud int
|
baud int
|
||||||
digital string // ADIF mode reported for DATA (FT8, RTTY…)
|
digital string // ADIF mode reported for DATA (FT8, RTTY…)
|
||||||
@@ -127,6 +130,14 @@ 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"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLowerLines chooses whether DTR and RTS are deasserted on connect. Set
|
||||||
|
// before Connect.
|
||||||
|
func (y *Yaesu) SetLowerLines(v bool) {
|
||||||
|
y.mu.Lock()
|
||||||
|
y.lowerLines = v
|
||||||
|
y.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (y *Yaesu) Name() string { return "yaesu" }
|
func (y *Yaesu) Name() string { return "yaesu" }
|
||||||
|
|
||||||
func (y *Yaesu) Connect() error {
|
func (y *Yaesu) Connect() error {
|
||||||
@@ -150,12 +161,12 @@ func (y *Yaesu) Connect() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("yaesu: open %s @ %d baud: %w", y.portName, y.baud, err)
|
return fmt.Errorf("yaesu: open %s @ %d baud: %w", y.portName, y.baud, err)
|
||||||
}
|
}
|
||||||
// The modem lines are LEFT ALONE — see the same note in kenwood.go.
|
// The modem lines are only touched when the operator asks for it — see the
|
||||||
//
|
// note on Kenwood.lowerLines. Both defaults break somebody's station.
|
||||||
// Deasserting them to keep an interface from keying the rig silenced radios
|
if y.lowerLines {
|
||||||
// instead: many USB-serial interfaces will not transmit with RTS low. The
|
_ = p.SetDTR(false)
|
||||||
// Xiegu fault that change was written for is handled in that backend, which
|
_ = p.SetRTS(false)
|
||||||
// now has an explicit setting for which line keys the rig.
|
}
|
||||||
p.SetReadTimeout(300 * time.Millisecond)
|
p.SetReadTimeout(300 * time.Millisecond)
|
||||||
y.port = p
|
y.port = p
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ func (s *Server) run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||||
applog.Printf("udp: [%s] rx %d bytes from %s\n", s.cfg.Name, len(pkt), remote)
|
|
||||||
ev := Event{ConfigID: s.cfg.ID, Service: s.cfg.ServiceType, Source: remote.String()}
|
ev := Event{ConfigID: s.cfg.ID, Service: s.cfg.ServiceType, Source: remote.String()}
|
||||||
switch s.cfg.ServiceType {
|
switch s.cfg.ServiceType {
|
||||||
case ServiceWSJT:
|
case ServiceWSJT:
|
||||||
@@ -229,8 +228,12 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
|||||||
ev.Mode = w.Mode
|
ev.Mode = w.Mode
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
applog.Printf("udp: [%s] WSJT decoded: prog=%q dx_call=%q grid=%q mode=%q freq=%d adif_len=%d\n",
|
// Only a logged QSO is worth a line — WSJT-X/MSHV send a Status packet
|
||||||
|
// every second, and logging each one buried the rest of the file.
|
||||||
|
if len(w.LoggedADIF) > 0 {
|
||||||
|
applog.Printf("udp: [%s] WSJT QSO logged: prog=%q dx_call=%q grid=%q mode=%q freq=%d adif_len=%d\n",
|
||||||
s.cfg.Name, w.ProgramID, w.DXCall, w.DXGrid, w.Mode, w.FreqHz, len(w.LoggedADIF))
|
s.cfg.Name, w.ProgramID, w.DXCall, w.DXGrid, w.Mode, w.FreqHz, len(w.LoggedADIF))
|
||||||
|
}
|
||||||
ev.DXCall = w.DXCall
|
ev.DXCall = w.DXCall
|
||||||
ev.DXGrid = w.DXGrid
|
ev.DXGrid = w.DXGrid
|
||||||
ev.Mode = w.Mode
|
ev.Mode = w.Mode
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ package powergenius
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -36,6 +37,16 @@ type Status struct {
|
|||||||
FanMode string `json:"fan_mode,omitempty"` // STANDARD / CONTEST / BROADCAST
|
FanMode string `json:"fan_mode,omitempty"` // STANDARD / CONTEST / BROADCAST
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
Operate bool `json:"operate"` // OPERATE vs STANDBY (optimistic until the amp reports it)
|
Operate bool `json:"operate"` // OPERATE vs STANDBY (optimistic until the amp reports it)
|
||||||
|
|
||||||
|
// Live power, read straight from the amplifier's own status frame rather
|
||||||
|
// than sampled off the FlexRadio meter stream. PeakW is the amp's own peak
|
||||||
|
// detector: a poll catches one instant of the envelope, so the plain forward
|
||||||
|
// figure lands between syllables as often as on a peak.
|
||||||
|
FwdW float64 `json:"fwd_w"` // forward power [W] (the frame reports dBm)
|
||||||
|
PeakW float64 `json:"peak_w"` // peak forward power [W]
|
||||||
|
Vswr float64 `json:"vswr"` // VSWR, derived from the frame's return loss in dB
|
||||||
|
Id float64 `json:"id"` // drain current [A]
|
||||||
|
PeakId float64 `json:"peak_id"` // peak drain current [A]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -226,9 +237,9 @@ func (c *Client) parse(resp string) {
|
|||||||
c.statusMu.Lock()
|
c.statusMu.Lock()
|
||||||
c.status.Connected = true
|
c.status.Connected = true
|
||||||
c.status.LastError = ""
|
c.status.LastError = ""
|
||||||
// Log each DISTINCT status payload once: the PGXL's field set isn't fully
|
// One raw frame per session is enough to learn the field set — the frames
|
||||||
// documented, so this is how we learn the real key for e.g. operate/standby.
|
// carry live meter values, so "log on change" logged every frame.
|
||||||
if data != c.lastRaw {
|
if c.lastRaw == "" {
|
||||||
c.lastRaw = data
|
c.lastRaw = data
|
||||||
applog.Printf("pgxl: status raw=%q", data)
|
applog.Printf("pgxl: status raw=%q", data)
|
||||||
}
|
}
|
||||||
@@ -252,7 +263,50 @@ func (c *Client) parse(resp string) {
|
|||||||
c.status.FanMode = dev
|
c.status.FanMode = dev
|
||||||
case "temp":
|
case "temp":
|
||||||
c.status.Temperature, _ = strconv.ParseFloat(kv[1], 64)
|
c.status.Temperature, _ = strconv.ParseFloat(kv[1], 64)
|
||||||
|
case "fwd":
|
||||||
|
if v, err := strconv.ParseFloat(kv[1], 64); err == nil {
|
||||||
|
c.status.FwdW = dbmToWatts(v)
|
||||||
|
}
|
||||||
|
case "peakfwd":
|
||||||
|
if v, err := strconv.ParseFloat(kv[1], 64); err == nil {
|
||||||
|
c.status.PeakW = dbmToWatts(v)
|
||||||
|
}
|
||||||
|
case "swr":
|
||||||
|
if v, err := strconv.ParseFloat(kv[1], 64); err == nil {
|
||||||
|
c.status.Vswr = returnLossToVswr(v)
|
||||||
|
}
|
||||||
|
case "id":
|
||||||
|
c.status.Id, _ = strconv.ParseFloat(kv[1], 64)
|
||||||
|
case "peakid":
|
||||||
|
c.status.PeakId, _ = strconv.ParseFloat(kv[1], 64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.statusMu.Unlock()
|
c.statusMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dbmToWatts converts a power reading in dBm to watts (0 dBm = 1 mW). The amp
|
||||||
|
// reports power that way — "fwd=60.5" is 1122 W, not 60 W.
|
||||||
|
func dbmToWatts(dbm float64) float64 {
|
||||||
|
if dbm <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return math.Pow(10, (dbm-30)/10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returnLossToVswr converts the amp's "swr" field — a return loss in dB, sent
|
||||||
|
// negative for a good match — into the VSWR ratio an operator reads.
|
||||||
|
func returnLossToVswr(swrDb float64) float64 {
|
||||||
|
rl := math.Abs(swrDb)
|
||||||
|
if rl <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
rho := math.Pow(10, -rl/20)
|
||||||
|
if rho >= 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
vswr := (1 + rho) / (1 - rho)
|
||||||
|
if vswr > 99.9 || math.IsInf(vswr, 0) || math.IsNaN(vswr) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return vswr
|
||||||
|
}
|
||||||
|
|||||||
+130
-27
@@ -33,13 +33,11 @@ const (
|
|||||||
cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE (confirmed on hw)
|
cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE (confirmed on hw)
|
||||||
cmdStatus byte = 0x90 // request the status string
|
cmdStatus byte = 0x90 // request the status string
|
||||||
|
|
||||||
// Best-guess keystroke codes reconstructed from the APG command table after
|
// From the official APG rev 1.1 command table. Note POWER does NOT switch a
|
||||||
// correcting the PDF's note-wrap misalignment (anchored to the confirmed
|
// sleeping amp on (that is the RTS/DTR wake, see PowerOn) — on a running amp
|
||||||
// OPERATE=0x0D): OFF=0x0A, POWER=0x0B. The POWER key doubles as power-on (when
|
// it cycles the output level L→M→H, which is all we use it for.
|
||||||
// the amp is off) and the output-level cycle L→M→H (when it's on) — same as the
|
cmdOff byte = 0x0A // SWITCH OFF key — how PowerOff switches the amp off
|
||||||
// single physical POWER button. To be verified on hardware.
|
cmdPower byte = 0x0B // POWER key — cycles the output power level
|
||||||
cmdOff byte = 0x0A // OFF key — switch the amplifier off
|
|
||||||
cmdPower byte = 0x0B // POWER key — turn on / cycle output power level
|
|
||||||
|
|
||||||
syncHost = 0x55
|
syncHost = 0x55
|
||||||
syncAmp = 0xAA
|
syncAmp = 0xAA
|
||||||
@@ -47,11 +45,16 @@ const (
|
|||||||
dialTimeout = 5 * time.Second
|
dialTimeout = 5 * time.Second
|
||||||
ioTimeout = 3 * time.Second
|
ioTimeout = 3 * time.Second
|
||||||
pollEvery = 800 * time.Millisecond
|
pollEvery = 800 * time.Millisecond
|
||||||
|
|
||||||
|
// How long a modem line is held LOW before being raised again, to give the
|
||||||
|
// amp's edge-triggered remote-on input a transition it can see.
|
||||||
|
wakePulse = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status is the decoded amplifier state for the UI.
|
// Status is the decoded amplifier state for the UI.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
|
Transport string `json:"transport"` // "serial" | "tcp" — the UI enables power-ON on serial even when asleep
|
||||||
LastError string `json:"last_error,omitempty"`
|
LastError string `json:"last_error,omitempty"`
|
||||||
Model string `json:"model,omitempty"` // "20K" / "13K"
|
Model string `json:"model,omitempty"` // "20K" / "13K"
|
||||||
Operate bool `json:"operate"` // true = OPERATE, false = STANDBY
|
Operate bool `json:"operate"` // true = OPERATE, false = STANDBY
|
||||||
@@ -91,6 +94,8 @@ type Client struct {
|
|||||||
|
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
running bool
|
running bool
|
||||||
|
|
||||||
|
lastConnErr string // last connect failure logged (log only on change)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg Config) *Client {
|
func New(cfg Config) *Client {
|
||||||
@@ -123,7 +128,13 @@ func (c *Client) Stop() {
|
|||||||
func (c *Client) GetStatus() Status {
|
func (c *Client) GetStatus() Status {
|
||||||
c.statusMu.RLock()
|
c.statusMu.RLock()
|
||||||
defer c.statusMu.RUnlock()
|
defer c.statusMu.RUnlock()
|
||||||
return c.status
|
s := c.status
|
||||||
|
if c.cfg.Transport == "tcp" {
|
||||||
|
s.Transport = "tcp"
|
||||||
|
} else {
|
||||||
|
s.Transport = "serial"
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) setErr(err error) {
|
func (c *Client) setErr(err error) {
|
||||||
@@ -146,27 +157,79 @@ func (c *Client) Operate(on bool) error {
|
|||||||
// ToggleOperate flips STANDBY/OPERATE unconditionally.
|
// ToggleOperate flips STANDBY/OPERATE unconditionally.
|
||||||
func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) }
|
func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) }
|
||||||
|
|
||||||
// PowerOn turns the amplifier on. Per the SPE manual this is NOT a data command
|
// PowerOn switches the amplifier on with a RISING EDGE on RTS, then on DTR.
|
||||||
// but a hardware DTR pulse on the serial line (the "Remote_ON" pin), which needs
|
//
|
||||||
// the special SPE cable — so it only applies to the serial transport. Over TCP
|
// Confirmed on a live 1.3K-FA. The remote-on input is EDGE-triggered, which is
|
||||||
// there is no DTR line and power-on isn't possible remotely.
|
// why holding the lines high does nothing: a plain port open already leaves both
|
||||||
|
// high (the serial library's default), so there is no transition left to give.
|
||||||
|
// Hence low → pause → high on each line, in that order. The keystroke route is a
|
||||||
|
// dead end — a switched-off amp does not answer its UART at all, and the POWER
|
||||||
|
// key (0x0B) merely cycles L/M/H on an amp that is already running.
|
||||||
|
//
|
||||||
|
// Serial only: over TCP there are no modem lines. The port is (re)opened first,
|
||||||
|
// since the poll loop has dropped the connection to a sleeping amp long before
|
||||||
|
// the user clicks ON.
|
||||||
func (c *Client) PowerOn() error {
|
func (c *Client) PowerOn() error {
|
||||||
c.mu.Lock()
|
if c.cfg.Transport == "tcp" {
|
||||||
defer c.mu.Unlock()
|
return fmt.Errorf("power-on needs the serial RTS/DTR lines; not available over a network bridge")
|
||||||
sp, ok := c.conn.(serial.Port)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("power-on needs the serial DTR line (special SPE cable); not available over network")
|
|
||||||
}
|
}
|
||||||
// A single positive pulse (~1.2s) on DTR, as the manual describes.
|
// On an amp that is already awake there is nothing to wake.
|
||||||
if err := sp.SetDTR(true); err != nil {
|
if c.GetStatus().Connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// The poll goroutine may still be inside a blocking read on the old handle;
|
||||||
|
// Windows keeps the port "busy" until that read times out (ioTimeout). Retry
|
||||||
|
// the open for a little longer than that.
|
||||||
|
var err error
|
||||||
|
deadline := time.Now().Add(ioTimeout + 3*time.Second)
|
||||||
|
for {
|
||||||
|
if err = c.ensureConn(); err == nil || time.Now().After(deadline) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
applog.Printf("spe: power ON failed — cannot open %s: %v", c.cfg.ComPort, err)
|
||||||
|
return fmt.Errorf("power-on: %w", err)
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
sp, ok := c.conn.(serial.Port)
|
||||||
|
c.mu.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("power-on needs a serial connection")
|
||||||
|
}
|
||||||
|
pulse := func(set func(bool) error) error {
|
||||||
|
if err := set(false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
time.Sleep(1200 * time.Millisecond)
|
time.Sleep(wakePulse)
|
||||||
return sp.SetDTR(false)
|
return set(true)
|
||||||
|
}
|
||||||
|
if err = pulse(sp.SetRTS); err == nil {
|
||||||
|
err = pulse(sp.SetDTR)
|
||||||
|
}
|
||||||
|
// Booting, the amp re-enumerates its USB interface, which leaves this handle
|
||||||
|
// pointing at a device that no longer exists — the amp came up and OpsLog
|
||||||
|
// still read "offline". Drop it; the poll loop reopens a fresh one as soon as
|
||||||
|
// the port is back.
|
||||||
|
c.mu.Lock()
|
||||||
|
c.dropLocked()
|
||||||
|
c.mu.Unlock()
|
||||||
|
applog.Printf("spe: power ON — RTS then DTR pulsed on %s (err=%v)", c.cfg.ComPort, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PowerOff presses the OFF key (switches the amp off).
|
// PowerOff presses the SWITCH OFF key (0x0A) — the amp is running, so it hears
|
||||||
func (c *Client) PowerOff() error { return c.sendCmd(cmdOff) }
|
// its UART. Dropping DTR then RTS switches it off too (it is the mirror of the
|
||||||
|
// wake, and does work when done by hand), but back-to-back from one click it
|
||||||
|
// did not, while this keystroke has never once failed. The amp then stays off:
|
||||||
|
// the poll loop keeps reopening the port with both lines high and that has
|
||||||
|
// never woken it — only the deliberate low→high sequence in PowerOn does.
|
||||||
|
func (c *Client) PowerOff() error {
|
||||||
|
err := c.sendCmd(cmdOff)
|
||||||
|
applog.Printf("spe: power OFF — SWITCH OFF key sent (err=%v)", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// SetPowerLevel cycles the output power level (L/M/H) to the requested one by
|
// SetPowerLevel cycles the output power level (L/M/H) to the requested one by
|
||||||
// tapping the POWER key (0x0B) and WAITING for the amp to actually report the new
|
// tapping the POWER key (0x0B) and WAITING for the amp to actually report the new
|
||||||
@@ -206,9 +269,16 @@ func (c *Client) pollLoop() {
|
|||||||
return
|
return
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
if err := c.ensureConn(); err != nil {
|
if err := c.ensureConn(); err != nil {
|
||||||
|
// Logged once per distinct message: "the amp is on but OpsLog
|
||||||
|
// says offline" was impossible to diagnose without the reason.
|
||||||
|
if msg := err.Error(); msg != c.lastConnErr {
|
||||||
|
c.lastConnErr = msg
|
||||||
|
applog.Printf("spe: connect %s failed: %s", c.cfg.ComPort, msg)
|
||||||
|
}
|
||||||
c.setErr(fmt.Errorf("connect: %w", err))
|
c.setErr(fmt.Errorf("connect: %w", err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
c.lastConnErr = ""
|
||||||
// The amp streams status frames faster than one per poll, so a backlog
|
// The amp streams status frames faster than one per poll, so a backlog
|
||||||
// builds up and we'd read stale frames (the display lagged ~15s behind
|
// builds up and we'd read stale frames (the display lagged ~15s behind
|
||||||
// the real amp). Flush anything pending, THEN request + read exactly one
|
// the real amp). Flush anything pending, THEN request + read exactly one
|
||||||
@@ -239,16 +309,48 @@ func (c *Client) ensureConn() error {
|
|||||||
nc, err = net.DialTimeout("tcp", net.JoinHostPort(c.cfg.Host, strconv.Itoa(c.cfg.Port)), dialTimeout)
|
nc, err = net.DialTimeout("tcp", net.JoinHostPort(c.cfg.Host, strconv.Itoa(c.cfg.Port)), dialTimeout)
|
||||||
rwc = nc
|
rwc = nc
|
||||||
} else {
|
} else {
|
||||||
rwc, err = serial.Open(c.cfg.ComPort, &serial.Mode{BaudRate: c.cfg.Baud})
|
var sp serial.Port
|
||||||
|
// Plain open: both modem lines come up high (the library's default), which
|
||||||
|
// is what the interface needs to talk. That is not enough to switch the amp
|
||||||
|
// on — the remote-on input wants the deliberate low→high sequence PowerOn
|
||||||
|
// sends — so reconnecting never wakes an amplifier the operator switched
|
||||||
|
// off, and forcing either line low here would switch a running one off.
|
||||||
|
sp, err = serial.Open(c.cfg.ComPort, &serial.Mode{BaudRate: c.cfg.Baud})
|
||||||
|
if err == nil {
|
||||||
|
// Without this, a read on a sleeping amp blocks forever inside the
|
||||||
|
// Windows serial driver — the poll goroutine survived the window and
|
||||||
|
// the process took ~a minute to die.
|
||||||
|
_ = sp.SetReadTimeout(ioTimeout)
|
||||||
|
rwc = sp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.conn = rwc
|
c.conn = rwc
|
||||||
|
if sp, ok := rwc.(serial.Port); ok {
|
||||||
|
// The driver reports a read timeout as "0 bytes, no error", which bufio
|
||||||
|
// spins on; surface it as a real error so the poll loop drops and retries.
|
||||||
|
c.r = bufio.NewReader(serialTimeoutReader{sp})
|
||||||
|
} else {
|
||||||
c.r = bufio.NewReader(rwc)
|
c.r = bufio.NewReader(rwc)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serialTimeoutReader converts the serial driver's timeout convention (n=0,
|
||||||
|
// err=nil once SetReadTimeout expires) into an explicit error, so a bufio
|
||||||
|
// reader fails fast instead of retrying an amp that is asleep.
|
||||||
|
type serialTimeoutReader struct{ p serial.Port }
|
||||||
|
|
||||||
|
func (s serialTimeoutReader) Read(b []byte) (int, error) {
|
||||||
|
n, err := s.p.Read(b)
|
||||||
|
if n == 0 && err == nil {
|
||||||
|
return 0, fmt.Errorf("spe: read timeout")
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) dropLocked() {
|
func (c *Client) dropLocked() {
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
@@ -361,9 +463,10 @@ func (c *Client) decodeCSV(payload string) {
|
|||||||
|
|
||||||
c.statusMu.Lock()
|
c.statusMu.Lock()
|
||||||
defer c.statusMu.Unlock()
|
defer c.statusMu.Unlock()
|
||||||
// Log the raw status frame ONCE per change, so field alignment can be verified
|
// One raw frame per session is enough to verify field alignment against real
|
||||||
// against real hardware (the doc's 19-field layout may differ per firmware).
|
// hardware — the frame carries live meter values, so logging each change
|
||||||
if payload != c.lastRaw {
|
// meant logging nearly every frame.
|
||||||
|
if c.lastRaw == "" {
|
||||||
c.lastRaw = payload
|
c.lastRaw = payload
|
||||||
applog.Printf("spe: status raw=%q fields=%d", payload, len(f))
|
applog.Printf("spe: status raw=%q fields=%d", payload, len(f))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type Status struct {
|
|||||||
|
|
||||||
FwdDbm float64 `json:"fwd_dbm"` // forward power [dBm], as reported
|
FwdDbm float64 `json:"fwd_dbm"` // forward power [dBm], as reported
|
||||||
FwdW float64 `json:"fwd_w"` // forward power [W], derived from dBm
|
FwdW float64 `json:"fwd_w"` // forward power [W], derived from dBm
|
||||||
|
PeakW float64 `json:"peak_w"` // the device's own peak forward power [W] — what the meters show
|
||||||
SwrDb float64 `json:"swr_db"` // return loss [dB] as reported (negative = good match)
|
SwrDb float64 `json:"swr_db"` // return loss [dB] as reported (negative = good match)
|
||||||
Vswr float64 `json:"vswr"` // VSWR ratio, derived from swr_db (1.0 = perfect)
|
Vswr float64 `json:"vswr"` // VSWR ratio, derived from swr_db (1.0 = perfect)
|
||||||
|
|
||||||
@@ -405,7 +406,9 @@ func (c *Client) parse(resp string) {
|
|||||||
if !strings.HasPrefix(data, "status") {
|
if !strings.HasPrefix(data, "status") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if data != c.lastRaw {
|
// One raw frame per session is enough to verify field alignment — the
|
||||||
|
// frames carry live meter values, so "log on change" logged every frame.
|
||||||
|
if c.lastRaw == "" {
|
||||||
c.lastRaw = data
|
c.lastRaw = data
|
||||||
applog.Printf("tunergenius: status raw=%q", data)
|
applog.Printf("tunergenius: status raw=%q", data)
|
||||||
}
|
}
|
||||||
@@ -439,6 +442,13 @@ func (c *Client) applyStatus(data string) {
|
|||||||
c.status.FwdDbm = v
|
c.status.FwdDbm = v
|
||||||
c.status.FwdW = dbmToWatts(v)
|
c.status.FwdW = dbmToWatts(v)
|
||||||
}
|
}
|
||||||
|
// The device's OWN peak detector. A poll samples one instant of the envelope,
|
||||||
|
// so on SSB the plain "fwd" figure lands between syllables as often as on a
|
||||||
|
// peak — the widget read a few hundred watts on a kilowatt transmission. This
|
||||||
|
// is the number to display.
|
||||||
|
if v, ok := parseFloat(kv["peak"]); ok {
|
||||||
|
c.status.PeakW = dbmToWatts(v)
|
||||||
|
}
|
||||||
if v, ok := parseFloat(kv["swr"]); ok {
|
if v, ok := parseFloat(kv["swr"]); ok {
|
||||||
c.status.SwrDb = v
|
c.status.SwrDb = v
|
||||||
c.status.Vswr = returnLossToVswr(v)
|
c.status.Vswr = returnLossToVswr(v)
|
||||||
|
|||||||
+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.22.8"
|
appVersion = "0.22.9"
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@@ -1,13 +1,47 @@
|
|||||||
# Amplifiers and Switches
|
# Amplifiers and Switches
|
||||||
|
|
||||||
## PowerGenius XL (4O3A) amplifier
|
## Amplifiers
|
||||||
|
|
||||||
Direct TCP connection (Settings → PowerGenius):
|
Settings → **Amplifier** is a **list**: configure several amps (e.g. two SPEs
|
||||||
|
run in parallel), each with its own name and connection. Brands:
|
||||||
|
|
||||||
- **Operate / Standby** and **fault** display.
|
- **SPE Expert** (1.3K / 1.5K / 2K) — USB serial or an RS232-to-Ethernet
|
||||||
- **Fan-mode** selector (Standard / Contest / Broadcast).
|
bridge. OPERATE/STANDBY, ON/OFF, Low/Mid/High power level, live status.
|
||||||
- When a Flex is connected, the amp controls + its **FWD / ID / TEMP** meters
|
Switching the amplifier **on and off** uses the remote-on control lines, so
|
||||||
appear in the FlexRadio panel's Amplifier card. See [[FlexRadio]].
|
it needs the **USB (or RS-232) connection**, not a network bridge — the ON
|
||||||
|
button works even while the amplifier is off and reporting nothing.
|
||||||
|
- **ACOM** (500S / 600S / 700S / 1200S / 2020S) — serial or bridge.
|
||||||
|
OPERATE/STANDBY/OFF, live power / SWR / PA temperature / band / fan.
|
||||||
|
Power-ON works over serial when the cable wires the DTR/RTS pins.
|
||||||
|
- **PowerGenius XL** (4O3A) — direct TCP. Operate/Standby, fault display, and a
|
||||||
|
**fan-mode** selector (Standard / Contest / Broadcast).
|
||||||
|
|
||||||
|
Where the controls appear:
|
||||||
|
|
||||||
|
- An **amplifier card** in the FlexRadio panel and in **Station Control**:
|
||||||
|
OPERATE/STANDBY, ON/OFF, power level, output-power bar and live FWD /
|
||||||
|
drain-current / temperature meters. Several amps → one card each.
|
||||||
|
- A **status chip per amp** in the bottom bar: green ON AIR = OPERATE, orange =
|
||||||
|
STANDBY, red = offline. Clicking it toggles OPERATE/STANDBY.
|
||||||
|
- A **docked widget** (flame icon, next to the keyers): Operate, power ON/OFF,
|
||||||
|
the SPE power level and live watts / SWR / temperature, in the narrow column
|
||||||
|
format of the Tuner Genius widget. With several amplifiers configured, a
|
||||||
|
dropdown on the icon picks which one it shows — or **All**.
|
||||||
|
|
||||||
|
### Band-follow (amp on a second COM port)
|
||||||
|
|
||||||
|
An ACOM or SPE that normally polls the radio for its frequency can follow
|
||||||
|
OpsLog instead: OpsLog answers the amp's polls **as a Kenwood rig** on a second
|
||||||
|
COM port (Settings → Amplifier → frequency port). Useful when the real radio's
|
||||||
|
CAT port is already taken by OpsLog itself.
|
||||||
|
|
||||||
|
## Tuner Genius XL (4O3A)
|
||||||
|
|
||||||
|
Settings → Tuner Genius (IP only; port fixed at 9010). Live **SWR and forward
|
||||||
|
power** with peak-hold, **Tune / Bypass / Operate-Standby**, and the two
|
||||||
|
channels A/B (source, frequency, antenna) shown and click-selectable. Available
|
||||||
|
as a docked widget, a card in the FlexRadio panel and a card in Station
|
||||||
|
Control. Direct TCP — uses one of the box's connection slots.
|
||||||
|
|
||||||
## Antenna Genius (4O3A) antenna switch
|
## Antenna Genius (4O3A) antenna switch
|
||||||
|
|
||||||
@@ -16,8 +50,20 @@ Over TCP / GSCP — a docked **A/B antenna-switch** widget:
|
|||||||
- One row per configured antenna, with a **Port A** and **Port B** button.
|
- One row per configured antenna, with a **Port A** and **Port B** button.
|
||||||
- Colours: green = selected on port A, blue = selected on port B, red (pulsing) =
|
- Colours: green = selected on port A, blue = selected on port B, red (pulsing) =
|
||||||
that port is transmitting. Clicking a selected port deselects it (→ None).
|
that port is transmitting. Clicking a selected port deselects it (→ None).
|
||||||
- **Band filter** (funnel icon in the header): shows only the antennas configured
|
- **Band filter** (funnel icon): shows only the antennas configured for the
|
||||||
for the current band, like the native app. Toggle it off to see all antennas.
|
current band. Antennas with an unknown band mask are always shown.
|
||||||
Antennas with an unknown band mask are always shown.
|
|
||||||
|
|
||||||
Configure the host / password in Settings → Antenna Genius.
|
Configure the host / password in Settings → Antenna Genius.
|
||||||
|
|
||||||
|
## Relay boards (Station Control)
|
||||||
|
|
||||||
|
USB relay boards for antenna/accessory switching, with named buttons in Station
|
||||||
|
Control: **Denkovi** (4/8 relays, FT245) and generic **CH340 / LCUS**
|
||||||
|
USB-serial boards. A Test-connection button and detection feedback live in the
|
||||||
|
setup panel.
|
||||||
|
|
||||||
|
## Station Control
|
||||||
|
|
||||||
|
The Station Control tab is a dashboard of fixed-width panels that wrap to fill
|
||||||
|
the window — amplifier(s), tuner, relays, rotator, Ultrabeam… Drag a panel by
|
||||||
|
the grip on its left edge to reorder; the order is remembered.
|
||||||
|
|||||||
+53
-16
@@ -2,35 +2,72 @@
|
|||||||
|
|
||||||
## CW keyer
|
## CW keyer
|
||||||
|
|
||||||
A CW keyer with **macros** and F-key macros. The keyer **engine** is selectable
|
A CW keyer with **macros** on F-keys (F1–F9; empty slots are hidden). The keyer
|
||||||
(Settings → CW Keyer):
|
**engine** is selectable (Settings → CW Keyer):
|
||||||
|
|
||||||
- **WinKeyer** — K1EL WK1/2/3 over a COM port.
|
- **WinKeyer** — K1EL WK1/2/3 over a COM port. The log names the generation
|
||||||
|
detected, and a byte-level trace can be enabled for bug reports.
|
||||||
|
- **Serial port (DTR=CW / RTS=PTT)** — OpsLog keys CW by toggling a serial
|
||||||
|
control line itself, the hardware method N1MM/WSJT-X use with a Yaesu SCU-17
|
||||||
|
or a generic keying interface. No WinKeyer needed. Choose the COM port, which
|
||||||
|
line carries CW, and *Invert keying* for interfaces that key backwards. This
|
||||||
|
is also the engine for an **FTDX10** (whose CAT refuses `KY`), on the rig's
|
||||||
|
second COM port.
|
||||||
- **Icom** — the radio's own keyer over CI-V, no extra hardware. Works over the
|
- **Icom** — the radio's own keyer over CI-V, no extra hardware. Works over the
|
||||||
remote link too. (Requires the Icom CAT backend; set break-in to SEMI/FULL or
|
remote link too. (Set break-in to SEMI/FULL or the rig keys the sidetone but
|
||||||
the rig keys the sidetone but stays in RX.)
|
stays in RX.)
|
||||||
|
- **FlexRadio (CWX)** — keys CW straight over the SmartSDR connection, with
|
||||||
|
type-ahead and mid-send backspace.
|
||||||
|
- **Yaesu (rig keyer)** — the radio's internal keyer over CAT (`KY`), for the
|
||||||
|
FTDX101 / FT-991A / FT-710 family. See [[Yaesu]].
|
||||||
- **TCI** — for TCI backends.
|
- **TCI** — for TCI backends.
|
||||||
|
|
||||||
|
**ESC** stops CW on whichever engine is active; typing a callsign aborts a
|
||||||
|
running macro. `<LOGQSO>` inside a macro logs the contact at that exact point.
|
||||||
|
**Auto-call** repeats a CQ with the gap you set measured *after* the message.
|
||||||
|
|
||||||
|
### ESM — Enter Sends Message
|
||||||
|
|
||||||
|
N1MM-style, for CW (Settings → CW Keyer). With the keyer on, **Enter** fires a
|
||||||
|
macro by QSO stage instead of logging: empty callsign → F1 (CQ); callsign
|
||||||
|
entered → F2 (report), focus jumps to RST; Enter in RST → F3 (TU), which logs
|
||||||
|
if the macro contains `<LOGQSO>`.
|
||||||
|
|
||||||
## Digital Voice Keyer (DVK)
|
## Digital Voice Keyer (DVK)
|
||||||
|
|
||||||
Record **F1–F6** voice messages and transmit them. Set the audio devices, PTT
|
Record **F1–F6** voice messages and transmit them. Set the audio devices, PTT
|
||||||
method and gains in Settings → Audio.
|
method and gains in Settings → Audio.
|
||||||
|
|
||||||
- **PTT** method: CAT, RTS, DTR, or none (VOX).
|
- **PTT** method: CAT (whichever backend is active — every one supports PTT),
|
||||||
- **Test PTT** keys the rig briefly. On CAT/OmniRig the key is held ~1.5 s so the
|
RTS, DTR, or none (VOX).
|
||||||
transmit command is actually sent (OmniRig is asynchronous and coalesces rapid
|
- **Auto CQ** repeats a CQ-labelled message on a timer until you stop it.
|
||||||
writes).
|
- The DVK refuses to transmit on non-phone modes — no speech on FT8.
|
||||||
|
- A **message level** control (Settings → Audio) drives the rig at the right
|
||||||
|
level even from a quietly recorded microphone. If the rig transmits almost
|
||||||
|
nothing, check its rear-input setting (on an FTDX10: SSB MOD SOURCE = REAR).
|
||||||
|
|
||||||
## QSO audio recording
|
## QSO audio recording
|
||||||
|
|
||||||
Continuous rolling capture. On **Log QSO** the contact is saved to a per-QSO WAV
|
Continuous rolling capture. On **Log QSO** the contact is saved to a per-QSO
|
||||||
(`CALL_YYYYMMDD_HHMMSS.wav`), mixing RX + mic. Configure the recordings folder,
|
WAV/MP3 (`CALL_YYYYMMDD_HHMMSS`), with pre-roll. With automatic recording off, a
|
||||||
format (WAV / MP3), pre-roll and gains in Settings → Audio.
|
**red dot** beside the callsign records the contact by hand.
|
||||||
|
|
||||||
|
A recording can be **replayed on the air** to the station being worked, like a
|
||||||
|
voice-keyer message — with its **own level**, separate from the voice keyer (a
|
||||||
|
receiver line-out needs far less gain than a microphone). The play button
|
||||||
|
becomes a **stop** button while it transmits. Hidden on CW.
|
||||||
|
|
||||||
|
## CW decoder (RX audio → text)
|
||||||
|
|
||||||
|
**Tools → CW decoder** (or the ear button) decodes received CW while the mode
|
||||||
|
is CW: adaptive envelope with noise squelch, per-character dit/dah
|
||||||
|
classification, follows 12–40 WPM and sloppy fists, rides QSB. Shows WPM /
|
||||||
|
pitch / level, locks onto the FlexRadio CW pitch automatically, and clicking a
|
||||||
|
decoded word fills the callsign. Clean-to-moderate signals decode solidly;
|
||||||
|
weak-signal pileups remain CW Skimmer territory.
|
||||||
|
|
||||||
## RX audio monitor (USB rigs)
|
## RX audio monitor (USB rigs)
|
||||||
|
|
||||||
Settings → Audio → **Listen to radio** pipes the rig's received audio (its "USB
|
Settings → Audio → **Listen to radio** pipes the rig's received audio (its "USB
|
||||||
Audio CODEC" input) through your speakers, so you hear the radio inside OpsLog.
|
Audio CODEC" input) through your speakers — the level applies live as you drag
|
||||||
**Talk to radio** keys PTT and pipes your mic to the rig's audio input. (Network
|
it. **Talk to radio** keys PTT and pipes your mic to the rig's audio input.
|
||||||
audio for remote Icom is a work in progress — see
|
|
||||||
[[Remote Icom over the Internet]].)
|
|
||||||
|
|||||||
+18
-4
@@ -6,8 +6,15 @@ shared **globally** across profiles.
|
|||||||
## Built-in awards
|
## Built-in awards
|
||||||
|
|
||||||
DXCC, WAS / WAZ / WAC, WPX, IOTA / POTA / SOTA / WWFF, **DDFM** (French
|
DXCC, WAS / WAZ / WAC, WPX, IOTA / POTA / SOTA / WWFF, **DDFM** (French
|
||||||
departments), and more — tracked **worked / confirmed / validated** by band and
|
departments), **DLD** (DARC DOK), **H26** (Helvetia 26 cantons), the US
|
||||||
mode.
|
counties (USA-CA), and more — tracked **worked / confirmed / validated** by
|
||||||
|
band and mode.
|
||||||
|
|
||||||
|
The awards panel has a **mode filter** (All / CW / Phone / Digital) that
|
||||||
|
applies to the whole award — bands, counts, confirmations and the contacts
|
||||||
|
behind a cell — and stacks with the worked/confirmed filter, so "worked on CW
|
||||||
|
but not confirmed on CW" is one click. Reference lists sort by reference or by
|
||||||
|
description.
|
||||||
|
|
||||||
## How matching works
|
## How matching works
|
||||||
|
|
||||||
@@ -121,13 +128,20 @@ drop off the list. This is how you close the gaps a matcher can't fill on its ow
|
|||||||
|
|
||||||
## Live detection & manual refs
|
## Live detection & manual refs
|
||||||
|
|
||||||
- References are detected **live** as you enter a callsign.
|
- References are detected **live** as you enter a callsign — from the looked-up
|
||||||
|
address *and* from the QTH, name, country, comment, note or grid you typed.
|
||||||
|
- An award can be marked **one reference per QSO** (used by DLD). When several
|
||||||
|
references then match the same contact, none is assigned silently — the
|
||||||
|
candidates are offered at log time and you pick. Off by default, so an n-fer
|
||||||
|
POTA activation still counts every park.
|
||||||
- You can **manually assign** a reference to a QSO (the *Award Refs* tab of the
|
- You can **manually assign** a reference to a QSO (the *Award Refs* tab of the
|
||||||
QSO editor). It's stored in an ADIF extra (`APP_OPSLOG_AWARDREFS`) so it
|
QSO editor). It's stored in an ADIF extra (`APP_OPSLOG_AWARDREFS`) so it
|
||||||
survives export/import — see [[Import and Export ADIF]] — and is honoured
|
survives export/import — see [[Import and Export ADIF]] — and is honoured
|
||||||
everywhere (award panel, grid columns, totals). For a list-backed award the
|
everywhere (award panel, grid columns, totals). For a list-backed award the
|
||||||
assigned reference must still be a **valid, listed** reference, and the QSO
|
assigned reference must still be a **valid, listed** reference, and the QSO
|
||||||
must be **in scope** (rule 5) for it to count.
|
must be **in scope** (rule 5) for it to count. A hand-assigned reference
|
||||||
|
**replaces** what the matcher found, so a correction survives the next
|
||||||
|
recompute.
|
||||||
|
|
||||||
## Reference lists & display
|
## Reference lists & display
|
||||||
|
|
||||||
|
|||||||
+52
-14
@@ -1,28 +1,46 @@
|
|||||||
# CAT Control
|
# CAT Control
|
||||||
|
|
||||||
OpsLog has **four native CAT backends** (Settings → CAT). Each auto-reconnects
|
OpsLog has **eight CAT backends** (Settings → CAT). Each auto-reconnects and
|
||||||
and connects *non-blocking*, so a powered-off radio never freezes the app.
|
connects *non-blocking*, so a powered-off radio never freezes the app. Backends
|
||||||
|
are named by **how the radio is reached**:
|
||||||
|
|
||||||
| Backend | Use it for |
|
| Backend | Use it for |
|
||||||
|---------|-----------|
|
|---------|-----------|
|
||||||
| **OmniRig** | Any OmniRig-supported rig (Rig 1 / Rig 2, hot-swap). |
|
| **OmniRig** | Any OmniRig-supported rig (Rig 1 / Rig 2, hot-swap). |
|
||||||
| **FlexRadio (SmartSDR)** | FlexRadio 6000/8000 over the TCP API. → [[FlexRadio]] |
|
| **FlexRadio (API)** | FlexRadio 6000/8000 over the TCP API. → [[FlexRadio]] |
|
||||||
| **Icom CI-V** | Any Icom over **USB** *or* over the internet via its **LAN server**. → [[Icom]] · [[Remote Icom over the Internet]] |
|
| **Yaesu (USB)** | FTDX10 / FTDX101 family, direct serial — no OmniRig. → [[Yaesu]] |
|
||||||
|
| **Kenwood (USB, network)** | TS-590 / TS-890 / TS-990 / TS-2000 (Elecraft K3/K4 speak the same dialect), serial or a `host:port` serial bridge. → [[Kenwood and Xiegu]] |
|
||||||
|
| **Xiegu (USB)** | G90 / X6100 / X6200 / X5105 — CI-V with their reduced command set. → [[Kenwood and Xiegu]] |
|
||||||
|
| **Icom (CI-V USB)** | Any Icom over USB. → [[Icom]] |
|
||||||
|
| **Icom (CI-V network)** | An Icom's built-in LAN/internet server, replacing RS-BA1. → [[Remote Icom over the Internet]] |
|
||||||
| **TCI** | SunSDR / ExpertSDR2 and any TCI (WebSocket) server. |
|
| **TCI** | SunSDR / ExpertSDR2 and any TCI (WebSocket) server. |
|
||||||
|
|
||||||
Once connected, frequency / band / mode follow the radio, clicking a cluster spot
|
Once connected, frequency / band / mode follow the radio, clicking a cluster
|
||||||
tunes the rig, and (for Flex) cluster spots can be pushed to the panadapter.
|
spot tunes the rig, and (for Flex) cluster spots can be pushed to the panadapter.
|
||||||
|
|
||||||
|
## Sharing the rig with WSJT-X / JTDX / MSHV (rigctld)
|
||||||
|
|
||||||
|
A native backend holds the radio's serial port **exclusively** — so OpsLog can
|
||||||
|
serve its rig connection to other programs instead: **Settings → CAT → Share
|
||||||
|
CAT**. In WSJT-X / JTDX / MSHV / Log4OM pick rig model **Hamlib NET rigctl** at
|
||||||
|
`127.0.0.1:4532`. Works with every backend and both Hamlib command dialects.
|
||||||
|
|
||||||
|
> Never point two programs at the same COM port. If another logger or WSJT-X
|
||||||
|
> opens the rig's port directly while OpsLog holds it, you get "port busy"
|
||||||
|
> errors, garbled CI-V frames and erratic rig behaviour. One master — OpsLog —
|
||||||
|
> and everyone else through the shared rigctl port.
|
||||||
|
|
||||||
## OmniRig
|
## OmniRig
|
||||||
|
|
||||||
Pick **Rig 1** or **Rig 2** (the two OmniRig slots). Configure the actual COM
|
Pick **Rig 1** or **Rig 2** (the two OmniRig slots). Configure the actual COM
|
||||||
port / baud in OmniRig's own settings. Works with anything OmniRig supports; PTT
|
port / baud in OmniRig's own settings.
|
||||||
keying via CAT is supported where the rig's OmniRig profile exposes it.
|
|
||||||
|
|
||||||
## FlexRadio
|
- **VFO to read** — some OmniRig rig files declare the wrong active VFO (an
|
||||||
|
IC-7610 file declared VFO B). If the frequency looks frozen or follows the
|
||||||
Enter the radio's IP (UDP discovery assists). You get a full SmartSDR-style
|
wrong VFO, force **A** or **B** here.
|
||||||
control tab — see **[[FlexRadio]]**.
|
- OpsLog and OmniRig must run at the **same privilege level** — one elevated
|
||||||
|
("as administrator") and not the other cannot see each other; OpsLog detects
|
||||||
|
and names this case.
|
||||||
|
|
||||||
## Icom CI-V (USB)
|
## Icom CI-V (USB)
|
||||||
|
|
||||||
@@ -31,7 +49,7 @@ control tab — see **[[FlexRadio]]**.
|
|||||||
- **COM port** + **baud** must match the radio (set *CI-V USB Echo Back* **OFF**
|
- **COM port** + **baud** must match the radio (set *CI-V USB Echo Back* **OFF**
|
||||||
on the rig).
|
on the rig).
|
||||||
- The model choice also tailors the console (e.g. attenuator steps: 20 dB on an
|
- The model choice also tailors the console (e.g. attenuator steps: 20 dB on an
|
||||||
IC-7300, 6/12/18 dB on an IC-7610).
|
IC-7300, 6/12/18 dB on an IC-7610/7800).
|
||||||
|
|
||||||
Full console details: **[[Icom]]**.
|
Full console details: **[[Icom]]**.
|
||||||
|
|
||||||
@@ -40,10 +58,30 @@ Full console details: **[[Icom]]**.
|
|||||||
Enter the TCI host + port (default 40001). Gives freq / mode / PTT / split and,
|
Enter the TCI host + port (default 40001). Gives freq / mode / PTT / split and,
|
||||||
optionally, panorama spots.
|
optionally, panorama spots.
|
||||||
|
|
||||||
|
## Serial lines (DTR / RTS)
|
||||||
|
|
||||||
|
Windows raises the DTR and RTS lines when a serial port opens, and some
|
||||||
|
interfaces read one of them as **PTT** — the radio goes into transmit the moment
|
||||||
|
OpsLog connects. Others need the lines **high** to work at all. So:
|
||||||
|
|
||||||
|
- **Xiegu** always lowers both on connect (a G90 behind a DE-19 keyed on open),
|
||||||
|
and has a setting for which line keys the rig.
|
||||||
|
- **Yaesu and Kenwood** have an option (off by default) to lower both lines on
|
||||||
|
connect — turn it on only if your interface transmits as soon as OpsLog opens
|
||||||
|
the port.
|
||||||
|
- **Icom CI-V** has always dropped them.
|
||||||
|
|
||||||
|
## Protocol trace
|
||||||
|
|
||||||
|
Settings → CAT can log the **raw protocol** (every CI-V frame, or every Kenwood
|
||||||
|
command, as hex/text) to the diagnostic log — invaluable when reporting a radio
|
||||||
|
that answers oddly. The checkbox shows whether the trace is really running.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The **digital sub-mode** (FT4 vs FT8) is inferred from the frequency.
|
- The **digital sub-mode** (FT4 vs FT8) is inferred from the frequency.
|
||||||
|
- A radio reporting LSB or USB selects **SSB** in the entry form.
|
||||||
- **Per-band Flex RX/TX antennas** can be configured and are applied
|
- **Per-band Flex RX/TX antennas** can be configured and are applied
|
||||||
automatically on band change (Settings → FlexRadio).
|
automatically on band change (Settings → CAT → FlexRadio).
|
||||||
- Split, RIT/XIT and other rig-specific features are exposed on each backend's
|
- Split, RIT/XIT and other rig-specific features are exposed on each backend's
|
||||||
control tab where supported.
|
control tab where supported.
|
||||||
|
|||||||
@@ -3,27 +3,39 @@
|
|||||||
## Servers
|
## Servers
|
||||||
|
|
||||||
Add multiple cluster servers (Settings). They auto-reconnect; one is the
|
Add multiple cluster servers (Settings). They auto-reconnect; one is the
|
||||||
**master** used for sending commands. The Cluster tab and the Main-view cluster
|
**master** used for sending commands. A quiet node is not treated as a dead one
|
||||||
pane share the same live feed.
|
— silence keeps its session, login and filters. The Cluster tab and the
|
||||||
|
Main-view cluster pane share the same live feed.
|
||||||
|
|
||||||
## Filter sidebar
|
## Filter sidebar
|
||||||
|
|
||||||
A show/hide **filter sidebar** (shared by the Cluster tab and the Main pane):
|
A show/hide **filter sidebar** (shared by the Cluster tab and the Main pane):
|
||||||
|
|
||||||
- **Callsign** search, **hide worked**, **group duplicates**.
|
- **Callsign** search, **hide worked**, **group duplicates**.
|
||||||
- Filter by **band / mode / status / source**.
|
- Filter by **band / mode / status / source**, plus **NEW POTA** and
|
||||||
|
**NEW COUNTY** chips.
|
||||||
|
|
||||||
## Per-spot status and tuning
|
## Per-spot status and tuning
|
||||||
|
|
||||||
Each spot is coloured by **status** relative to your log:
|
Each spot is coloured by **status** relative to your log:
|
||||||
|
|
||||||
- **new** (entity never worked), **new-band**, **new-slot**, **worked**.
|
- **new** (entity never worked), **new-band**, **new-mode**, **new-slot**,
|
||||||
|
**worked**.
|
||||||
|
- Settings → General → **Group digital modes as one (DXCC-style)** makes
|
||||||
|
FT8/FT4/RTTY/PSK count as a single Digital mode for the colouring and the
|
||||||
|
matrix badges, matching how DXCC counts.
|
||||||
|
|
||||||
Click a spot to **tune the rig** (fills the callsign into the entry strip). A
|
Click a spot to **tune the rig** (fills the callsign into the entry strip). A
|
||||||
multi-band **Band Map** shows panadapter-style strips per band.
|
multi-band **Band Map** shows panadapter-style strips per band — its CW / data
|
||||||
|
/ phone sub-bands use their own colour-blind-checked hues, and **Ctrl+↑ /
|
||||||
|
Ctrl+↓** jumps to the next spot above/below the current frequency and tunes it.
|
||||||
|
|
||||||
**POTA** spots are tagged with their park reference (via `api.pota.app`).
|
**POTA** spots are tagged with their park reference (via `api.pota.app`).
|
||||||
|
|
||||||
|
The entry strip's band matrix can also show the **ClubLog Most Wanted rank**
|
||||||
|
(Settings → General): an `MW #rank` pill next to the entity name, personalised
|
||||||
|
to your callsign, refreshed daily.
|
||||||
|
|
||||||
## Sending a spot
|
## Sending a spot
|
||||||
|
|
||||||
Use the **Send Spot** window to announce a DX spot on the master cluster:
|
Use the **Send Spot** window to announce a DX spot on the master cluster:
|
||||||
@@ -35,6 +47,12 @@ frequency** (full resolution, including sub-kHz).
|
|||||||
**Tools → Alert management** — Log4OM-style alert rules on **call / country /
|
**Tools → Alert management** — Log4OM-style alert rules on **call / country /
|
||||||
band / mode / spotter**, with **sound**, **visual** and **e-mail** notification.
|
band / mode / spotter**, with **sound**, **visual** and **e-mail** notification.
|
||||||
Active alerts appear as a discreet bell in the entry strip; click it for the
|
Active alerts appear as a discreet bell in the entry strip; click it for the
|
||||||
list. Alerts are kept briefly and only shown when active.
|
list.
|
||||||
|
|
||||||
|
## External spot sources
|
||||||
|
|
||||||
|
A **DXHunter** remote-call UDP packet fills the callsign, and when it carries
|
||||||
|
`<FREQ>` and `<MODE>` it also tunes whichever CAT backend is active. See
|
||||||
|
[[Settings and Data]] for the UDP configuration.
|
||||||
|
|
||||||
See also: [[Maps and Antennas]] for turning the beam to a spot.
|
See also: [[Maps and Antennas]] for turning the beam to a spot.
|
||||||
|
|||||||
+8
-6
@@ -1,10 +1,11 @@
|
|||||||
# OpsLog
|
# OpsLog
|
||||||
|
|
||||||
A modern, fast ham-radio logger for **Windows** — Log4OM-style entry, real-time
|
A modern, fast ham-radio logger for **Windows** — Log4OM-style entry, real-time
|
||||||
CAT for **OmniRig**, native **FlexRadio/SmartSDR**, native **Icom CI-V** (USB
|
CAT for **OmniRig**, native **FlexRadio/SmartSDR**, **Icom CI-V** (USB *and*
|
||||||
*and* remote-over-internet, replacing RS-BA1) and **TCI** (SunSDR / Expert
|
remote-over-internet, replacing RS-BA1), **Yaesu**, **Kenwood**, **Xiegu** and
|
||||||
Electronics), a DX cluster with spot alerts, awards tracking, maps, contest
|
**TCI** (SunSDR / Expert Electronics) — with **CAT sharing** so WSJT-X & co use
|
||||||
logging, QSL management and a QSL-card designer.
|
the same rig link — a DX cluster with spot alerts, awards tracking, maps,
|
||||||
|
contest logging, QSL management and a QSL-card designer.
|
||||||
|
|
||||||
Built with **Wails v2** (Go backend + React/TypeScript frontend), **pure Go**
|
Built with **Wails v2** (Go backend + React/TypeScript frontend), **pure Go**
|
||||||
(no CGO). SQLite holds the configuration; the logbook can be the local SQLite
|
(no CGO). SQLite holds the configuration; the logbook can be the local SQLite
|
||||||
@@ -18,14 +19,15 @@ Developed by **F4BPO**.
|
|||||||
1. **[[Installation]]** — download / build and first launch.
|
1. **[[Installation]]** — download / build and first launch.
|
||||||
2. **[[Getting Started]]** — the entry strip, logging your first QSO, choosing a
|
2. **[[Getting Started]]** — the entry strip, logging your first QSO, choosing a
|
||||||
profile.
|
profile.
|
||||||
3. **[[CAT Control]]** — connect your radio (OmniRig, FlexRadio, Icom, TCI).
|
3. **[[CAT Control]]** — connect your radio (OmniRig, FlexRadio, Icom, Yaesu,
|
||||||
|
Kenwood, Xiegu, TCI).
|
||||||
|
|
||||||
## What OpsLog does
|
## What OpsLog does
|
||||||
|
|
||||||
| Area | Pages |
|
| Area | Pages |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| Enter and manage QSOs | [[Logging Basics]] · [[Recent QSOs and Filters]] · [[Import and Export ADIF]] |
|
| Enter and manage QSOs | [[Logging Basics]] · [[Recent QSOs and Filters]] · [[Import and Export ADIF]] |
|
||||||
| Control your radio | [[CAT Control]] · [[FlexRadio]] · [[Icom]] · [[Remote Icom over the Internet]] |
|
| Control your radio | [[CAT Control]] · [[FlexRadio]] · [[Icom]] · [[Remote Icom over the Internet]] · [[Yaesu]] · [[Kenwood and Xiegu]] |
|
||||||
| Chase DX | [[DX Cluster and Spots]] · [[Maps and Antennas]] |
|
| Chase DX | [[DX Cluster and Spots]] · [[Maps and Antennas]] |
|
||||||
| Station hardware | [[Amplifiers and Switches]] · [[Audio and Keyers]] |
|
| Station hardware | [[Amplifiers and Switches]] · [[Audio and Keyers]] |
|
||||||
| Events & contests | [[Contest Logging]] · [[Net Control]] · [[Multi-Operator Live Status]] |
|
| Events & contests | [[Contest Logging]] · [[Net Control]] · [[Multi-Operator Live Status]] |
|
||||||
|
|||||||
@@ -8,22 +8,34 @@ export modes.
|
|||||||
|
|
||||||
Import an `.adi` / `.adif` file into the active profile's logbook. On import,
|
Import an `.adi` / `.adif` file into the active profile's logbook. On import,
|
||||||
OpsLog parses every field, maps standard fields to columns and keeps unknown but
|
OpsLog parses every field, maps standard fields to columns and keeps unknown but
|
||||||
valid ADIF fields in a per-QSO **extras** store (editable later). Country / zones
|
valid ADIF fields in a per-QSO **extras** store (editable later). Country /
|
||||||
are enriched from `cty.dat` where missing.
|
zones are enriched from `cty.dat` where missing. A file **without a header**
|
||||||
|
(records pasted from another log) imports fine.
|
||||||
|
|
||||||
|
- **Field remapping**: the import dialog can move a field as it reads it —
|
||||||
|
e.g. the RSGB IOTA contest exports the island reference in `STATE`; add a
|
||||||
|
`STATE → IOTA` row and it lands where the award looks.
|
||||||
|
- **Fill from profile**: optionally stamps your station fields *and* your
|
||||||
|
default confirmation statuses (paper QSL, LoTW, eQSL, Club Log, HRDLog,
|
||||||
|
QRZ.com) on QSOs the file leaves empty — a WSJT-X log carries almost none.
|
||||||
|
Existing values are never overwritten.
|
||||||
|
|
||||||
## Export
|
## Export
|
||||||
|
|
||||||
- **Whole log**, **selected rows**, or the **filtered view** (no limit) — see
|
- **Whole log**, **selected rows**, or the **filtered view** (no limit) — see
|
||||||
[[Recent QSOs and Filters]].
|
[[Recent QSOs and Filters]].
|
||||||
- **Standard** mode exports the common promoted fields; **All** mode also exports
|
- **Standard** mode exports the common promoted fields; **All** mode also
|
||||||
every extra field.
|
exports every extra field; **Choose fields…** opens a picker that separates
|
||||||
|
the official ADIF 3.1.7 fields (by category) from the OpsLog / non-standard
|
||||||
|
tags actually present in your log, with All/None per group. Your selection is
|
||||||
|
remembered.
|
||||||
- **Cabrillo** export for contest submission.
|
- **Cabrillo** export for contest submission.
|
||||||
|
|
||||||
## Round-trip safety
|
## Round-trip safety
|
||||||
|
|
||||||
Award references you assign manually are stored in an ADIF extra field
|
Award references you assign manually are stored in an ADIF extra field
|
||||||
(`APP_OPSLOG_AWARDREFS`), so exporting your full log and re-importing it does not
|
(`APP_OPSLOG_AWARDREFS`), so exporting your full log and re-importing it does
|
||||||
lose your award-reference assignments.
|
not lose your award-reference assignments.
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Kenwood and Xiegu (native CAT)
|
||||||
|
|
||||||
|
## Kenwood (USB, network)
|
||||||
|
|
||||||
|
A native backend talks straight to a **TS-590 / TS-890 / TS-990 / TS-2000**
|
||||||
|
over its serial port — frequency, mode, VFO, split and PTT, no OmniRig in
|
||||||
|
between. **Elecraft K3/K4** speak the same dialect. Pick **Kenwood (USB,
|
||||||
|
network)** in Settings → CAT.
|
||||||
|
|
||||||
|
- **Serial**: COM port + baud.
|
||||||
|
- **Network**: give a `host:port` instead — for a serial bridge (ser2net, an
|
||||||
|
Ethernet-serial adapter). *Not* the radio's own RJ45, which speaks Kenwood's
|
||||||
|
KNS/ARCP protocol.
|
||||||
|
- Split is detected from FR/FT when the radio omits it from its status frame.
|
||||||
|
- The backend was tested end-to-end against a TS-2000 emulator and reports
|
||||||
|
useful errors: a port that opens but stays silent is distinguished from one
|
||||||
|
sending data that never answers (the error quotes what arrived).
|
||||||
|
|
||||||
|
## Xiegu (G90, X6100, X6200, X5105)
|
||||||
|
|
||||||
|
These radios speak CI-V but with a **reduced command set** (no scope, no DSP
|
||||||
|
block, no data mode), so they have their own backend — pick **Xiegu (USB)** in
|
||||||
|
Settings → CAT. Frequency, mode, split and PTT.
|
||||||
|
|
||||||
|
### PTT on a G90 — read this
|
||||||
|
|
||||||
|
A **G90 ignores the CI-V PTT command**. That has three consequences:
|
||||||
|
|
||||||
|
1. In the Xiegu settings, choose **how the rig is keyed**: CI-V command, RTS or
|
||||||
|
DTR — pick the hardware line your interface wires to PTT.
|
||||||
|
2. WSJT-X & co transmit through OpsLog's **shared CAT link** (Hamlib NET rigctl
|
||||||
|
`127.0.0.1:4532` — see [[CAT Control]]): OpsLog converts the PTT request
|
||||||
|
into the hardware line. This is the supported path and it works.
|
||||||
|
3. **WSJT-X + OmniRig will NOT transmit** a G90: OmniRig sends PTT as a CI-V
|
||||||
|
command, which the radio ignores. Either use the native Xiegu backend +
|
||||||
|
rigctl sharing, or configure the PTT line (RTS/DTR) inside OmniRig itself.
|
||||||
|
|
||||||
|
### Keying on connect
|
||||||
|
|
||||||
|
Behind an interface that carries PTT on a modem line (e.g. a DE-19), a G90 used
|
||||||
|
to go **straight into transmit when OpsLog connected** — Windows raises DTR and
|
||||||
|
RTS when a serial port opens. The Xiegu backend now drops both lines on
|
||||||
|
connect, then drives only the line you selected for keying.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Both backends refuse a CI-V/CAT frame read at the wrong offset, so a garbled
|
||||||
|
answer can no longer turn into a phantom frequency.
|
||||||
|
- The protocol trace (Settings → CAT) covers both — see [[CAT Control]].
|
||||||
+31
-8
@@ -10,8 +10,24 @@ save. Fields: callsign, RST tx/rx, name, QTH, grid, band, mode, TX/RX frequency
|
|||||||
CQ/ITU zones, continent) from `cty.dat` including `/MM` `/AM` `/B` and call-area
|
CQ/ITU zones, continent) from `cty.dat` including `/MM` `/AM` `/B` and call-area
|
||||||
handling (`/8`, `/W6`), plus **ClubLog** DXpedition date overrides.
|
handling (`/8`, `/W6`), plus **ClubLog** DXpedition date overrides.
|
||||||
- **Callsign lookup** (QRZ.com / HamQTH) fills name / QTH / grid and shows the
|
- **Callsign lookup** (QRZ.com / HamQTH) fills name / QTH / grid and shows the
|
||||||
photo and a QRZ.com tab. Configure it in Settings → Lookup.
|
photo and a QRZ.com tab. Configure it in Settings → Lookup. Portable calls
|
||||||
|
(`/M`, `/P`) are looked up on the home call too.
|
||||||
|
- **No lookup account?** The name / QTH / locator are recovered from the **last
|
||||||
|
time you worked the station**, and for US calls the offline **FCC (ULS)**
|
||||||
|
database fills state, county and a 6-character grid as you type. A real
|
||||||
|
QRZ/HamQTH hit still wins.
|
||||||
|
- A green **NEW** badge appears on the County field (Info / F2) when the US
|
||||||
|
county has never been worked.
|
||||||
- **ESC** clears the callsign (and stops CW).
|
- **ESC** clears the callsign (and stops CW).
|
||||||
|
- The frequency readout is **scroll-tunable**: roll the wheel over the
|
||||||
|
hundreds / tens / units-of-kHz digit and the rig follows.
|
||||||
|
|
||||||
|
## Super Check Partial / N+1
|
||||||
|
|
||||||
|
Enable in Settings → General (downloads the community MASTER.SCP list). As you
|
||||||
|
type a call, a docked two-column widget shows known calls that **contain** what
|
||||||
|
you typed (Partial) and calls **one character away** (N+1) — click one to fix a
|
||||||
|
busted call.
|
||||||
|
|
||||||
## Editing a QSO
|
## Editing a QSO
|
||||||
|
|
||||||
@@ -22,14 +38,18 @@ fields** editor for any ADIF field not promoted to a column.
|
|||||||
|
|
||||||
## Bulk operations
|
## Bulk operations
|
||||||
|
|
||||||
Select rows in Recent QSOs, right-click:
|
Select rows in Recent QSOs (a **Select all** button takes every displayed row),
|
||||||
|
right-click:
|
||||||
|
|
||||||
- **Fix country & zones** from `cty.dat`, or **update from QRZ / ClubLog**.
|
- **Fix country & zones** from `cty.dat`, or **update from the callsign
|
||||||
- **Bulk edit field** — set one field across many QSOs (QSL/upload status, my
|
databases** (QRZ / ClubLog), with a progress bar.
|
||||||
station, contest, propagation, contacted-station refs, comment…). Great for
|
- **Bulk edit field** — set one field across many QSOs: QSL/confirmation
|
||||||
flipping a batch to *Requested* before an upload.
|
statuses *and dates*, my station, contest, propagation, contacted-station
|
||||||
|
refs, comment, **gridsquare**, even the **frequency** (band recomputed,
|
||||||
|
out-of-band values refused).
|
||||||
- **Send to** a QSL service, **export** selected / filtered to ADIF or Cabrillo,
|
- **Send to** a QSL service, **export** selected / filtered to ADIF or Cabrillo,
|
||||||
**delete**.
|
**delete** (optionally withdrawing the QSO from QRZ.com and Club Log —
|
||||||
|
Settings → External services, off by default).
|
||||||
|
|
||||||
## Find duplicates
|
## Find duplicates
|
||||||
|
|
||||||
@@ -40,6 +60,9 @@ same day or same minute) and lets you pick which to delete.
|
|||||||
|
|
||||||
- **Worked-before matrix** shows prior contacts with the entity per band/mode
|
- **Worked-before matrix** shows prior contacts with the entity per band/mode
|
||||||
slot as you type.
|
slot as you type.
|
||||||
- Awards references are detected **live** as you enter a call (see [[Awards]]).
|
- Award references are detected **live** as you enter a call — including from
|
||||||
|
the QTH, name, comment or grid you typed (see [[Awards]]).
|
||||||
|
- The callsign in the top bar is the **station switcher**: click it to change
|
||||||
|
profile.
|
||||||
|
|
||||||
See also: [[Recent QSOs and Filters]] · [[Import and Export ADIF]]
|
See also: [[Recent QSOs and Filters]] · [[Import and Export ADIF]]
|
||||||
|
|||||||
+27
-15
@@ -3,36 +3,48 @@
|
|||||||
## Main view
|
## Main view
|
||||||
|
|
||||||
The Main view is **two configurable panes** (per profile, Settings → General →
|
The Main view is **two configurable panes** (per profile, Settings → General →
|
||||||
*Main view*). Choose from: great-circle map, locator (street) map, the cluster
|
*Main view*), with a **draggable divider** between them (double-click it to
|
||||||
grid, the worked-before grid, recent QSOs, the **FlexRadio** controls, the
|
even them out). Choose from: great-circle map, locator (street) map, the
|
||||||
**Icom** console, or the **Net control** panel.
|
cluster grid, the worked-before grid, recent QSOs, the **FlexRadio** controls,
|
||||||
|
the **Icom** or **Yaesu** console, or the **Net control** panel.
|
||||||
|
|
||||||
## Great-circle map
|
## Great-circle map
|
||||||
|
|
||||||
- Short/long-path **distance & azimuth** to the worked station.
|
- Short/long-path **distance & azimuth** to the worked station.
|
||||||
- Selectable, **key-free** basemaps: Light / Voyager / Street / Satellite (all
|
- Selectable, **key-free** basemaps: Light / Voyager / Street / Satellite.
|
||||||
labelled).
|
|
||||||
- The **antenna beam lobe(s)** are drawn from the rotor azimuth.
|
- The **antenna beam lobe(s)** are drawn from the rotor azimuth.
|
||||||
|
- **Day/night terminator** and twilight band (button at the top right).
|
||||||
|
|
||||||
## Locator (street) map
|
## Locator (street) map
|
||||||
|
|
||||||
An OSM street map centred on the contacted grid square — handy for local /
|
An OSM street map centred on the contacted grid square — handy for local /
|
||||||
portable contacts. No API key.
|
portable contacts. No API key.
|
||||||
|
|
||||||
## Rotor compass
|
## Rotators
|
||||||
|
|
||||||
An azimuthal-equidistant **click-to-turn** compass driven by **PstRotator**. The
|
An azimuthal-equidistant **click-to-turn** compass; the current heading and
|
||||||
current heading and target are shown; click a bearing to turn the rotor.
|
target are shown. Three rotator types (Settings → Rotator):
|
||||||
|
|
||||||
## Ultrabeam
|
- **PstRotator** — via its UDP interface.
|
||||||
|
- **microHAM ARCO** — native, over LAN or USB: set the ARCO's CONTROL PROTOCOL
|
||||||
|
to *Yaesu GS-232A*.
|
||||||
|
- **GS-232A (generic)** — any GS-232A controller, ERC (Easy Rotor Control)
|
||||||
|
included; serial speed selectable. Set an ERC to GS-232 emulation, not
|
||||||
|
Hy-Gain DCU-1.
|
||||||
|
|
||||||
Ultrabeam antennas are supported with three modes — **Normal**, **180° reverse**,
|
## Motorized antennas (Ultrabeam / SteppIR)
|
||||||
|
|
||||||
|
Both follow the **rig frequency** (band change → retune) and retune immediately
|
||||||
|
when you click a spot, with three modes — **Normal**, **180° reverse**,
|
||||||
**Bidirectional**:
|
**Bidirectional**:
|
||||||
|
|
||||||
- The **radiating direction** is shown in green and the **mechanical boom** in
|
- The **radiating direction** is shown in green and the **mechanical boom** in
|
||||||
grey, on both the compass and the map, so you always know where the antenna
|
grey, on both the compass and the map.
|
||||||
actually points.
|
- On a FlexRadio, transmit is **inhibited while the elements move**, and
|
||||||
- The antenna **follows the rig frequency** (band change → retune), and retunes
|
released as soon as the antenna is tuned.
|
||||||
immediately when you click a spot.
|
- **SteppIR — Tunable range** (Settings → Hardware → Antenna, default
|
||||||
|
13–54 MHz): outside it OpsLog leaves the antenna and the TX interlock
|
||||||
|
completely alone, so tuning to 30 m on a 20 m–6 m SteppIR does nothing.
|
||||||
|
|
||||||
Related hardware: [[Amplifiers and Switches]] (Antenna Genius switch).
|
Related hardware: [[Amplifiers and Switches]] (Antenna Genius switch, Tuner
|
||||||
|
Genius, relays).
|
||||||
|
|||||||
@@ -6,18 +6,29 @@ Every setting in OpsLog is **per-profile**: station identity, CAT, lookup, QSL
|
|||||||
defaults, awards display, antennas, and which logbook to use. Create one profile
|
defaults, awards display, antennas, and which logbook to use. Create one profile
|
||||||
per callsign / station / event.
|
per callsign / station / event.
|
||||||
|
|
||||||
Switch or manage profiles in Settings. The active profile decides which logbook
|
The **callsign in the top bar is the station switcher** — click it and pick a
|
||||||
your QSOs go to.
|
profile. Managing profiles is at the bottom of that list (and in Settings).
|
||||||
|
|
||||||
## Config vs. logbook — two different stores
|
## Two separate databases
|
||||||
|
|
||||||
|
Since 0.21.0 the settings and the QSOs live in **separate files**:
|
||||||
|
|
||||||
| Store | What | Where |
|
| Store | What | Where |
|
||||||
|-------|------|-------|
|
|-------|------|-------|
|
||||||
| **Config** | settings, profiles, rigs/antennas, cluster nodes, lookup cache, award lists, QSL templates | always the **local SQLite** file under `data/` |
|
| **Settings database** (`settings.db`, or `opslog.db` on older installs) | settings, profiles, rigs/antennas, cluster nodes, lookup cache, award lists, QSL templates | always the **local SQLite** file under `data/` |
|
||||||
| **Logbook** | your QSOs | where the active profile points — local SQLite **or** shared MySQL |
|
| **Logbook** (`logbook.db`) | your QSOs | where the active profile points — a local SQLite file **or** shared MySQL |
|
||||||
|
|
||||||
Keeping config local means the UI is instant even when the logbook is a far-away
|
Keeping the settings local means the UI is instant even when the logbook is a
|
||||||
MySQL server.
|
far-away MySQL server. Settings → Database shows the two as separate sections,
|
||||||
|
with an *Open folder* shortcut; a logbook file can be **renamed or relocated**
|
||||||
|
(its QSOs move with it), and the status bar names the **logbook** file — that
|
||||||
|
is the one to back up.
|
||||||
|
|
||||||
|
A profile can point at its **own logbook file** — ideal for a visiting
|
||||||
|
operator, whose contacts stay out of your log without touching your settings.
|
||||||
|
|
||||||
|
**Portable**: paths inside the OpsLog folder are stored relative to it, so
|
||||||
|
moving the whole folder to another drive or PC loses nothing.
|
||||||
|
|
||||||
## Shared MySQL logbook (multi-operator)
|
## Shared MySQL logbook (multi-operator)
|
||||||
|
|
||||||
@@ -25,11 +36,18 @@ Point a profile's logbook at a **MySQL** database so several operators run **one
|
|||||||
log** — e.g. a multi-op special-event call. Configure the host / database /
|
log** — e.g. a multi-op special-event call. Configure the host / database /
|
||||||
credentials in the profile's database settings.
|
credentials in the profile's database settings.
|
||||||
|
|
||||||
This is also what powers [[Multi-Operator Live Status]] (each instance heartbeats
|
- The database and tables are converted to **utf8mb4** on connect, so Cyrillic
|
||||||
its activity into a `live_status` table).
|
or Polish characters from a QRZ lookup store correctly even when the hosting
|
||||||
|
panel created the database as latin1.
|
||||||
|
- An FT8/MSHV QSO that cannot reach a laggy shared database is **parked and
|
||||||
|
re-logged automatically** once it recovers — no lost QSOs.
|
||||||
|
- Switching to a profile whose MySQL is unreachable shows a clear warning
|
||||||
|
instead of silently staying on the previous logbook.
|
||||||
|
|
||||||
|
This is also what powers [[Multi-Operator Live Status]].
|
||||||
|
|
||||||
## Backups
|
## Backups
|
||||||
|
|
||||||
Optional **database + ADIF backup at shutdown** (Settings → Backup). Regardless,
|
Optional **database + ADIF backup at shutdown** (Settings → Backup). Regardless,
|
||||||
copying the `data/` folder backs up your config and local logbook. See
|
copying the `data/` folder backs up your settings and local logbook. See
|
||||||
[[Settings and Data]].
|
[[Settings and Data]].
|
||||||
|
|||||||
@@ -4,13 +4,23 @@
|
|||||||
|
|
||||||
The main log table. Double-click a row to edit; right-click for bulk actions
|
The main log table. Double-click a row to edit; right-click for bulk actions
|
||||||
(see [[Logging Basics]]). Columns are configurable — click **Columns** to choose
|
(see [[Logging Basics]]). Columns are configurable — click **Columns** to choose
|
||||||
which are visible; your selection is saved (per profile).
|
which are visible; widths, order and hidden columns are saved (per profile),
|
||||||
|
with a separate layout for the narrower Main-tab pane.
|
||||||
|
|
||||||
|
- The selected-row count is always visible at the top, and a **Select all /
|
||||||
|
Unselect all** button takes every displayed row.
|
||||||
|
- QSL and upload status columns are coloured: **Y** green, **N** red, **R**
|
||||||
|
blue.
|
||||||
|
- A **Distance (km)** column (also in Worked-before) is computed from the QSO's
|
||||||
|
own locator pair.
|
||||||
|
- There are **no per-column header filters** — they only ever searched the rows
|
||||||
|
on screen, which made a QSO further back in the log look missing. Use the
|
||||||
|
advanced filter, which queries the whole logbook.
|
||||||
|
|
||||||
### Award columns
|
### Award columns
|
||||||
|
|
||||||
Each defined award can show a column with the reference the QSO counts for. These
|
Each defined award can show a column with the reference the QSO counts for.
|
||||||
columns are **hidden by default** and opt-in from the Columns picker — turn on
|
These columns are **hidden by default** and opt-in from the Columns picker.
|
||||||
only the awards you're chasing. Your choice persists across restarts.
|
|
||||||
|
|
||||||
## Advanced filter builder
|
## Advanced filter builder
|
||||||
|
|
||||||
@@ -19,15 +29,19 @@ Click the filter button to open the **QSO filter builder** (Log4OM-style):
|
|||||||
- Add one or more **conditions**: *field · operator · value*.
|
- Add one or more **conditions**: *field · operator · value*.
|
||||||
- Operators: equals, not-equal, contains, starts/ends with, greater/less
|
- Operators: equals, not-equal, contains, starts/ends with, greater/less
|
||||||
(or equal), is empty, is not empty.
|
(or equal), is empty, is not empty.
|
||||||
|
- Fields include every confirmation **status and date** (sent / received,
|
||||||
|
before / after) for paper QSL, LoTW, eQSL, Club Log, HRDLog and QRZ.com.
|
||||||
|
Field names stay in English — they are ADIF names, a standard vocabulary.
|
||||||
- Join conditions with **ALL (AND)** or **ANY (OR)**.
|
- Join conditions with **ALL (AND)** or **ANY (OR)**.
|
||||||
- Save named **presets** (they travel with the `data/` folder).
|
- Save named **presets** (they travel with the `data/` folder).
|
||||||
- The field dropdown is sorted alphabetically to find things fast.
|
|
||||||
|
|
||||||
The filtered view can be exported: **selected rows** or the **whole filtered
|
A filter shows **every** match (the on-screen row limit only applies to the
|
||||||
view** (no row limit) → ADIF or Cabrillo.
|
unfiltered log; safety cap 10 000). The filtered view can be exported:
|
||||||
|
**selected rows** or the **whole filtered view** → ADIF or Cabrillo.
|
||||||
|
|
||||||
## Worked-before matrix
|
## Worked-before matrix
|
||||||
|
|
||||||
As you type a callsign, the worked-before grid shows prior contacts with that
|
As you type a callsign, the worked-before grid shows prior contacts with that
|
||||||
**entity**, arranged by band/mode slot, so you instantly see what's new (new
|
**entity**, arranged by band/mode slot, so you instantly see what's new (new
|
||||||
DXCC / new band / new slot) versus already worked.
|
DXCC / new band / new slot) versus already worked. Its right-click menu offers
|
||||||
|
the same bulk edit and export as the main grid.
|
||||||
|
|||||||
+32
-22
@@ -4,48 +4,58 @@
|
|||||||
|
|
||||||
A `data/` folder is created **next to `OpsLog.exe`**:
|
A `data/` folder is created **next to `OpsLog.exe`**:
|
||||||
|
|
||||||
- **Config** (SQLite): settings, profiles, rigs/antennas, cluster nodes, lookup
|
- **Settings database** (SQLite): settings, profiles, rigs/antennas, cluster
|
||||||
cache, award lists, QSL templates. Always local, so the UI is instant.
|
nodes, lookup cache, award lists, QSL templates. Always local.
|
||||||
- **Local logbook** (SQLite): QSOs, when the profile logs locally.
|
- **Logbook** (`logbook.db`, SQLite): QSOs, when the profile logs locally.
|
||||||
- `data/qsl/templates/…` — QSL card templates and their photos.
|
- `data/qsl/templates/…` — QSL card templates and their photos.
|
||||||
- `data/qsl/outbox/…` — sent QSL card JPEGs.
|
- `data/qsl/outbox/…` — sent QSL card JPEGs.
|
||||||
- Recordings folder (configurable) — per-QSO WAV/MP3.
|
- Recordings folder (configurable) — per-QSO WAV/MP3.
|
||||||
- Backups.
|
- Backups, diagnostic logs.
|
||||||
|
|
||||||
Back up your setup by copying `data/` (and keep it with the exe). See
|
Paths are stored **relative to the folder**, so the whole OpsLog directory can
|
||||||
|
move to another drive or PC. Back up your setup by copying `data/`. See
|
||||||
[[Profiles and Databases]] for local-vs-MySQL logbooks.
|
[[Profiles and Databases]] for local-vs-MySQL logbooks.
|
||||||
|
|
||||||
## Settings map
|
## Settings map
|
||||||
|
|
||||||
| Section | What |
|
| Section | What |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| **General** | language, theme, Main-view panes, live-status publish |
|
| **General** | language, theme, date format (US / FR / Standard), Main-view panes, digital-modes grouping, Super Check Partial, ClubLog exceptions & Most Wanted |
|
||||||
| **Station** | your callsign, grid, name, address (my-station ADIF) |
|
| **Station** | your callsign, grid, name, address (my-station ADIF) — hand-corrected CQ/ITU zones stay corrected |
|
||||||
| **CAT** | radio backend + connection ([[CAT Control]]) |
|
| **CAT** | radio backend + connection, CAT sharing (rigctld), protocol trace ([[CAT Control]]) |
|
||||||
| **Lookup** | QRZ.com / HamQTH credentials |
|
| **Lookup** | QRZ.com / HamQTH credentials |
|
||||||
| **FlexRadio** | per-band RX/TX antennas |
|
| **Audio** | DVK / recorder devices, PTT, gains and levels, RX monitor |
|
||||||
| **Audio** | DVK / recorder devices, PTT, gains, RX monitor |
|
| **CW Keyer** | engine (WinKeyer / Serial / Icom / Flex CWX / Yaesu / TCI), macros, ESM |
|
||||||
| **CW Keyer** | WinKeyer / Icom / TCI engine + macros |
|
| **External services** | LoTW (TQSL), QRZ, eQSL, ClubLog, HRDLog, POTA, Cloudlog/Wavelog |
|
||||||
| **External services** | LoTW (TQSL), QRZ, eQSL, ClubLog, HRDLog, POTA |
|
| **Email** | SMTP for eQSL-by-email and *Send log to F4BPO* |
|
||||||
| **Email** | SMTP for eQSL-by-email |
|
| **Amplifier / Tuner Genius / Antenna Genius / Rotator / Antenna** | station hardware ([[Amplifiers and Switches]] · [[Maps and Antennas]]) |
|
||||||
| **PowerGenius / Antenna Genius / Rotator / Ultrabeam** | station hardware |
|
| **Database** | settings DB + this profile's logbook, rename/relocate |
|
||||||
| **Backup** | database + ADIF backup at shutdown |
|
| **Backup** | database + ADIF backup at shutdown |
|
||||||
|
|
||||||
## Appearance & language
|
## Appearance & language
|
||||||
|
|
||||||
Four themes (Warm light, Warm dark, Graphite dark, High contrast) plus **Auto**
|
Four themes (Warm light, Warm dark, Graphite dark, High contrast) plus **Auto**
|
||||||
(follows the OS). Full **English / French** UI with a first-run flag chooser and
|
(follows the OS); fresh installs start dark. Full **English / French** UI with
|
||||||
a switcher in Settings → General.
|
a first-run flag chooser and a switcher in Settings → General. **Ctrl + wheel**
|
||||||
|
zooms the whole UI (remembered; Ctrl+0 resets). The window title bar is drawn
|
||||||
|
by OpsLog — drag the top bar to move, double-click to maximise — and the window
|
||||||
|
reopens on the screen it was closed on.
|
||||||
|
|
||||||
## Integrations (outbound UDP)
|
## UDP integrations
|
||||||
|
|
||||||
Push the current frequency to **PstRotator**, radio info in **N1MM `RadioInfo`**
|
- **Inbound**: WSJT-X / MSHV / JTDX QSO logging and status (unicast or
|
||||||
format, or an **ADIF record on each logged QSO** — so external tools stay in sync.
|
multicast), and DXHunter remote-call packets (fills the callsign, tunes the
|
||||||
|
rig when the packet carries `<FREQ>`/`<MODE>`).
|
||||||
|
- **Outbound**: the current frequency to **PstRotator**, radio info in **N1MM
|
||||||
|
`RadioInfo`** format, or an **ADIF record on each logged QSO**.
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
- **Autostart** external programs (WSJT-X, JTAlert, rotator control…) at launch,
|
- **Autostart** external programs (WSJT-X, JTAlert, rotator control…) at
|
||||||
skipping any already running.
|
launch, skipping any already running.
|
||||||
- **Update check** at startup (toggleable).
|
- **Update check** at startup, when opening Help → About, and every 5 minutes.
|
||||||
|
A "What's new" summary appears on the first launch after each update.
|
||||||
|
- **Help → Send log to F4BPO** (shown when SMTP is configured) e-mails the
|
||||||
|
diagnostic logs straight to the developer.
|
||||||
- **Anonymous usage telemetry** — a once-a-day heartbeat (random install ID +
|
- **Anonymous usage telemetry** — a once-a-day heartbeat (random install ID +
|
||||||
version + OS; no callsign or QSO data). Opt out in Preferences.
|
version + OS; no callsign or QSO data). Opt out in Preferences.
|
||||||
|
|||||||
+50
-30
@@ -2,57 +2,77 @@
|
|||||||
|
|
||||||
## Where's the log file?
|
## Where's the log file?
|
||||||
|
|
||||||
OpsLog writes a diagnostic log. The path is shown in Settings (and can be opened
|
OpsLog writes a diagnostic log. The path is shown in Settings (and can be
|
||||||
from there). When reporting a problem, include the relevant lines — they're
|
opened from there). When reporting a problem, include the relevant lines —
|
||||||
prefixed by subsystem (`Flex:`, `icom net:`, `icom scope`, `antgenius:`,
|
they're prefixed by subsystem (`cat:`, `Flex:`, `icom net:`, `antgenius:`,
|
||||||
`cluster:`, `qslmgr:log`, `audio:`…).
|
`cluster:`, `qslmgr:log`, `audio:`…). With an SMTP server configured, **Help →
|
||||||
|
Send log to F4BPO** e-mails the logs straight to the developer.
|
||||||
|
|
||||||
|
A crash no longer leaves a blank window: the error is shown on screen,
|
||||||
|
selectable, with a copy button.
|
||||||
|
|
||||||
## CAT
|
## CAT
|
||||||
|
|
||||||
- **Radio not connecting** — check the backend fields (COM port + baud for USB;
|
- **Radio not connecting** — check the backend fields (COM port + baud for USB;
|
||||||
IP/credentials for network). OpsLog connects non-blocking, so it won't freeze —
|
IP/credentials for network). The CAT log line carries the error explaining a
|
||||||
watch the CAT status.
|
disconnection.
|
||||||
- **Icom: no CAT at all over USB** — the CI-V address must match the radio; pick
|
- **"Serial port busy" in the log** — another program holds the COM port
|
||||||
your **model** from the dropdown (it sets the address) and set *CI-V USB Echo
|
(WSJT-X, MSHV, another logger configured for direct CAT). Only one program
|
||||||
Back* **OFF** on the rig.
|
can own the port: let OpsLog hold it and point the others at **Hamlib NET
|
||||||
- **Icom scope blank on an IC-7300** — this was a bug where the scope decoder was
|
rigctl 127.0.0.1:4532** (Settings → CAT → Share CAT). Two masters on one
|
||||||
chosen from the configured CI-V address; it now uses the model detected from the
|
CI-V bus also cause garbled frames and erratic behaviour.
|
||||||
CI-V ID, so a single-scope rig run at address `0x98` decodes correctly. Update
|
- **The radio transmits as soon as OpsLog connects** — your interface reads DTR
|
||||||
to the latest build.
|
or RTS as PTT. Xiegu drops the lines automatically; on Yaesu and Kenwood
|
||||||
- **Icom attenuator button does nothing** — the dB steps are model-specific; make
|
enable the *lower DTR/RTS on connect* option. (It is off by default because
|
||||||
sure the right model is selected (IC-7300 = 20 dB, IC-7610 = 6/12/18 dB).
|
other interfaces stop transmitting with the lines low.)
|
||||||
|
- **WSJT-X won't key a Xiegu G90 through OmniRig** — a G90 ignores the CI-V PTT
|
||||||
|
command. Use the native Xiegu backend (which keys RTS/DTR) + rigctl sharing,
|
||||||
|
or set the PTT line inside OmniRig. See [[Kenwood and Xiegu]].
|
||||||
|
- **OmniRig frequency frozen / wrong VFO** — set **VFO to read** (A or B) in
|
||||||
|
the OmniRig backend settings; some rig files declare the wrong VFO.
|
||||||
|
- **"OmniRig not found" with OmniRig running** — one of the two runs as
|
||||||
|
administrator and the other doesn't. Start both the same way.
|
||||||
|
- **Icom: no CAT at all over USB** — the CI-V address must match the radio;
|
||||||
|
pick your **model** from the dropdown (it sets the address) and set *CI-V USB
|
||||||
|
Echo Back* **OFF** on the rig.
|
||||||
|
- **A radio that answers oddly** — enable the **protocol trace** (Settings →
|
||||||
|
CAT): every frame to and from the rig lands in the log as hex, which turns a
|
||||||
|
guess into a diagnosis.
|
||||||
|
|
||||||
## Remote Icom
|
## Remote Icom
|
||||||
|
|
||||||
- **Login rejected** → wrong Network User1 name/password.
|
- **Login rejected** → wrong Network User1 name/password.
|
||||||
- **Can't bind local port** → the Icom **Remote Utility is still running** —
|
- **Can't bind local port** → the Icom **Remote Utility is still running** —
|
||||||
close it.
|
close it.
|
||||||
- **Link drops with the scope on** → receive-side retransmit should hold it; if
|
- **Frequency frozen after another program took the CI-V session** — OpsLog
|
||||||
not, check network quality and the `icom net:` log lines. See
|
reconnects on its own; if it doesn't, check the `icom net:` log lines. See
|
||||||
[[Remote Icom over the Internet]].
|
[[Remote Icom over the Internet]].
|
||||||
|
|
||||||
## FlexRadio
|
## FlexRadio
|
||||||
|
|
||||||
- **A control reads 0 / stale at startup** — some SmartSDR status fields use short
|
- **A control reads 0 / stale at startup** — some SmartSDR status fields use
|
||||||
names (e.g. the TX filter is reported as `lo`/`hi`, not `filter_low/high`). If a
|
short names. If a value never populates, capture the `Flex: … status` log
|
||||||
value never populates, capture the `Flex: … status` log line and report it.
|
line and report it.
|
||||||
- **Antenna wrong band on a spot click** — fixed: `SetFrequency`/`SetMode` update
|
- **SmartSDR CAT keeps disconnecting while OpsLog is open** — fixed: OpsLog now
|
||||||
the slice cache immediately so the panel and the Ultrabeam follow don't chase a
|
binds only to the actual SmartSDR/Maestro GUI client. Update.
|
||||||
stale frequency.
|
- **"Interlock is preventing transmission" with a SteppIR** — see the tunable
|
||||||
|
range setting in [[Maps and Antennas]].
|
||||||
|
|
||||||
## Multi-op live status PHP
|
## Multi-op live status PHP
|
||||||
|
|
||||||
`Connection refused` from the PHP page is almost always MySQL `bind-address =
|
`Connection refused` from the PHP page is almost always MySQL `bind-address =
|
||||||
127.0.0.1` or a firewall/grant issue — see
|
127.0.0.1` or a firewall/grant issue — see [[Multi-Operator Live Status]].
|
||||||
[[Multi-Operator Live Status]].
|
|
||||||
|
|
||||||
## DVK / PTT
|
## DVK / PTT
|
||||||
|
|
||||||
- **Test PTT does nothing on CAT/OmniRig** — the key is held ~1.5 s so the async
|
- **Test PTT does nothing on CAT/OmniRig** — the key is held ~1.5 s so the
|
||||||
OmniRig write is actually sent; if it still doesn't key, the rig's OmniRig
|
async OmniRig write is actually sent; if it still doesn't key, the rig's
|
||||||
profile may not expose CAT TX keying — use RTS/DTR or VOX.
|
OmniRig profile may not expose CAT TX keying — use RTS/DTR or VOX.
|
||||||
|
- **A voice message transmits almost nothing** — raise the message level
|
||||||
|
(Settings → Audio) and check the rig's rear-input setting (FTDX10: SSB MOD
|
||||||
|
SOURCE = REAR).
|
||||||
|
|
||||||
## Still stuck?
|
## Still stuck?
|
||||||
|
|
||||||
Open an issue with: what you did, what happened, your radio/model, and the
|
Open an issue (or *Send log to F4BPO*) with: what you did, what happened, your
|
||||||
relevant log lines.
|
radio/model, and the relevant log lines.
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Yaesu (native CAT)
|
||||||
|
|
||||||
|
A native backend talks to an **FTDX10 / FTDX101** (and close relatives) directly
|
||||||
|
over the rig's USB/serial port — no OmniRig. Pick **Yaesu (USB)** in
|
||||||
|
Settings → CAT: COM port + baud (FTDX10/101 default 38400).
|
||||||
|
|
||||||
|
Frequency, mode, VFO A/B (MAIN/SUB on the 101), split and PTT are covered, and
|
||||||
|
OpsLog **follows the VFO you are actually on** — the displayed frequency, the
|
||||||
|
logged frequency and the VFO a cluster spot tunes. Split is worked out from
|
||||||
|
which VFO transmits, so simplex on the SUB receiver is not shown as split.
|
||||||
|
|
||||||
|
## The Yaesu console
|
||||||
|
|
||||||
|
With the native backend connected, a console tab (and a Main-view pane) gives:
|
||||||
|
|
||||||
|
- **S / PO / SWR meters** — power in watts on a curve measured against the rig's
|
||||||
|
own display, SWR as the ratio itself, with peak-hold so they stay readable
|
||||||
|
between CW words or SSB syllables.
|
||||||
|
- **Band and mode buttons** — CW, RTTY, DIGI and PSK show their sideband; click
|
||||||
|
the active button again to switch it.
|
||||||
|
- AF / RF / squelch, AGC, IPO–AMP1–AMP2, 6/12/18 dB attenuator, NB, DNR, narrow
|
||||||
|
filter, power, mic gain, VOX, split and **ATU tune**.
|
||||||
|
- **SPLIT** places the transmit VFO up 1 kHz on CW/data, up 5 kHz on phone.
|
||||||
|
- In CW the mic gain and VOX give way to a **CW card**: keyer speed, break-in
|
||||||
|
and ZIN (zero-in).
|
||||||
|
|
||||||
|
Sliders only respond to the mouse wheel after you click them, so scrolling the
|
||||||
|
panel can't change the transmit power by accident.
|
||||||
|
|
||||||
|
## CW with the rig's own keyer
|
||||||
|
|
||||||
|
A CW engine sends your macros with the radio's internal keyer (CAT `KY`) — no
|
||||||
|
WinKeyer, no second cable. Pick **Yaesu (rig keyer)** in Settings → CW keyer.
|
||||||
|
|
||||||
|
- Works on the **FTDX101 / FT-991A / FT-710** family.
|
||||||
|
- An **FTDX10 does not accept it** (`KY` is refused, `DAKY` doesn't help) —
|
||||||
|
OpsLog says so and points to the **serial DTR keyer** on the rig's second COM
|
||||||
|
port (the *Serial port* CW engine), which works. See [[Audio and Keyers]].
|
||||||
|
- CW speed is one setting everywhere: the console slider, the CW panel and the
|
||||||
|
rig's front panel stay in agreement.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- A Yaesu that is switched off is reported as such (opening the port is not
|
||||||
|
"connected").
|
||||||
|
- If your interface goes into transmit the moment OpsLog connects, enable the
|
||||||
|
**lower DTR/RTS on connect** option — see [[CAT Control]].
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
- [[FlexRadio]]
|
- [[FlexRadio]]
|
||||||
- [[Icom]]
|
- [[Icom]]
|
||||||
- [[Remote Icom over the Internet]]
|
- [[Remote Icom over the Internet]]
|
||||||
|
- [[Yaesu]]
|
||||||
|
- [[Kenwood and Xiegu]]
|
||||||
|
|
||||||
**Operating**
|
**Operating**
|
||||||
- [[DX Cluster and Spots]]
|
- [[DX Cluster and Spots]]
|
||||||
|
|||||||
Reference in New Issue
Block a user