feat: While using Flex & Steppir or Ultrabeam can prevent transmit when antenna is moving
This commit is contained in:
@@ -162,12 +162,13 @@ const (
|
|||||||
keyUltrabeamEnabled = "ultrabeam.enabled"
|
keyUltrabeamEnabled = "ultrabeam.enabled"
|
||||||
keyUltrabeamHost = "ultrabeam.host"
|
keyUltrabeamHost = "ultrabeam.host"
|
||||||
keyUltrabeamPort = "ultrabeam.port"
|
keyUltrabeamPort = "ultrabeam.port"
|
||||||
keyUltrabeamFollow = "ultrabeam.follow" // "1" → re-tune to the rig frequency
|
keyUltrabeamFollow = "ultrabeam.follow" // "1" → re-tune to the rig frequency
|
||||||
keyUltrabeamStep = "ultrabeam.step_khz" // re-tune hysteresis: 25 | 50 | 100 kHz
|
keyUltrabeamStep = "ultrabeam.step_khz" // re-tune hysteresis: 25 | 50 | 100 kHz
|
||||||
keyMotorType = "ultrabeam.type" // "ultrabeam" | "steppir" (default ultrabeam)
|
keyMotorType = "ultrabeam.type" // "ultrabeam" | "steppir" (default ultrabeam)
|
||||||
keyMotorTransport = "ultrabeam.transport" // "tcp" | "serial" (default tcp)
|
keyMotorTransport = "ultrabeam.transport" // "tcp" | "serial" (default tcp)
|
||||||
keyMotorCOM = "ultrabeam.com" // serial device name (COM3, /dev/ttyUSB0)
|
keyMotorCOM = "ultrabeam.com" // serial device name (COM3, /dev/ttyUSB0)
|
||||||
keyMotorBaud = "ultrabeam.baud" // serial baud (SteppIR default 9600)
|
keyMotorBaud = "ultrabeam.baud" // serial baud (SteppIR default 9600)
|
||||||
|
keyMotorTXInhibit = "ultrabeam.tx_inhibit" // "1" → block Flex TX while the antenna is moving
|
||||||
|
|
||||||
// Antenna Genius (4O3A) antenna switch — Hardware → Antenna Genius. TCP
|
// Antenna Genius (4O3A) antenna switch — Hardware → Antenna Genius. TCP
|
||||||
// port is fixed at 9007, so only the IP is configurable.
|
// port is fixed at 9007, so only the IP is configurable.
|
||||||
@@ -441,6 +442,9 @@ type App struct {
|
|||||||
clublog *clublog.Manager
|
clublog *clublog.Manager
|
||||||
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
|
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
|
||||||
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
||||||
|
motorInhibStop chan struct{} // stops the "inhibit TX while moving" loop; nil when off
|
||||||
|
motorMoveCmdNs atomic.Int64 // unixnano of the last commanded antenna move (grace window)
|
||||||
|
motorInhibited atomic.Bool // TX currently inhibited by the motor-antenna watcher
|
||||||
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
|
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
|
||||||
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
|
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
|
||||||
audioMgr *audio.Manager
|
audioMgr *audio.Manager
|
||||||
@@ -9247,6 +9251,7 @@ func (a *App) ultrabeamFollowNow(freqHz int64) {
|
|||||||
applog.Printf("ultrabeam: followNow within deadband (%d kHz vs ref %d, step %d) — no move", khz, ref, step)
|
applog.Printf("ultrabeam: followNow within deadband (%d kHz vs ref %d, step %d) — no move", khz, ref, step)
|
||||||
return // within the deadband — don't chase a tiny QSY
|
return // within the deadband — don't chase a tiny QSY
|
||||||
}
|
}
|
||||||
|
a.noteMotorMoveCommanded()
|
||||||
if err := c.SetFrequency(khz, st.Direction); err != nil {
|
if err := c.SetFrequency(khz, st.Direction); err != nil {
|
||||||
applog.Printf("ultrabeam: immediate re-tune to %d kHz failed: %v", khz, err)
|
applog.Printf("ultrabeam: immediate re-tune to %d kHz failed: %v", khz, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -10590,14 +10595,15 @@ func (a steppirAdapter) Status() motorStatus {
|
|||||||
// is kept (bindings + frontend) though it now covers SteppIR too.
|
// is kept (bindings + frontend) though it now covers SteppIR too.
|
||||||
type UltrabeamSettings struct {
|
type UltrabeamSettings struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Type string `json:"type"` // "ultrabeam" | "steppir"
|
Type string `json:"type"` // "ultrabeam" | "steppir"
|
||||||
Transport string `json:"transport"` // "tcp" | "serial"
|
Transport string `json:"transport"` // "tcp" | "serial"
|
||||||
Host string `json:"host"` // tcp
|
Host string `json:"host"` // tcp
|
||||||
Port int `json:"port"` // tcp
|
Port int `json:"port"` // tcp
|
||||||
COM string `json:"com"` // serial device
|
COM string `json:"com"` // serial device
|
||||||
Baud int `json:"baud"` // serial baud
|
Baud int `json:"baud"` // serial baud
|
||||||
Follow bool `json:"follow"` // re-tune the antenna to the rig's frequency
|
Follow bool `json:"follow"` // re-tune the antenna to the rig's frequency
|
||||||
StepKHz int `json:"step_khz"` // re-tune only when the freq moved this far (25/50/100)
|
StepKHz int `json:"step_khz"` // re-tune only when the freq moved this far (25/50/100)
|
||||||
|
TXInhibit bool `json:"tx_inhibit"` // block Flex transmission while the elements are moving
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUltrabeamSettings returns the persisted motorized-antenna config, defaulting
|
// GetUltrabeamSettings returns the persisted motorized-antenna config, defaulting
|
||||||
@@ -10609,7 +10615,7 @@ func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
|||||||
return out, fmt.Errorf("db not initialized")
|
return out, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
m, err := a.settings.GetMany(a.ctx, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort, keyUltrabeamFollow, keyUltrabeamStep,
|
m, err := a.settings.GetMany(a.ctx, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort, keyUltrabeamFollow, keyUltrabeamStep,
|
||||||
keyMotorType, keyMotorTransport, keyMotorCOM, keyMotorBaud)
|
keyMotorType, keyMotorTransport, keyMotorCOM, keyMotorBaud, keyMotorTXInhibit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
@@ -10629,6 +10635,7 @@ func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
|||||||
out.Baud = b
|
out.Baud = b
|
||||||
}
|
}
|
||||||
out.Follow = m[keyUltrabeamFollow] == "1"
|
out.Follow = m[keyUltrabeamFollow] == "1"
|
||||||
|
out.TXInhibit = m[keyMotorTXInhibit] == "1"
|
||||||
if st, _ := strconv.Atoi(m[keyUltrabeamStep]); st == 25 || st == 50 || st == 100 {
|
if st, _ := strconv.Atoi(m[keyUltrabeamStep]); st == 25 || st == 50 || st == 100 {
|
||||||
out.StepKHz = st
|
out.StepKHz = st
|
||||||
}
|
}
|
||||||
@@ -10666,6 +10673,7 @@ func (a *App) SaveUltrabeamSettings(s UltrabeamSettings) error {
|
|||||||
keyMotorTransport: s.Transport,
|
keyMotorTransport: s.Transport,
|
||||||
keyMotorCOM: strings.TrimSpace(s.COM),
|
keyMotorCOM: strings.TrimSpace(s.COM),
|
||||||
keyMotorBaud: strconv.Itoa(s.Baud),
|
keyMotorBaud: strconv.Itoa(s.Baud),
|
||||||
|
keyMotorTXInhibit: boolStr(s.TXInhibit),
|
||||||
} {
|
} {
|
||||||
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
|
||||||
@@ -10705,6 +10713,11 @@ func (a *App) startUltrabeam() {
|
|||||||
close(a.ubFollowStop)
|
close(a.ubFollowStop)
|
||||||
a.ubFollowStop = nil
|
a.ubFollowStop = nil
|
||||||
}
|
}
|
||||||
|
// Stop the inhibit watcher (its deferred cleanup releases any active inhibit).
|
||||||
|
if a.motorInhibStop != nil {
|
||||||
|
close(a.motorInhibStop)
|
||||||
|
a.motorInhibStop = nil
|
||||||
|
}
|
||||||
if a.motorAnt != nil {
|
if a.motorAnt != nil {
|
||||||
// Background teardown so saving Settings doesn't block on an in-progress
|
// Background teardown so saving Settings doesn't block on an in-progress
|
||||||
// connect (Stop waits for the dial timeout).
|
// connect (Stop waits for the dial timeout).
|
||||||
@@ -10726,12 +10739,68 @@ func (a *App) startUltrabeam() {
|
|||||||
a.ubFollowStop = stop
|
a.ubFollowStop = stop
|
||||||
go a.ultrabeamFollowLoop(a.motorAnt, s.StepKHz, stop)
|
go a.ultrabeamFollowLoop(a.motorAnt, s.StepKHz, stop)
|
||||||
}
|
}
|
||||||
|
if s.TXInhibit {
|
||||||
|
stop := make(chan struct{})
|
||||||
|
a.motorInhibStop = stop
|
||||||
|
go a.motorTXInhibitLoop(a.motorAnt, stop)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ultrabeamFollowLoop re-tunes the antenna to the rig's current frequency
|
// ultrabeamFollowLoop re-tunes the antenna to the rig's current frequency
|
||||||
// whenever it drifts at least stepKHz from what the antenna is set to — so the
|
// whenever it drifts at least stepKHz from what the antenna is set to — so the
|
||||||
// elements track the band without the motors chasing every small QSY. Runs
|
// elements track the band without the motors chasing every small QSY. Runs
|
||||||
// until stop is closed (a settings change or shutdown).
|
// until stop is closed (a settings change or shutdown).
|
||||||
|
// noteMotorMoveCommanded marks that OpsLog just told the antenna to move. The
|
||||||
|
// inhibit watcher polls the antenna's Moving status, but that status is only
|
||||||
|
// refreshed every couple of seconds — so a fresh command opens a short grace
|
||||||
|
// window during which TX stays inhibited even before the poll confirms motion,
|
||||||
|
// closing the gap at the start of a move.
|
||||||
|
func (a *App) noteMotorMoveCommanded() { a.motorMoveCmdNs.Store(time.Now().UnixNano()) }
|
||||||
|
|
||||||
|
// applyMotorInhibit sets or clears the Flex transmit inhibit, but only when the
|
||||||
|
// active CAT backend IS a FlexRadio — the inhibit is a Flex-API feature; with any
|
||||||
|
// other rig the option simply does nothing (as documented in the UI). Idempotent.
|
||||||
|
func (a *App) applyMotorInhibit(on bool) {
|
||||||
|
if a.cat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.cat.State().Backend != "flex" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.motorInhibited.Load() == on {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetTXInhibit(on) }); err != nil {
|
||||||
|
applog.Printf("motor-antenna: TX inhibit=%v failed: %v", on, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.motorInhibited.Store(on)
|
||||||
|
applog.Printf("motor-antenna: Flex TX inhibit %s (antenna moving)", map[bool]string{true: "ON", false: "OFF"}[on])
|
||||||
|
}
|
||||||
|
|
||||||
|
// motorTXInhibitLoop blocks Flex transmission while the motorized antenna's
|
||||||
|
// elements are moving. Runs only when the option is on and a motor antenna is
|
||||||
|
// active; it errs toward SAFE — TX is inhibited while the status reports motion
|
||||||
|
// OR within a grace window after a commanded move — and always releases the
|
||||||
|
// inhibit when it stops (so a settings change / shutdown never leaves TX blocked).
|
||||||
|
func (a *App) motorTXInhibitLoop(c motorAntenna, stop <-chan struct{}) {
|
||||||
|
const grace = 3 * time.Second // cover the poll latency at the very start of a move
|
||||||
|
ticker := time.NewTicker(300 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
defer a.applyMotorInhibit(false) // never leave TX blocked when the loop ends
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
st := c.Status()
|
||||||
|
recentCmd := time.Since(time.Unix(0, a.motorMoveCmdNs.Load())) < grace
|
||||||
|
moving := st.Connected && (st.Moving || recentCmd)
|
||||||
|
a.applyMotorInhibit(moving)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) ultrabeamFollowLoop(c motorAntenna, stepKHz int, stop <-chan struct{}) {
|
func (a *App) ultrabeamFollowLoop(c motorAntenna, stepKHz int, stop <-chan struct{}) {
|
||||||
if stepKHz <= 0 {
|
if stepKHz <= 0 {
|
||||||
stepKHz = 50
|
stepKHz = 50
|
||||||
@@ -10786,6 +10855,7 @@ func (a *App) ultrabeamFollowLoop(c motorAntenna, stepKHz int, stop <-chan struc
|
|||||||
if ref > 0 && diff < stepKHz {
|
if ref > 0 && diff < stepKHz {
|
||||||
continue // within the deadband — leave the motors alone
|
continue // within the deadband — leave the motors alone
|
||||||
}
|
}
|
||||||
|
a.noteMotorMoveCommanded()
|
||||||
if err := c.SetFrequency(rigKHz, st.Direction); err != nil {
|
if err := c.SetFrequency(rigKHz, st.Direction); err != nil {
|
||||||
applog.Printf("ultrabeam: follow re-tune to %d kHz failed: %v", rigKHz, err)
|
applog.Printf("ultrabeam: follow re-tune to %d kHz failed: %v", rigKHz, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -10837,6 +10907,7 @@ func (a *App) SetUltrabeamDirection(direction int) error {
|
|||||||
// current frequency with the new direction byte. If the antenna hasn't reported
|
// current frequency with the new direction byte. If the antenna hasn't reported
|
||||||
// a frequency yet (just connected / link settling), fall back to the rig's CAT
|
// a frequency yet (just connected / link settling), fall back to the rig's CAT
|
||||||
// frequency so the control still works.
|
// frequency so the control still works.
|
||||||
|
a.noteMotorMoveCommanded()
|
||||||
st := a.motorAnt.Status()
|
st := a.motorAnt.Status()
|
||||||
if st.Frequency <= 0 && a.cat != nil {
|
if st.Frequency <= 0 && a.cat != nil {
|
||||||
if rs := a.cat.State(); rs.Connected && rs.FreqHz > 0 {
|
if rs := a.cat.State(); rs.Connected && rs.FreqHz > 0 {
|
||||||
|
|||||||
@@ -827,8 +827,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||||
|
|
||||||
// Motorized antenna (Ultrabeam TCP or SteppIR TCP/serial) settings.
|
// Motorized antenna (Ultrabeam TCP or SteppIR TCP/serial) settings.
|
||||||
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; type: string; transport: string; host: string; port: number; com: string; baud: number; follow: boolean; step_khz: number }>({
|
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; type: string; transport: string; host: string; port: number; com: string; baud: number; follow: boolean; step_khz: number; tx_inhibit: boolean }>({
|
||||||
enabled: false, type: 'ultrabeam', transport: 'tcp', host: '', port: 23, com: '', baud: 9600, follow: false, step_khz: 50,
|
enabled: false, type: 'ultrabeam', transport: 'tcp', host: '', port: 23, com: '', baud: 9600, follow: false, step_khz: 50, tx_inhibit: false,
|
||||||
});
|
});
|
||||||
const [ubTesting, setUbTesting] = useState(false);
|
const [ubTesting, setUbTesting] = useState(false);
|
||||||
const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||||
@@ -2373,6 +2373,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-t border-border/60 pt-3 space-y-1">
|
||||||
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<Checkbox checked={ultrabeam.tx_inhibit} onCheckedChange={(c) => setUltrabeam((s) => ({ ...s, tx_inhibit: !!c }))} />
|
||||||
|
{t('hw.motorTxInhibit')}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground pl-6">{t('hw.motorTxInhibitHint')}</p>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2 pt-2">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
<Button variant="outline" size="sm" onClick={testUltrabeam} disabled={ubTesting || (isSerial ? !ultrabeam.com.trim() : !ultrabeam.host.trim())}>
|
<Button variant="outline" size="sm" onClick={testUltrabeam} disabled={ubTesting || (isSerial ? !ultrabeam.com.trim() : !ultrabeam.host.trim())}>
|
||||||
{ubTesting ? t('hw.connecting') : t('hw.testConn')}
|
{ubTesting ? t('hw.connecting') : t('hw.testConn')}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const en: Dict = {
|
|||||||
'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.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.',
|
||||||
'rot.hint': "OpsLog sends UDP commands to PstRotator. Enable PstRotator's UDP listener (Setup → Communication → UDP) before testing.",
|
'rot.hint': "OpsLog sends UDP commands to PstRotator. Enable PstRotator's UDP listener (Setup → Communication → UDP) before testing.",
|
||||||
'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.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.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.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 (any rig, Windows COM)', 'cat.optFlex': 'FlexRadio / SmartSDR (native)', 'cat.optIcom': 'Icom CI-V (USB serial)', 'cat.optIcomNet': 'Icom CI-V (network / remote)', 'cat.optTci': 'TCI (Expert Electronics / SunSDR)',
|
'cat.enable': 'Enable CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig (any rig, Windows COM)', 'cat.optFlex': 'FlexRadio / SmartSDR (native)', 'cat.optIcom': 'Icom CI-V (USB serial)', 'cat.optIcomNet': 'Icom CI-V (network / remote)', 'cat.optTci': 'TCI (Expert Electronics / SunSDR)',
|
||||||
'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',
|
||||||
@@ -420,7 +420,7 @@ const fr: Dict = {
|
|||||||
'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.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.",
|
||||||
'rot.hint': "OpsLog envoie des commandes UDP à PstRotator. Active l'écouteur UDP de PstRotator (Setup → Communication → UDP) avant de tester.",
|
'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.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.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.ultrabeam': 'Antenne (Ultrabeam)', 'hw.audioVoice': 'Périphériques audio & manipulateur vocal',
|
||||||
'cat.enable': 'Activer le CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig (tout poste, COM Windows)', 'cat.optFlex': 'FlexRadio / SmartSDR (natif)', 'cat.optIcom': 'Icom CI-V (USB série)', 'cat.optIcomNet': 'Icom CI-V (réseau / remote)', 'cat.optTci': 'TCI (Expert Electronics / SunSDR)',
|
'cat.enable': 'Activer le CAT', 'cat.backend': 'Backend', 'cat.optOmnirig': 'OmniRig (tout poste, COM Windows)', 'cat.optFlex': 'FlexRadio / SmartSDR (natif)', 'cat.optIcom': 'Icom CI-V (USB série)', 'cat.optIcomNet': 'Icom CI-V (réseau / remote)', 'cat.optTci': 'TCI (Expert Electronics / SunSDR)',
|
||||||
'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.",
|
||||||
|
|||||||
@@ -2473,6 +2473,7 @@ export namespace main {
|
|||||||
baud: number;
|
baud: number;
|
||||||
follow: boolean;
|
follow: boolean;
|
||||||
step_khz: number;
|
step_khz: number;
|
||||||
|
tx_inhibit: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new UltrabeamSettings(source);
|
return new UltrabeamSettings(source);
|
||||||
@@ -2489,6 +2490,7 @@ export namespace main {
|
|||||||
this.baud = source["baud"];
|
this.baud = source["baud"];
|
||||||
this.follow = source["follow"];
|
this.follow = source["follow"];
|
||||||
this.step_khz = source["step_khz"];
|
this.step_khz = source["step_khz"];
|
||||||
|
this.tx_inhibit = source["tx_inhibit"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class UltrabeamStatusInfo {
|
export class UltrabeamStatusInfo {
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ type FlexController interface {
|
|||||||
SetRFPower(int) error
|
SetRFPower(int) error
|
||||||
SetTunePower(int) error
|
SetTunePower(int) error
|
||||||
SetTune(bool) error
|
SetTune(bool) error
|
||||||
|
SetTXInhibit(bool) error
|
||||||
SetVOX(bool) error
|
SetVOX(bool) error
|
||||||
SetVOXLevel(int) error
|
SetVOXLevel(int) error
|
||||||
SetVOXDelay(int) error
|
SetVOXDelay(int) error
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ type flexTX struct {
|
|||||||
tunePower int
|
tunePower int
|
||||||
tune bool
|
tune bool
|
||||||
transmitting bool // interlock state == TRANSMITTING
|
transmitting bool // interlock state == TRANSMITTING
|
||||||
|
inhibit bool // transmit inhibited (e.g. while a motorized antenna moves)
|
||||||
voxEnable bool
|
voxEnable bool
|
||||||
voxLevel int
|
voxLevel int
|
||||||
voxDelay int
|
voxDelay int
|
||||||
@@ -1708,6 +1709,14 @@ func (f *Flex) SetTune(on bool) error {
|
|||||||
return f.txSet(cmd, "tune", func(t *flexTX) { t.tune = on })
|
return f.txSet(cmd, "tune", func(t *flexTX) { t.tune = on })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTXInhibit blocks (on=true) or allows transmission at the radio. Used to keep
|
||||||
|
// the operator from keying while a motorized antenna's elements are moving —
|
||||||
|
// SmartSDR refuses to transmit while inhibit is set, so it holds even against a
|
||||||
|
// footswitch or an external keyer, which a software PTT block could not.
|
||||||
|
func (f *Flex) SetTXInhibit(on bool) error {
|
||||||
|
return f.txSet("transmit set inhibit="+boolFlex(on), "inhibit", func(t *flexTX) { t.inhibit = on })
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Flex) SetVOX(on bool) error {
|
func (f *Flex) SetVOX(on bool) error {
|
||||||
return f.txSet("transmit set vox_enable="+boolFlex(on), "vox_enable", func(t *flexTX) { t.voxEnable = on })
|
return f.txSet("transmit set vox_enable="+boolFlex(on), "vox_enable", func(t *flexTX) { t.voxEnable = on })
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -286,19 +286,33 @@ func (n *icomNet) ctrlPump() {
|
|||||||
switch icnLE.Uint16(buf[4:]) {
|
switch icnLE.Uint16(buf[4:]) {
|
||||||
case 0x07: // ping
|
case 0x07: // ping
|
||||||
_, _ = n.ctrl.Write(icnPingReply(buf[:k], n.cID, n.cRemote))
|
_, _ = n.ctrl.Write(icnPingReply(buf[:k], n.cID, n.cRemote))
|
||||||
|
case 0x00: // idle keepalive from the rig — nothing to do
|
||||||
case 0x01: // retransmit request — resend from the CONTROL sent-buffer
|
case 0x01: // retransmit request — resend from the CONTROL sent-buffer
|
||||||
if k >= 8 {
|
if k >= 8 {
|
||||||
n.ctrlResend(icnLE.Uint16(buf[6:]))
|
n.ctrlResend(icnLE.Uint16(buf[6:]))
|
||||||
}
|
}
|
||||||
case 0x05: // rig-initiated disconnect — it dropped US
|
case 0x05: // rig-initiated disconnect — it dropped US
|
||||||
debugLog.Printf("icom net: rig sent DISCONNECT on control stream — session dropped by the rig")
|
debugLog.Printf("icom net: rig sent DISCONNECT on control stream — session dropped by the rig")
|
||||||
|
default:
|
||||||
|
// Anything else on the control stream is (almost always) the rig's
|
||||||
|
// reply to our token renewal. Log it: a 0x40-length token packet
|
||||||
|
// carries a result code, and if the rig is REJECTING renewals this is
|
||||||
|
// where the ~2-3 min disconnect originates. The hex makes the cause
|
||||||
|
// visible in the friend's log without a protocol analyzer.
|
||||||
|
if k >= 0x18 {
|
||||||
|
debugLog.Printf("icom net: control reply len=%d head=% X", k, buf[:0x18])
|
||||||
|
} else {
|
||||||
|
debugLog.Printf("icom net: control reply len=%d head=% X", k, buf[:k])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if time.Since(lastIdle) > 100*time.Millisecond {
|
if time.Since(lastIdle) > 100*time.Millisecond {
|
||||||
_, _ = n.ctrl.Write(icnCtrl(0x00, 0, n.cID, n.cRemote))
|
_, _ = n.ctrl.Write(icnCtrl(0x00, 0, n.cID, n.cRemote))
|
||||||
lastIdle = time.Now()
|
lastIdle = time.Now()
|
||||||
}
|
}
|
||||||
if time.Since(lastToken) > 45*time.Second {
|
// Renew well inside the rig's ~2-min token timeout. 30 s (was 45) leaves room
|
||||||
|
// for one lost renewal + its retransmit before the token would lapse.
|
||||||
|
if time.Since(lastToken) > 30*time.Second {
|
||||||
n.renewToken()
|
n.renewToken()
|
||||||
lastToken = time.Now()
|
lastToken = time.Now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ func (b *IcomSerial) ReadState() (RigState, error) {
|
|||||||
b.dspMu.Unlock()
|
b.dspMu.Unlock()
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
debugLog.Printf("icom net: control link went quiet (no rig packets for >6 s) → reconnecting. If this recurs every ~2-3 min, the rig is invalidating the session (token renewal rejected).")
|
||||||
return RigState{}, err // control link dead → let the Manager reconnect
|
return RigState{}, err // control link dead → let the Manager reconnect
|
||||||
}
|
}
|
||||||
// USB (no liveness signal): the rig briefly stops answering CI-V while it
|
// USB (no liveness signal): the rig briefly stops answering CI-V while it
|
||||||
|
|||||||
Reference in New Issue
Block a user