This commit is contained in:
2026-06-07 11:00:25 +02:00
parent 8040a37315
commit d0526c39f5
4 changed files with 115 additions and 7 deletions
+82 -2
View File
@@ -127,6 +127,8 @@ const (
keyUltrabeamEnabled = "ultrabeam.enabled"
keyUltrabeamHost = "ultrabeam.host"
keyUltrabeamPort = "ultrabeam.port"
keyUltrabeamFollow = "ultrabeam.follow" // "1" → re-tune to the rig frequency
keyUltrabeamStep = "ultrabeam.step_khz" // re-tune hysteresis: 25 | 50 | 100 kHz
// WinKeyer CW keyer (serial) — Hardware → CW Keyer.
keyWKEnabled = "winkeyer.enabled"
@@ -351,6 +353,7 @@ type App struct {
winkeyer *winkeyer.Manager
clublog *clublog.Manager
ultrabeam *ultrabeam.Client // Ultrabeam antenna (TCP); nil when disabled
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
@@ -3969,6 +3972,7 @@ func (a *App) pttKey(cfg AudioSettings) error {
return fmt.Errorf("CAT not initialized")
}
if err := a.cat.SetPTT(true); err != nil {
applog.Printf("ptt: CAT SetPTT failed: %v", err)
return err
}
a.pttMu.Lock()
@@ -4045,6 +4049,11 @@ func (a *App) pttUnkey() {
// UI selection), so the user can test a method/port without saving first —
// matching TestRotator / TestUltrabeam.
func (a *App) TestPTT(cfg AudioSettings) error {
if cfg.PTTMethod == "rts" || cfg.PTTMethod == "dtr" {
applog.Printf("ptt: TestPTT method=%q port=%q", cfg.PTTMethod, cfg.PTTPort)
} else {
applog.Printf("ptt: TestPTT method=%q (CAT via OmniRig — serial port not used)", cfg.PTTMethod)
}
if cfg.PTTMethod == "" || cfg.PTTMethod == "none" {
return fmt.Errorf("PTT method is None (VOX) — pick CAT, RTS or DTR first")
}
@@ -6005,15 +6014,17 @@ type UltrabeamSettings struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
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)
}
// GetUltrabeamSettings returns the persisted Ultrabeam config with defaults.
func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
out := UltrabeamSettings{Port: 23}
out := UltrabeamSettings{Port: 23, StepKHz: 50}
if a.settings == nil {
return out, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort)
m, err := a.settings.GetMany(a.ctx, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort, keyUltrabeamFollow, keyUltrabeamStep)
if err != nil {
return out, err
}
@@ -6022,6 +6033,10 @@ func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
if p, _ := strconv.Atoi(m[keyUltrabeamPort]); p > 0 && p <= 65535 {
out.Port = p
}
out.Follow = m[keyUltrabeamFollow] == "1"
if st, _ := strconv.Atoi(m[keyUltrabeamStep]); st == 25 || st == 50 || st == 100 {
out.StepKHz = st
}
return out, nil
}
@@ -6034,10 +6049,15 @@ func (a *App) SaveUltrabeamSettings(s UltrabeamSettings) error {
if s.Port <= 0 || s.Port > 65535 {
s.Port = 23
}
if s.StepKHz != 25 && s.StepKHz != 50 && s.StepKHz != 100 {
s.StepKHz = 50
}
for k, v := range map[string]string{
keyUltrabeamEnabled: boolStr(s.Enabled),
keyUltrabeamHost: strings.TrimSpace(s.Host),
keyUltrabeamPort: strconv.Itoa(s.Port),
keyUltrabeamFollow: boolStr(s.Follow),
keyUltrabeamStep: strconv.Itoa(s.StepKHz),
} {
if err := a.settings.Set(a.ctx, k, v); err != nil {
return err
@@ -6051,6 +6071,11 @@ func (a *App) SaveUltrabeamSettings(s UltrabeamSettings) error {
// antenna is enabled and configured. Safe to call repeatedly (on startup and
// after a settings save).
func (a *App) startUltrabeam() {
// Stop any running follow loop first.
if a.ubFollowStop != nil {
close(a.ubFollowStop)
a.ubFollowStop = nil
}
if a.ultrabeam != nil {
a.ultrabeam.Stop()
a.ultrabeam = nil
@@ -6061,6 +6086,61 @@ func (a *App) startUltrabeam() {
}
a.ultrabeam = ultrabeam.New(s.Host, s.Port)
_ = a.ultrabeam.Start()
if s.Follow {
stop := make(chan struct{})
a.ubFollowStop = stop
go a.ultrabeamFollowLoop(a.ultrabeam, s.StepKHz, stop)
}
}
// 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
// elements track the band without the motors chasing every small QSY. Runs
// until stop is closed (a settings change or shutdown).
func (a *App) ultrabeamFollowLoop(c *ultrabeam.Client, stepKHz int, stop <-chan struct{}) {
if stepKHz <= 0 {
stepKHz = 50
}
ticker := time.NewTicker(1500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stop:
return
case <-ticker.C:
if a.cat == nil {
continue
}
rs := a.cat.State()
if !rs.Connected || rs.FreqHz <= 0 {
continue
}
st, err := c.GetStatus()
if err != nil || st == nil || !st.Connected {
continue
}
rigKHz := int(rs.FreqHz / 1000)
// Skip frequencies outside the antenna's tunable range (other band).
if st.FreqMin > 0 && st.FreqMax > 0 {
rigMHz := rs.FreqHz / 1_000_000
if rigMHz < int64(st.FreqMin) || rigMHz > int64(st.FreqMax) {
continue
}
}
diff := rigKHz - st.Frequency
if diff < 0 {
diff = -diff
}
if st.Frequency > 0 && diff < stepKHz {
continue // within the deadband — leave the motors alone
}
if err := c.SetFrequency(rigKHz, st.Direction); err != nil {
applog.Printf("ultrabeam: follow re-tune to %d kHz failed: %v", rigKHz, err)
} else {
applog.Printf("ultrabeam: followed rig → %d kHz (dir %d)", rigKHz, st.Direction)
}
}
}
}
// UltrabeamStatusInfo is the live antenna status for the UI (status bar +
+4
View File
@@ -2535,6 +2535,9 @@ export default function App() {
if (m) setMode(m);
}
onCallsignInput(s.dx_call);
// Clicking a spot fills the call programmatically (no blur
// on the call field), so start the QSO recording here too.
if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {});
}}
/>
);
@@ -2750,6 +2753,7 @@ export default function App() {
if (m) setMode(m);
}
onCallsignInput(s.dx_call);
if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {});
}}
onClose={() => setShowBandMap(false)}
/>
+22 -2
View File
@@ -370,8 +370,8 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
// Ultrabeam antenna (TCP) settings.
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; host: string; port: number }>({
enabled: false, host: '', port: 23,
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; host: string; port: number; follow: boolean; step_khz: number }>({
enabled: false, host: '', port: 23, follow: false, step_khz: 50,
});
const [ubTesting, setUbTesting] = useState(false);
const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);
@@ -1554,6 +1554,26 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
/>
</div>
</div>
<div className="border-t border-border/60 pt-3 space-y-2">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={ultrabeam.follow} onCheckedChange={(c) => setUltrabeam((s) => ({ ...s, follow: !!c }))} />
Follow rig frequency (auto-tune the antenna)
</label>
{ultrabeam.follow && (
<div className="flex items-center gap-3 pl-6">
<Label className="text-sm">Re-tune step</Label>
<Select value={String(ultrabeam.step_khz)} onValueChange={(v) => setUltrabeam((s) => ({ ...s, step_khz: parseInt(v, 10) || 50 }))}>
<SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="25">25 kHz</SelectItem>
<SelectItem value="50">50 kHz</SelectItem>
<SelectItem value="100">100 kHz</SelectItem>
</SelectContent>
</Select>
<span className="text-xs text-muted-foreground">re-tune only when the frequency moves this far</span>
</div>
)}
</div>
<div className="flex items-center gap-2 pt-2">
<Button variant="outline" size="sm" onClick={testUltrabeam} disabled={ubTesting || !ultrabeam.host.trim()}>
{ubTesting ? 'Connecting…' : 'Test connection'}
+4
View File
@@ -1286,6 +1286,8 @@ export namespace main {
enabled: boolean;
host: string;
port: number;
follow: boolean;
step_khz: number;
static createFrom(source: any = {}) {
return new UltrabeamSettings(source);
@@ -1296,6 +1298,8 @@ export namespace main {
this.enabled = source["enabled"];
this.host = source["host"];
this.port = source["port"];
this.follow = source["follow"];
this.step_khz = source["step_khz"];
}
}
export class UltrabeamStatusInfo {