up
This commit is contained in:
@@ -127,6 +127,8 @@ 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
|
||||||
|
keyUltrabeamStep = "ultrabeam.step_khz" // re-tune hysteresis: 25 | 50 | 100 kHz
|
||||||
|
|
||||||
// WinKeyer CW keyer (serial) — Hardware → CW Keyer.
|
// WinKeyer CW keyer (serial) — Hardware → CW Keyer.
|
||||||
keyWKEnabled = "winkeyer.enabled"
|
keyWKEnabled = "winkeyer.enabled"
|
||||||
@@ -349,9 +351,10 @@ type App struct {
|
|||||||
udpRepo *udp.Repo
|
udpRepo *udp.Repo
|
||||||
extsvc *extsvc.Manager
|
extsvc *extsvc.Manager
|
||||||
winkeyer *winkeyer.Manager
|
winkeyer *winkeyer.Manager
|
||||||
clublog *clublog.Manager
|
clublog *clublog.Manager
|
||||||
ultrabeam *ultrabeam.Client // Ultrabeam antenna (TCP); nil when disabled
|
ultrabeam *ultrabeam.Client // Ultrabeam antenna (TCP); nil when disabled
|
||||||
audioMgr *audio.Manager
|
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
||||||
|
audioMgr *audio.Manager
|
||||||
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
|
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
|
||||||
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
||||||
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
||||||
@@ -3969,6 +3972,7 @@ func (a *App) pttKey(cfg AudioSettings) error {
|
|||||||
return fmt.Errorf("CAT not initialized")
|
return fmt.Errorf("CAT not initialized")
|
||||||
}
|
}
|
||||||
if err := a.cat.SetPTT(true); err != nil {
|
if err := a.cat.SetPTT(true); err != nil {
|
||||||
|
applog.Printf("ptt: CAT SetPTT failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
a.pttMu.Lock()
|
a.pttMu.Lock()
|
||||||
@@ -4045,6 +4049,11 @@ func (a *App) pttUnkey() {
|
|||||||
// UI selection), so the user can test a method/port without saving first —
|
// UI selection), so the user can test a method/port without saving first —
|
||||||
// matching TestRotator / TestUltrabeam.
|
// matching TestRotator / TestUltrabeam.
|
||||||
func (a *App) TestPTT(cfg AudioSettings) error {
|
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" {
|
if cfg.PTTMethod == "" || cfg.PTTMethod == "none" {
|
||||||
return fmt.Errorf("PTT method is None (VOX) — pick CAT, RTS or DTR first")
|
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"`
|
Enabled bool `json:"enabled"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
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.
|
// GetUltrabeamSettings returns the persisted Ultrabeam config with defaults.
|
||||||
func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
||||||
out := UltrabeamSettings{Port: 23}
|
out := UltrabeamSettings{Port: 23, StepKHz: 50}
|
||||||
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, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort)
|
m, err := a.settings.GetMany(a.ctx, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort, keyUltrabeamFollow, keyUltrabeamStep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
@@ -6022,6 +6033,10 @@ func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
|||||||
if p, _ := strconv.Atoi(m[keyUltrabeamPort]); p > 0 && p <= 65535 {
|
if p, _ := strconv.Atoi(m[keyUltrabeamPort]); p > 0 && p <= 65535 {
|
||||||
out.Port = p
|
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
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6034,10 +6049,15 @@ func (a *App) SaveUltrabeamSettings(s UltrabeamSettings) error {
|
|||||||
if s.Port <= 0 || s.Port > 65535 {
|
if s.Port <= 0 || s.Port > 65535 {
|
||||||
s.Port = 23
|
s.Port = 23
|
||||||
}
|
}
|
||||||
|
if s.StepKHz != 25 && s.StepKHz != 50 && s.StepKHz != 100 {
|
||||||
|
s.StepKHz = 50
|
||||||
|
}
|
||||||
for k, v := range map[string]string{
|
for k, v := range map[string]string{
|
||||||
keyUltrabeamEnabled: boolStr(s.Enabled),
|
keyUltrabeamEnabled: boolStr(s.Enabled),
|
||||||
keyUltrabeamHost: strings.TrimSpace(s.Host),
|
keyUltrabeamHost: strings.TrimSpace(s.Host),
|
||||||
keyUltrabeamPort: strconv.Itoa(s.Port),
|
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 {
|
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||||
return err
|
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
|
// antenna is enabled and configured. Safe to call repeatedly (on startup and
|
||||||
// after a settings save).
|
// after a settings save).
|
||||||
func (a *App) startUltrabeam() {
|
func (a *App) startUltrabeam() {
|
||||||
|
// Stop any running follow loop first.
|
||||||
|
if a.ubFollowStop != nil {
|
||||||
|
close(a.ubFollowStop)
|
||||||
|
a.ubFollowStop = nil
|
||||||
|
}
|
||||||
if a.ultrabeam != nil {
|
if a.ultrabeam != nil {
|
||||||
a.ultrabeam.Stop()
|
a.ultrabeam.Stop()
|
||||||
a.ultrabeam = nil
|
a.ultrabeam = nil
|
||||||
@@ -6061,6 +6086,61 @@ func (a *App) startUltrabeam() {
|
|||||||
}
|
}
|
||||||
a.ultrabeam = ultrabeam.New(s.Host, s.Port)
|
a.ultrabeam = ultrabeam.New(s.Host, s.Port)
|
||||||
_ = a.ultrabeam.Start()
|
_ = 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 +
|
// UltrabeamStatusInfo is the live antenna status for the UI (status bar +
|
||||||
|
|||||||
@@ -2535,6 +2535,9 @@ export default function App() {
|
|||||||
if (m) setMode(m);
|
if (m) setMode(m);
|
||||||
}
|
}
|
||||||
onCallsignInput(s.dx_call);
|
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);
|
if (m) setMode(m);
|
||||||
}
|
}
|
||||||
onCallsignInput(s.dx_call);
|
onCallsignInput(s.dx_call);
|
||||||
|
if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {});
|
||||||
}}
|
}}
|
||||||
onClose={() => setShowBandMap(false)}
|
onClose={() => setShowBandMap(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -370,8 +370,8 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||||
|
|
||||||
// Ultrabeam antenna (TCP) settings.
|
// Ultrabeam antenna (TCP) settings.
|
||||||
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; host: string; port: number }>({
|
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; host: string; port: number; follow: boolean; step_khz: number }>({
|
||||||
enabled: false, host: '', port: 23,
|
enabled: false, host: '', port: 23, follow: false, step_khz: 50,
|
||||||
});
|
});
|
||||||
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);
|
||||||
@@ -1554,6 +1554,26 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
<Button variant="outline" size="sm" onClick={testUltrabeam} disabled={ubTesting || !ultrabeam.host.trim()}>
|
<Button variant="outline" size="sm" onClick={testUltrabeam} disabled={ubTesting || !ultrabeam.host.trim()}>
|
||||||
{ubTesting ? 'Connecting…' : 'Test connection'}
|
{ubTesting ? 'Connecting…' : 'Test connection'}
|
||||||
|
|||||||
@@ -1286,6 +1286,8 @@ export namespace main {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
follow: boolean;
|
||||||
|
step_khz: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new UltrabeamSettings(source);
|
return new UltrabeamSettings(source);
|
||||||
@@ -1296,6 +1298,8 @@ export namespace main {
|
|||||||
this.enabled = source["enabled"];
|
this.enabled = source["enabled"];
|
||||||
this.host = source["host"];
|
this.host = source["host"];
|
||||||
this.port = source["port"];
|
this.port = source["port"];
|
||||||
|
this.follow = source["follow"];
|
||||||
|
this.step_khz = source["step_khz"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class UltrabeamStatusInfo {
|
export class UltrabeamStatusInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user