feat(antgenius): the selected antenna can fill MY_ANTENNA
A station with a switch knows something the log does not: which antenna is actually connected. The working conditions hold what was PLANNED for the band, and stay right until the operator throws the switch — after which every QSO keeps claiming the other antenna. Optional, and off by default: a station that names its antennas differently in the two places would otherwise find its log quietly rewritten. The name written is the one configured on the device, since that is the name the operator gave it and the one they will look for. WHICH PORT is the real problem, and the reason this is not a one-liner. The switch has two, the radio has two jacks, and the QSO went out through one of them; naming the wrong port's antenna is worse than naming the band default, because it looks authoritative. The radio's own TX antenna selection decides, and the jack-to-port wiring is a setting — it is the station's cabling and neither device can report it. When the port cannot be told — a transverter jack, a rig that reports nothing, both switch ports live — nothing is written and the band default stands. Silence beats a confident guess in a field nobody re-checks.
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// MY_ANTENNA from the Antenna Genius.
|
||||||
|
//
|
||||||
|
// A station with a switch has a truth the log does not: which antenna is
|
||||||
|
// actually connected right now. The working conditions hold what was PLANNED
|
||||||
|
// for the band — the default antenna ticked for 20 m — and that is right until
|
||||||
|
// the operator throws the switch, at which point the log quietly keeps claiming
|
||||||
|
// the other antenna for the rest of the session.
|
||||||
|
//
|
||||||
|
// So, as an option: when the Antenna Genius knows which antenna is selected,
|
||||||
|
// its name wins. The name is the one configured on the device, because that is
|
||||||
|
// the name the operator gave it and the one they will look for in the log.
|
||||||
|
//
|
||||||
|
// WHICH PORT is the whole difficulty. An Antenna Genius has two, A and B, and a
|
||||||
|
// FlexRadio has two transmit antenna jacks, ANT1 and ANT2. The QSO was made on
|
||||||
|
// exactly one of them, and stamping the wrong port's antenna would be worse
|
||||||
|
// than stamping the band default — a wrong answer that looks authoritative. The
|
||||||
|
// radio's own TX antenna selection is what decides.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hamlog/internal/antgenius"
|
||||||
|
"hamlog/internal/applog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyAntGeniusMyAnt = "antgenius.my_antenna" // use the selected antenna as MY_ANTENNA
|
||||||
|
keyAntGeniusPortForA1 = "antgenius.port_for_ant1" // which AG port ANT1 is wired to (1=A, 2=B)
|
||||||
|
)
|
||||||
|
|
||||||
|
// antGeniusPortFor maps the radio's transmit antenna jack to an Antenna Genius
|
||||||
|
// port.
|
||||||
|
//
|
||||||
|
// The wiring is the station's, not something that can be read from either
|
||||||
|
// device: ANT1 usually goes to port A and ANT2 to port B, which is what the
|
||||||
|
// setting defaults to, but a station wired the other way round would otherwise
|
||||||
|
// log every QSO with the other antenna's name.
|
||||||
|
func (a *App) antGeniusPortFor(txAnt string) int {
|
||||||
|
ant1Port := 1
|
||||||
|
if a.settingOr(keyAntGeniusPortForA1, "1") == "2" {
|
||||||
|
ant1Port = 2
|
||||||
|
}
|
||||||
|
other := 3 - ant1Port
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(txAnt)) {
|
||||||
|
case "ANT1", "ANT 1", "1", "A":
|
||||||
|
return ant1Port
|
||||||
|
case "ANT2", "ANT 2", "2", "B":
|
||||||
|
return other
|
||||||
|
}
|
||||||
|
return 0 // XVTR, a rig with one jack, or nothing reported: no port to name
|
||||||
|
}
|
||||||
|
|
||||||
|
// antGeniusAntennaName returns the name of the antenna currently selected on
|
||||||
|
// the port the radio is transmitting through, or "" when it cannot be told.
|
||||||
|
//
|
||||||
|
// Deliberately silent rather than approximate. Every "" here means the log
|
||||||
|
// keeps the band default it would have had anyway, which is a defensible
|
||||||
|
// answer; a guessed port is not.
|
||||||
|
func (a *App) antGeniusAntennaName() string {
|
||||||
|
if a.antgenius == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
st := a.antgenius.GetStatus()
|
||||||
|
if !st.Connected {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// The jack in use comes from the FlexRadio's own state — TXAnt lives on the
|
||||||
|
// Flex panel state, not on the backend-agnostic RigState, because only a
|
||||||
|
// Flex has named antenna jacks to report.
|
||||||
|
txAnt := ""
|
||||||
|
if a.cat != nil {
|
||||||
|
if fs, ok := a.cat.FlexState(); ok {
|
||||||
|
txAnt = fs.TXAnt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port := a.antGeniusPortFor(txAnt)
|
||||||
|
if port == 0 {
|
||||||
|
// One switch port in use and no ambiguity about which: a station whose
|
||||||
|
// radio has a single jack still deserves the name. Two ports carrying
|
||||||
|
// different antennas with nothing to choose between them does not.
|
||||||
|
if st.PortA > 0 && st.PortB == 0 {
|
||||||
|
port = 1
|
||||||
|
} else if st.PortB > 0 && st.PortA == 0 {
|
||||||
|
port = 2
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx := st.PortA
|
||||||
|
if port == 2 {
|
||||||
|
idx = st.PortB
|
||||||
|
}
|
||||||
|
if idx <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return antGeniusNameOf(st, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// antGeniusNameOf looks an antenna index up in the device's own list.
|
||||||
|
func antGeniusNameOf(st antgenius.Status, idx int) string {
|
||||||
|
for _, ant := range st.Antennas {
|
||||||
|
if ant.Index == idx {
|
||||||
|
return strings.TrimSpace(ant.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyAntGeniusAntenna overrides MY_ANTENNA with the switch's selection, when
|
||||||
|
// the option is on.
|
||||||
|
//
|
||||||
|
// Called at log time rather than while the entry form is open: an operator who
|
||||||
|
// changes antenna mid-QSO is telling us what the contact was actually made on,
|
||||||
|
// and the value that matters is the one at the moment it is logged.
|
||||||
|
func (a *App) applyAntGeniusAntenna(myAntenna *string) {
|
||||||
|
if myAntenna == nil || a.settingOr(keyAntGeniusMyAnt, "") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := a.antGeniusAntennaName()
|
||||||
|
if name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if *myAntenna != name {
|
||||||
|
applog.Printf("antgenius: MY_ANTENNA %q → %q (the antenna selected on the switch)", *myAntenna, name)
|
||||||
|
}
|
||||||
|
*myAntenna = name
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hamlog/internal/antgenius"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The switch has two ports and the radio two antenna jacks; a QSO was made
|
||||||
|
// through exactly one of them. Naming the wrong port's antenna would be worse
|
||||||
|
// than naming the band default — it is a wrong answer that looks authoritative.
|
||||||
|
func TestAntennaNameComesFromTheDeviceList(t *testing.T) {
|
||||||
|
st := antgenius.Status{
|
||||||
|
Connected: true,
|
||||||
|
PortA: 2,
|
||||||
|
PortB: 5,
|
||||||
|
Antennas: []antgenius.Antenna{
|
||||||
|
{Index: 2, Name: "OB11-5 20/15/10"},
|
||||||
|
{Index: 5, Name: "Vertical 80m"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if got := antGeniusNameOf(st, 2); got != "OB11-5 20/15/10" {
|
||||||
|
t.Fatalf("port A antenna = %q", got)
|
||||||
|
}
|
||||||
|
if got := antGeniusNameOf(st, 5); got != "Vertical 80m" {
|
||||||
|
t.Fatalf("port B antenna = %q", got)
|
||||||
|
}
|
||||||
|
// An index the device never described has no name to give, and inventing
|
||||||
|
// one ("Antenna 7") would put a label in the log that exists nowhere else.
|
||||||
|
if got := antGeniusNameOf(st, 7); got != "" {
|
||||||
|
t.Fatalf("unknown index produced %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The jack-to-port wiring is the station's own. Defaulting ANT1 to port A is a
|
||||||
|
// convention, not a fact, so it is a setting — and the mapping has to hold both
|
||||||
|
// ways round.
|
||||||
|
func TestJackToPortMapping(t *testing.T) {
|
||||||
|
app := &App{}
|
||||||
|
// No settings store: settingOr falls back, which is ANT1 → port A.
|
||||||
|
if got := app.antGeniusPortFor("ANT1"); got != 1 {
|
||||||
|
t.Fatalf("ANT1 mapped to port %d, want A(1)", got)
|
||||||
|
}
|
||||||
|
if got := app.antGeniusPortFor("ANT2"); got != 2 {
|
||||||
|
t.Fatalf("ANT2 mapped to port %d, want B(2)", got)
|
||||||
|
}
|
||||||
|
// A jack that is neither — a transverter port, or a rig that reports
|
||||||
|
// nothing — has no port to name, and saying so beats guessing.
|
||||||
|
if got := app.antGeniusPortFor("XVTR"); got != 0 {
|
||||||
|
t.Fatalf("XVTR mapped to port %d, want none", got)
|
||||||
|
}
|
||||||
|
if got := app.antGeniusPortFor(""); got != 0 {
|
||||||
|
t.Fatalf("an unreported jack mapped to port %d, want none", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -781,8 +781,8 @@ type App struct {
|
|||||||
// so closing the QSL Manager — or starting another download — stops the previous
|
// so closing the QSL Manager — or starting another download — stops the previous
|
||||||
// one instead of leaving it running against the app-lifetime context (which made
|
// one instead of leaving it running against the app-lifetime context (which made
|
||||||
// a still-running QRZ sync bleed its log into a freshly started LoTW download).
|
// a still-running QRZ sync bleed its log into a freshly started LoTW download).
|
||||||
confDLMu sync.Mutex
|
confDLMu sync.Mutex
|
||||||
confDLCancel context.CancelFunc
|
confDLCancel context.CancelFunc
|
||||||
|
|
||||||
// hamlogUnmatched holds the confirmations the last HAMLOG.online import
|
// hamlogUnmatched holds the confirmations the last HAMLOG.online import
|
||||||
// could not place onto a QSO, kept so they can be exported and worked
|
// could not place onto a QSO, kept so they can be exported and worked
|
||||||
@@ -790,31 +790,31 @@ type App struct {
|
|||||||
// accumulated: it describes one run, not a history.
|
// accumulated: it describes one run, not a history.
|
||||||
hamlogUnmatchedMu sync.Mutex
|
hamlogUnmatchedMu sync.Mutex
|
||||||
hamlogUnmatched []qso.QSO
|
hamlogUnmatched []qso.QSO
|
||||||
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
||||||
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
||||||
syncMu sync.Mutex // serialises folder synchronisation: config, the seq counter, and the append to our own file
|
syncMu sync.Mutex // serialises folder synchronisation: config, the seq counter, and the append to our own file
|
||||||
syncSent int64 // changes written to the folder this session
|
syncSent int64 // changes written to the folder this session
|
||||||
syncReceived int64 // changes taken from the other machines this session
|
syncReceived int64 // changes taken from the other machines this session
|
||||||
syncLast time.Time // last completed pass, for the status panel
|
syncLast time.Time // last completed pass, for the status panel
|
||||||
syncErr string // last folder error, shown in settings — a share that dropped is otherwise invisible
|
syncErr string // last folder error, shown in settings — a share that dropped is otherwise invisible
|
||||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||||
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
|
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
|
||||||
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
|
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
|
||||||
relayDrvMu sync.Mutex // guards the cached relay drivers below
|
relayDrvMu sync.Mutex // guards the cached relay drivers below
|
||||||
relayDrv map[string]cachedRelay // deviceID → live driver (reused across polls; stateful boards can't be reopened per call)
|
relayDrv map[string]cachedRelay // deviceID → live driver (reused across polls; stateful boards can't be reopened per call)
|
||||||
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
|
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
|
||||||
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
|
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
|
||||||
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
|
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
|
||||||
startupErr string // captured for surfacing to the frontend
|
startupErr string // captured for surfacing to the frontend
|
||||||
settingsScoped atomic.Bool // true once a.settings is scoped to the active profile — GetUIPref/SetUIPref (per-profile) must wait for it, else an early call reads the wrong scope and e.g. resets the theme
|
settingsScoped atomic.Bool // true once a.settings is scoped to the active profile — GetUIPref/SetUIPref (per-profile) must wait for it, else an early call reads the wrong scope and e.g. resets the theme
|
||||||
dbPath string // settings/config database file (settings + profiles); may be a user-chosen location
|
dbPath string // settings/config database file (settings + profiles); may be a user-chosen location
|
||||||
logbookPath string // default SQLite logbook file (QSOs), next to the settings db — used when a profile doesn't point elsewhere
|
logbookPath string // default SQLite logbook file (QSOs), next to the settings db — used when a profile doesn't point elsewhere
|
||||||
logDbPath string // resolved SQLite file actually backing logDb ("" on MySQL, or when the settings db serves as the logbook) — the file the backup snapshots
|
logDbPath string // resolved SQLite file actually backing logDb ("" on MySQL, or when the settings db serves as the logbook) — the file the backup snapshots
|
||||||
logDb *sql.DB // QSO logbook connection — MySQL, a per-profile SQLite file, or the default logbook.db (never the settings db, except on fallback)
|
logDb *sql.DB // QSO logbook connection — MySQL, a per-profile SQLite file, or the default logbook.db (never the settings db, except on fallback)
|
||||||
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
|
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
|
||||||
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
|
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
|
||||||
offlineQ *offlineq.Queue // ADIF outbox: QSOs logged while the DB was unreachable
|
offlineQ *offlineq.Queue // ADIF outbox: QSOs logged while the DB was unreachable
|
||||||
offlineMode bool // last write failed because the DB was unreachable
|
offlineMode bool // last write failed because the DB was unreachable
|
||||||
|
|
||||||
catFlexSpots bool // push cluster spots to the FlexRadio panadapter
|
catFlexSpots bool // push cluster spots to the FlexRadio panadapter
|
||||||
catFlexDVKDax bool // raise the Flex transmit DAX button around a voice message
|
catFlexDVKDax bool // raise the Flex transmit DAX button around a voice message
|
||||||
@@ -3330,6 +3330,9 @@ func (a *App) applyStationDefaults(q *qso.QSO, includeIdentity bool) {
|
|||||||
if q.MyAntenna == "" {
|
if q.MyAntenna == "" {
|
||||||
q.MyAntenna = p.MyAntenna
|
q.MyAntenna = p.MyAntenna
|
||||||
}
|
}
|
||||||
|
// The switch has the last word, when asked to: everything above is what was
|
||||||
|
// PLANNED for this band, and the Antenna Genius knows what is connected.
|
||||||
|
a.applyAntGeniusAntenna(&q.MyAntenna)
|
||||||
if q.TXPower == nil && p.TxPower != nil {
|
if q.TXPower == nil && p.TxPower != nil {
|
||||||
v := *p.TxPower
|
v := *p.TxPower
|
||||||
q.TXPower = &v
|
q.TXPower = &v
|
||||||
@@ -17653,6 +17656,12 @@ type AntGeniusSettings struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Password string `json:"password"` // remote-access password; leave blank on LAN (no AUTH)
|
Password string `json:"password"` // remote-access password; leave blank on LAN (no AUTH)
|
||||||
|
// UseForMyAntenna stamps the SELECTED antenna's name into MY_ANTENNA on
|
||||||
|
// every QSO logged, ahead of the band default from Operating conditions.
|
||||||
|
UseForMyAntenna bool `json:"use_for_my_antenna"`
|
||||||
|
// Ant1Port says which switch port the radio's ANT1 jack is wired to
|
||||||
|
// (1 = A, 2 = B). Station wiring; neither device can report it.
|
||||||
|
Ant1Port int `json:"ant1_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAntGeniusSettings returns the persisted Antenna Genius config.
|
// GetAntGeniusSettings returns the persisted Antenna Genius config.
|
||||||
@@ -17661,13 +17670,19 @@ func (a *App) GetAntGeniusSettings() (AntGeniusSettings, error) {
|
|||||||
if a.settings == nil {
|
if a.settings == nil {
|
||||||
return out, fmt.Errorf("db not initialized")
|
return out, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
m, err := a.settings.GetMany(a.ctx, keyAntGeniusEnabled, keyAntGeniusHost, keyAntGeniusPassword)
|
m, err := a.settings.GetMany(a.ctx, keyAntGeniusEnabled, keyAntGeniusHost, keyAntGeniusPassword,
|
||||||
|
keyAntGeniusMyAnt, keyAntGeniusPortForA1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
out.Enabled = m[keyAntGeniusEnabled] == "1"
|
out.Enabled = m[keyAntGeniusEnabled] == "1"
|
||||||
out.Host = m[keyAntGeniusHost]
|
out.Host = m[keyAntGeniusHost]
|
||||||
out.Password = m[keyAntGeniusPassword]
|
out.Password = m[keyAntGeniusPassword]
|
||||||
|
out.UseForMyAntenna = m[keyAntGeniusMyAnt] == "1"
|
||||||
|
out.Ant1Port = 1
|
||||||
|
if m[keyAntGeniusPortForA1] == "2" {
|
||||||
|
out.Ant1Port = 2
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17677,9 +17692,11 @@ func (a *App) SaveAntGeniusSettings(s AntGeniusSettings) error {
|
|||||||
return fmt.Errorf("db not initialized")
|
return fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
for k, v := range map[string]string{
|
for k, v := range map[string]string{
|
||||||
keyAntGeniusEnabled: boolStr(s.Enabled),
|
keyAntGeniusEnabled: boolStr(s.Enabled),
|
||||||
keyAntGeniusHost: strings.TrimSpace(s.Host),
|
keyAntGeniusHost: strings.TrimSpace(s.Host),
|
||||||
keyAntGeniusPassword: s.Password,
|
keyAntGeniusPassword: s.Password,
|
||||||
|
keyAntGeniusMyAnt: boolStr(s.UseForMyAntenna),
|
||||||
|
keyAntGeniusPortForA1: map[bool]string{true: "2", false: "1"}[s.Ant1Port == 2],
|
||||||
} {
|
} {
|
||||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -20343,8 +20360,6 @@ func clampSpotMax(n int) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// GetSpotTTLMinutes returns how long a spot stays in the list, in minutes.
|
// GetSpotTTLMinutes returns how long a spot stays in the list, in minutes.
|
||||||
// 0 means spots are kept until the count cap pushes them out, which is what
|
// 0 means spots are kept until the count cap pushes them out, which is what
|
||||||
// OpsLog always did.
|
// OpsLog always did.
|
||||||
|
|||||||
+4
-2
@@ -7,14 +7,16 @@
|
|||||||
"Cluster: the list holds still while it is being read. Scrolled away from the top it stops redrawing and shows how many spots are waiting; scrolling back to the top, or clicking the notice, releases it. On a busy evening a spot lands every second or two and the callsign under the pointer had moved by the time the click arrived.",
|
"Cluster: the list holds still while it is being read. Scrolled away from the top it stops redrawing and shows how many spots are waiting; scrolling back to the top, or clicking the notice, releases it. On a busy evening a spot lands every second or two and the callsign under the pointer had moved by the time the click arrived.",
|
||||||
"Cluster command buttons take 500 characters instead of 120. A DXSpider filter listing wanted prefixes runs past a hundred easily, and the field simply stopped accepting keystrokes — saving the command truncated, with nothing to say so.",
|
"Cluster command buttons take 500 characters instead of 120. A DXSpider filter listing wanted prefixes runs past a hundred easily, and the field simply stopped accepting keystrokes — saving the command truncated, with nothing to say so.",
|
||||||
"Multi-monitor: the saved window position is now checked against the monitors themselves, not the rectangle that spans them. Monitors rarely fill that rectangle, and a window in one of the leftover gaps passed the old test while being invisible. A position that is genuinely lost is moved onto the nearest screen — keeping the window size — instead of being handed back to Windows, and the screen layout is written to the log at every start.",
|
"Multi-monitor: the saved window position is now checked against the monitors themselves, not the rectangle that spans them. Monitors rarely fill that rectangle, and a window in one of the leftover gaps passed the old test while being invisible. A position that is genuinely lost is moved onto the nearest screen — keeping the window size — instead of being handed back to Windows, and the screen layout is written to the log at every start.",
|
||||||
"PowerGenius XL: the amplifier's real state is read at startup. Its status frame carries no 'operate' field — the state is in 'state' — so on the direct GSCP link the flag was never read at all and OpsLog opened claiming STANDBY on an amp that was in line, with the first press of the button then commanding the state it was already in. IDLE means in line, not keyed."
|
"PowerGenius XL: the amplifier's real state is read at startup. Its status frame carries no 'operate' field — the state is in 'state' — so on the direct GSCP link the flag was never read at all and OpsLog opened claiming STANDBY on an amp that was in line, with the first press of the button then commanding the state it was already in. IDLE means in line, not keyed.",
|
||||||
|
"Antenna Genius: an option to write the SELECTED antenna into MY_ANTENNA, under the name it carries on the switch, ahead of the band default from Operating conditions. Which of the two ports counts is decided by the antenna jack the radio is transmitting on (ANT1/ANT2 on a Flex), with the jack-to-port wiring set once in Preferences — neither device can report it. When the port cannot be told, the log keeps the band default rather than naming an antenna at random."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.",
|
"Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.",
|
||||||
"Cluster : la liste se fige pendant qu'on la lit. Dès qu'on quitte le haut, elle cesse de se redessiner et indique combien de spots attendent ; revenir en haut, ou cliquer sur l'avis, la relâche. Un soir chargé, un spot tombe toutes les une ou deux secondes et l'indicatif sous le pointeur avait bougé avant que le clic n'arrive.",
|
"Cluster : la liste se fige pendant qu'on la lit. Dès qu'on quitte le haut, elle cesse de se redessiner et indique combien de spots attendent ; revenir en haut, ou cliquer sur l'avis, la relâche. Un soir chargé, un spot tombe toutes les une ou deux secondes et l'indicatif sous le pointeur avait bougé avant que le clic n'arrive.",
|
||||||
"Les boutons de commande du cluster acceptent 500 caractères au lieu de 120. Un filtre DXSpider qui énumère des préfixes dépasse la centaine sans peine, et le champ cessait simplement d'accepter les frappes — la commande était enregistrée tronquée, sans un mot.",
|
"Les boutons de commande du cluster acceptent 500 caractères au lieu de 120. Un filtre DXSpider qui énumère des préfixes dépasse la centaine sans peine, et le champ cessait simplement d'accepter les frappes — la commande était enregistrée tronquée, sans un mot.",
|
||||||
"Multi-écrans : la position enregistrée est désormais vérifiée contre les écrans eux-mêmes, et non contre le rectangle qui les englobe. Les écrans remplissent rarement ce rectangle, et une fenêtre tombée dans un des trous passait l'ancien test tout en étant invisible. Une position réellement perdue est déplacée sur l'écran le plus proche — en conservant la taille de la fenêtre — au lieu d'être rendue à Windows, et la disposition des écrans est écrite dans le journal à chaque démarrage.",
|
"Multi-écrans : la position enregistrée est désormais vérifiée contre les écrans eux-mêmes, et non contre le rectangle qui les englobe. Les écrans remplissent rarement ce rectangle, et une fenêtre tombée dans un des trous passait l'ancien test tout en étant invisible. Une position réellement perdue est déplacée sur l'écran le plus proche — en conservant la taille de la fenêtre — au lieu d'être rendue à Windows, et la disposition des écrans est écrite dans le journal à chaque démarrage.",
|
||||||
"Power Genius XL : l'état réel de l'amplificateur est lu au démarrage. Sa trame d'état ne contient pas de champ « operate » — l'état est dans « state » — si bien que sur la liaison GSCP directe l'indicateur n'était jamais lu : OpsLog s'ouvrait en annonçant STANDBY sur un ampli en ligne, et le premier appui commandait l'état dans lequel il se trouvait déjà. IDLE veut dire en ligne, pas en émission."
|
"Power Genius XL : l'état réel de l'amplificateur est lu au démarrage. Sa trame d'état ne contient pas de champ « operate » — l'état est dans « state » — si bien que sur la liaison GSCP directe l'indicateur n'était jamais lu : OpsLog s'ouvrait en annonçant STANDBY sur un ampli en ligne, et le premier appui commandait l'état dans lequel il se trouvait déjà. IDLE veut dire en ligne, pas en émission.",
|
||||||
|
"Antenna Genius : une option pour inscrire l'antenne SÉLECTIONNÉE dans MY_ANTENNA, sous le nom qu'elle porte sur le switch, avant l'antenne par défaut des conditions de trafic. C'est la prise d'antenne sur laquelle la radio émet (ANT1/ANT2 sur un Flex) qui décide du port retenu, le câblage prise→port se règlant une fois dans les préférences — aucun des deux appareils ne peut le dire. Quand le port ne peut pas être déterminé, le journal conserve l'antenne par défaut plutôt que d'en nommer une au hasard."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1543,7 +1543,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||||
|
|
||||||
// Antenna Genius (4O3A) switch settings — TCP port is fixed at 9007.
|
// Antenna Genius (4O3A) switch settings — TCP port is fixed at 9007.
|
||||||
const [antgenius, setAntgenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' });
|
const [antgenius, setAntgenius] = useState<{ enabled: boolean; host: string; password: string; use_for_my_antenna?: boolean; ant1_port?: number }>({ enabled: false, host: '', password: '', use_for_my_antenna: false, ant1_port: 1 });
|
||||||
const [tunergenius, setTunergenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' });
|
const [tunergenius, setTunergenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' });
|
||||||
const [psuCfg, setPsuCfg] = useState<{ enabled: boolean; com_port: string; baud: number; address: number }>({ enabled: false, com_port: '', baud: 9600, address: 1 });
|
const [psuCfg, setPsuCfg] = useState<{ enabled: boolean; com_port: string; baud: number; address: number }>({ enabled: false, com_port: '', baud: 9600, address: 1 });
|
||||||
|
|
||||||
@@ -3822,6 +3822,38 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">{t('ag2.passwordHint')}</p>
|
<p className="text-xs text-muted-foreground">{t('ag2.passwordHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* MY_ANTENNA from the switch.
|
||||||
|
The working conditions hold what was PLANNED for the band; the
|
||||||
|
switch knows what is connected. Off by default: a station that
|
||||||
|
names its antennas differently in the two places would otherwise
|
||||||
|
find its log quietly rewritten. */}
|
||||||
|
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||||
|
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||||
|
<Checkbox className="mt-0.5" checked={!!antgenius.use_for_my_antenna}
|
||||||
|
onCheckedChange={(c) => setAntgenius((s: any) => ({ ...s, use_for_my_antenna: !!c }))} />
|
||||||
|
<span>
|
||||||
|
{t('ag2.useForMyAnt')}
|
||||||
|
<span className="block text-xs text-muted-foreground">{t('ag2.useForMyAntHint')}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{!!antgenius.use_for_my_antenna && (
|
||||||
|
<div className="pl-6 space-y-1">
|
||||||
|
<Label>{t('ag2.ant1Port')}</Label>
|
||||||
|
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs">
|
||||||
|
{[1, 2].map((n) => (
|
||||||
|
<button key={n} type="button"
|
||||||
|
onClick={() => setAntgenius((s: any) => ({ ...s, ant1_port: n }))}
|
||||||
|
className={cn('px-3 py-1.5 font-medium',
|
||||||
|
(antgenius.ant1_port ?? 1) === n ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
|
||||||
|
{n === 1 ? t('ag2.portA') : t('ag2.portB')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{t('ag2.ant1PortHint')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ const en: Dict = {
|
|||||||
// Section hints (hardware/software panel headers)
|
// Section hints (hardware/software panel headers)
|
||||||
'autostart.hint': 'Launch external programs (WSJT-X, JTAlert, rotator control…) when OpsLog starts. A program already running is not started again. Saved per profile.',
|
'autostart.hint': 'Launch external programs (WSJT-X, JTAlert, rotator control…) when OpsLog starts. A program already running is not started again. Saved per profile.',
|
||||||
'cat.hint': "Reads the rig's frequency / band / mode and pushes them into the entry strip in real time. Use OmniRig (free, any rig) or — for FlexRadio — the native SmartSDR API (no OmniRig needed, real-time, no second-click mode bug).",
|
'cat.hint': "Reads the rig's frequency / band / mode and pushes them into the entry strip in real time. Use OmniRig (free, any rig) or — for FlexRadio — the native SmartSDR API (no OmniRig needed, real-time, no second-click mode bug).",
|
||||||
'ag2.hint': 'OpsLog talks to the 4O3A Antenna Genius switch over TCP (GSCP protocol). The port is fixed at 9007, so only the device IP is needed. A docked widget then lets you switch antennas per port (A/B).', 'ag2.password': 'Remote password', 'ag2.passwordPh': 'blank on LAN', 'ag2.passwordHint': 'Only needed when reaching the device remotely — it then announces "AG AUTH" and rejects commands until you log in. Leave blank on the local network.',
|
'ag2.hint': 'OpsLog talks to the 4O3A Antenna Genius switch over TCP (GSCP protocol). The port is fixed at 9007, so only the device IP is needed. A docked widget then lets you switch antennas per port (A/B).', 'ag2.password': 'Remote password', 'ag2.passwordPh': 'blank on LAN', 'ag2.useForMyAnt': 'Use the selected antenna as MY_ANTENNA', 'ag2.useForMyAntHint': 'The antenna selected on the switch, under the name it carries there, is written into every QSO as it is logged — ahead of the band default from Operating conditions. Which port counts is decided by the antenna jack the radio is transmitting on.', 'ag2.ant1Port': 'The radio\u2019s ANT1 jack is wired to', 'ag2.portA': 'Port A', 'ag2.portB': 'Port B', 'ag2.ant1PortHint': 'Station wiring — neither the radio nor the switch can report it. ANT2 then goes to the other port.', 'ag2.passwordHint': 'Only needed when reaching the device remotely — it then announces "AG AUTH" and rejects commands until you log in. Leave blank on the local network.',
|
||||||
'tg2.hint': 'OpsLog talks to the 4O3A Tuner Genius XL over TCP (port fixed at 9010), so only the device IP is needed. A docked widget then shows SWR and forward power and offers Tune, Bypass and Operate/Standby. Control it directly (not through the radio) so OpsLog uses just one of the box\'s connection slots.', 'tg2.enable': 'Enable Tuner Genius control', 'tg2.portHint': 'The TCP port is fixed at 9010 on the device.', 'tg2.password': 'Remote code', 'tg2.passwordPh': 'blank on LAN', 'tg2.passwordHint': 'Only needed when reaching the device remotely — it then announces "AUTH" and rejects commands until you log in. Leave blank on the local network.',
|
'tg2.hint': 'OpsLog talks to the 4O3A Tuner Genius XL over TCP (port fixed at 9010), so only the device IP is needed. A docked widget then shows SWR and forward power and offers Tune, Bypass and Operate/Standby. Control it directly (not through the radio) so OpsLog uses just one of the box\'s connection slots.', 'tg2.enable': 'Enable Tuner Genius control', 'tg2.portHint': 'The TCP port is fixed at 9010 on the device.', 'tg2.password': 'Remote code', 'tg2.passwordPh': 'blank on LAN', 'tg2.passwordHint': 'Only needed when reaching the device remotely — it then announces "AUTH" and rejects commands until you log in. Leave blank on the local network.',
|
||||||
'rot.spidHint': 'Native SPID protocol over the controller’s COM port — no PstRotator needed. Pick the dialect above: Rot2Prog answers with azimuth and elevation at 600 baud, Rot1Prog with azimuth only at 1200.', 'rot.spidModel': 'SPID protocol',
|
'rot.spidHint': 'Native SPID protocol over the controller’s COM port — no PstRotator needed. Pick the dialect above: Rot2Prog answers with azimuth and elevation at 600 baud, Rot1Prog with azimuth only at 1200.', 'rot.spidModel': 'SPID protocol',
|
||||||
'rot.widget': 'Widget', 'rot.compact': 'Compact mode', 'rot.compactHint': 'Show only the dial and the short/long-path azimuths. The quick-turn buttons, the azimuth box and Stop are hidden — turn the antenna from the bearing pill in the entry strip instead.',
|
'rot.widget': 'Widget', 'rot.compact': 'Compact mode', 'rot.compactHint': 'Show only the dial and the short/long-path azimuths. The quick-turn buttons, the azimuth box and Stop are hidden — turn the antenna from the bearing pill in the entry strip instead.',
|
||||||
@@ -804,7 +804,7 @@ const fr: Dict = {
|
|||||||
'bk.lastRun': 'Dernière exécution :', 'bk.never': 'jamais', 'bk.backupNow': 'Sauvegarder maintenant', 'bk.backingUp': 'Sauvegarde…', 'bk.writtenTo': 'Sauvegarde écrite dans',
|
'bk.lastRun': 'Dernière exécution :', 'bk.never': 'jamais', 'bk.backupNow': 'Sauvegarder maintenant', 'bk.backingUp': 'Sauvegarde…', 'bk.writtenTo': 'Sauvegarde écrite dans',
|
||||||
'autostart.hint': "Lance des programmes externes (WSJT-X, JTAlert, contrôle rotator…) au démarrage d'OpsLog. Un programme déjà lancé n'est pas relancé. Enregistré par profil.",
|
'autostart.hint': "Lance des programmes externes (WSJT-X, JTAlert, contrôle rotator…) au démarrage d'OpsLog. Un programme déjà lancé n'est pas relancé. Enregistré par profil.",
|
||||||
'cat.hint': "Lit la fréquence / bande / mode du poste et les injecte dans le bandeau de saisie en temps réel. Utilise OmniRig (gratuit, tout poste) ou — pour FlexRadio — l'API native SmartSDR (sans OmniRig, temps réel, sans le bug du mode au 2ᵉ clic).",
|
'cat.hint': "Lit la fréquence / bande / mode du poste et les injecte dans le bandeau de saisie en temps réel. Utilise OmniRig (gratuit, tout poste) ou — pour FlexRadio — l'API native SmartSDR (sans OmniRig, temps réel, sans le bug du mode au 2ᵉ clic).",
|
||||||
'ag2.hint': "OpsLog dialogue avec le switch 4O3A Antenna Genius en TCP (protocole GSCP). Le port est fixé à 9007, seule l'IP de l'appareil est nécessaire. Un widget ancré permet ensuite de commuter les antennes par port (A/B).", 'ag2.password': 'Mot de passe distant', 'ag2.passwordPh': 'vide en LAN', 'ag2.passwordHint': "Nécessaire seulement à distance — l'appareil annonce alors « AG AUTH » et refuse les commandes tant qu'on n'est pas identifié. Laisse vide sur le réseau local.",
|
'ag2.hint': "OpsLog dialogue avec le switch 4O3A Antenna Genius en TCP (protocole GSCP). Le port est fixé à 9007, seule l'IP de l'appareil est nécessaire. Un widget ancré permet ensuite de commuter les antennes par port (A/B).", 'ag2.password': 'Mot de passe distant', 'ag2.passwordPh': 'vide en LAN', 'ag2.useForMyAnt': "Utiliser l'antenne sélectionnée comme MY_ANTENNA", 'ag2.useForMyAntHint': "L'antenne sélectionnée sur le switch, sous le nom qu'elle y porte, est inscrite dans chaque QSO au moment où il est enregistré — avant l'antenne par défaut des conditions de trafic. C'est la prise d'antenne sur laquelle la radio émet qui décide du port retenu.", 'ag2.ant1Port': 'La prise ANT1 de la radio est câblée sur', 'ag2.portA': 'Port A', 'ag2.portB': 'Port B', 'ag2.ant1PortHint': "Câblage de la station — ni la radio ni le switch ne peuvent le dire. ANT2 va alors sur l'autre port.", 'ag2.passwordHint': "Nécessaire seulement à distance — l'appareil annonce alors « AG AUTH » et refuse les commandes tant qu'on n'est pas identifié. Laisse vide sur le réseau local.",
|
||||||
'tg2.hint': "OpsLog dialogue avec le 4O3A Tuner Genius XL en TCP (port fixé à 9010), seule l'IP de l'appareil est nécessaire. Un widget ancré affiche le ROS et la puissance directe et propose Accord, Bypass et Operate/Standby. Pilotage direct (pas via la radio) pour n'utiliser qu'une des connexions de la boîte.", 'tg2.enable': "Activer le contrôle du Tuner Genius", 'tg2.portHint': "Le port TCP est fixé à 9010 sur l'appareil.", 'tg2.password': 'Code distant', 'tg2.passwordPh': 'vide en LAN', 'tg2.passwordHint': "Nécessaire seulement à distance — l'appareil annonce alors « AUTH » et refuse les commandes tant qu'on n'est pas identifié. Laisse vide sur le réseau local.",
|
'tg2.hint': "OpsLog dialogue avec le 4O3A Tuner Genius XL en TCP (port fixé à 9010), seule l'IP de l'appareil est nécessaire. Un widget ancré affiche le ROS et la puissance directe et propose Accord, Bypass et Operate/Standby. Pilotage direct (pas via la radio) pour n'utiliser qu'une des connexions de la boîte.", 'tg2.enable': "Activer le contrôle du Tuner Genius", 'tg2.portHint': "Le port TCP est fixé à 9010 sur l'appareil.", 'tg2.password': 'Code distant', 'tg2.passwordPh': 'vide en LAN', 'tg2.passwordHint': "Nécessaire seulement à distance — l'appareil annonce alors « AUTH » et refuse les commandes tant qu'on n'est pas identifié. Laisse vide sur le réseau local.",
|
||||||
'rot.spidHint': 'Protocole SPID natif sur le port COM du contrôleur — sans PstRotator. Choisissez le dialecte ci-dessus : Rot2Prog répond azimut et élévation à 600 bauds, Rot1Prog azimut seul à 1200.', 'rot.spidModel': 'Protocole SPID',
|
'rot.spidHint': 'Protocole SPID natif sur le port COM du contrôleur — sans PstRotator. Choisissez le dialecte ci-dessus : Rot2Prog répond azimut et élévation à 600 bauds, Rot1Prog azimut seul à 1200.', 'rot.spidModel': 'Protocole SPID',
|
||||||
'rot.widget': 'Widget', 'rot.compact': 'Mode compact', 'rot.compactHint': 'N’afficher que le cadran et les azimuts courte/longue distance. Les boutons de rotation rapide, la case d’azimut et Stop sont masqués — tourne l’antenne depuis la pastille de cap de la barre de saisie.',
|
'rot.widget': 'Widget', 'rot.compact': 'Mode compact', 'rot.compactHint': 'N’afficher que le cadran et les azimuts courte/longue distance. Les boutons de rotation rapide, la case d’azimut et Stop sont masqués — tourne l’antenne depuis la pastille de cap de la barre de saisie.',
|
||||||
|
|||||||
@@ -1738,6 +1738,8 @@ export namespace main {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
host: string;
|
host: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
use_for_my_antenna: boolean;
|
||||||
|
ant1_port: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new AntGeniusSettings(source);
|
return new AntGeniusSettings(source);
|
||||||
@@ -1748,6 +1750,8 @@ export namespace main {
|
|||||||
this.enabled = source["enabled"];
|
this.enabled = source["enabled"];
|
||||||
this.host = source["host"];
|
this.host = source["host"];
|
||||||
this.password = source["password"];
|
this.password = source["password"];
|
||||||
|
this.use_for_my_antenna = source["use_for_my_antenna"];
|
||||||
|
this.ant1_port = source["ant1_port"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class AudioSettings {
|
export class AudioSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user