up
This commit is contained in:
@@ -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 +
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user