feat(cluster): self-spot on the master node while logging

Settings -> DX Cluster: a toggle and an interval. When it is on, logging a QSO
announces the station on the master cluster — the QSO's own station callsign as
the DX, on the frequency the contact was made on — so callers find the run
without waiting for someone else to spot it. The node fills the DE field from
the login, so the spotter is us too: a self-spot.

It fires on the FIRST QSO of a frequency and then at most once per interval.
Both halves matter: announcing every QSO would flood the node and get the
station filtered out, while a pure timer would stay silent for minutes after a
band change. A drift of up to 500 Hz still counts as the same run, so nudging
the VFO mid-pileup does not re-announce.

Five minutes is the floor, clamped in SaveSelfSpotSettings as well as in the
input: the limit protects the node from us, so it must not depend on the
frontend. The interval input keeps raw text and clamps on blur — clamping per
keystroke rewrote "10" to "5" as soon as the "1" landed.

Wired into both log paths (manual entry and UDP auto-log) on the async side, so
a cluster that is slow or down never holds up logging. A send failure restores
the previous throttle state, so the next QSO retries instead of sitting out an
interval that produced no spot.
This commit is contained in:
2026-08-08 20:34:10 +02:00
parent 90c6458af0
commit 1f55cfe9fa
7 changed files with 236 additions and 30 deletions
+157 -26
View File
@@ -287,7 +287,9 @@ const (
keyWKCWLine = "winkeyer.cw_key_line" // serial engine: "dtr" (CW) / "rts" (PTT) or swapped
keyWKCWInvert = "winkeyer.cw_invert" // serial engine: invert line polarity (active-LOW)
keyClusterAutoConnect = "cluster.auto_connect" // open every enabled server at app start
keyClusterAutoConnect = "cluster.auto_connect" // open every enabled server at app start
keyClusterSelfSpot = "cluster.self_spot" // "1" → announce ourselves on the cluster as we log
keyClusterSelfSpotMin = "cluster.self_spot_minutes" // shortest gap between two self-spots
keyScpEnabled = "scp.enabled" // Super Check Partial / N+1 suggestions on
@@ -581,31 +583,36 @@ type App struct {
// or when a setting that shapes the maps flips.
clusterStatusIdx *clusterStatusCache
clusterStatusMu sync.Mutex
pota *pota.Cache
uls *uls.Store // US callsign→county/grid (offline FCC ULS), lazily opened
awardRefs *awardref.Repo
qslTemplates *qslcard.Repo
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
extsvc *extsvc.Manager
winkeyer *winkeyer.Manager
clublog *clublog.Manager
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
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
tunergenius *tunergenius.Client // Tuner Genius XL (4O3A) ATU (TCP); nil when disabled
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
spe *spe.Client // legacy pointer: FIRST enabled SPE amp (kept for the pre-multi bindings)
acom *acom.Client // legacy pointer: FIRST enabled ACOM amp
ampsMu sync.Mutex // guards ampInsts
ampInsts map[string]*ampInst // one running client per enabled configured amplifier, by config ID
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
// Self-spot throttle: when and on what frequency we last announced ourselves.
// Held in memory only — a restart legitimately re-announces the station.
selfSpotMu sync.Mutex
selfSpotAt time.Time
selfSpotHz int64
pota *pota.Cache
uls *uls.Store // US callsign→county/grid (offline FCC ULS), lazily opened
awardRefs *awardref.Repo
qslTemplates *qslcard.Repo
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
extsvc *extsvc.Manager
winkeyer *winkeyer.Manager
clublog *clublog.Manager
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
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
tunergenius *tunergenius.Client // Tuner Genius XL (4O3A) ATU (TCP); nil when disabled
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
spe *spe.Client // legacy pointer: FIRST enabled SPE amp (kept for the pre-multi bindings)
acom *acom.Client // legacy pointer: FIRST enabled ACOM amp
ampsMu sync.Mutex // guards ampInsts
ampInsts map[string]*ampInst // one running client per enabled configured amplifier, by config ID
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
// qsoRecManual marks a take the operator started by hand while automatic
// recording is OFF. Such a take opened the sound devices itself, so it must
// close them again when it ends — an operator who records one contact does
@@ -2608,6 +2615,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
a.extsvc.OnQSOLogged(id)
}
a.maybeAutoSendEQSL(qc)
a.maybeSelfSpot(qc)
if a.udp != nil {
a.udp.EmitLoggedADIF(adif.SingleRecordADIF(qc))
}
@@ -11584,6 +11592,7 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
a.extsvc.OnQSOLogged(id)
}
a.maybeAutoSendEQSL(qc)
a.maybeSelfSpot(qc)
// Forward to the outbound UDP integrations, exactly like the manual log
// path — otherwise a QSO logged FROM WSJT-X/JTDX/MSHV was never re-emitted
// to the outbound ADIF listeners (Log4OM, N1MM, gridtracker…).
@@ -15990,6 +15999,128 @@ func (a *App) GetClusterAutoConnect() (bool, error) {
return a.clusterAutoConnect()
}
// ── Self-spot ─────────────────────────────────────────────────────────────
// selfSpotMinMinutes is the shortest gap OpsLog will announce itself at.
//
// A self-spot is traffic every user of the node sees, so the floor is a
// courtesy rule, not a preference: at one spot per QSO a run would flood the
// cluster within minutes and get the station filtered out — which is worse for
// the operator than not spotting at all. Five minutes is the interval the
// DX-cluster etiquette guides give.
const selfSpotMinMinutes = 5
// selfSpotSameRunHz is how far the frequency may drift and still count as the
// same run. A run creeps a few hundred Hz as the operator nudges the VFO, and
// re-announcing on every nudge is exactly the flood the gap exists to prevent.
// Move further than this and it IS a new frequency, which is announced at once.
const selfSpotSameRunHz = 500
// SelfSpotSettings configures announcing the station on the DX cluster as it
// logs, so callers can find it without anyone else having to spot it.
type SelfSpotSettings struct {
Enabled bool `json:"enabled"`
Minutes int `json:"minutes"` // shortest gap between two self-spots
}
// GetSelfSpotSettings reads the self-spot configuration.
func (a *App) GetSelfSpotSettings() (SelfSpotSettings, error) {
out := SelfSpotSettings{Minutes: selfSpotMinMinutes}
if a.settings == nil {
return out, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyClusterSelfSpot, keyClusterSelfSpotMin)
if err != nil {
return out, err
}
out.Enabled = m[keyClusterSelfSpot] == "1"
if n, _ := strconv.Atoi(m[keyClusterSelfSpotMin]); n >= selfSpotMinMinutes {
out.Minutes = n
}
return out, nil
}
// SaveSelfSpotSettings persists it. The minimum gap is clamped here as well as
// in the UI: the floor protects the node from us, so it must not depend on a
// frontend that could be bypassed or carry a stale value.
func (a *App) SaveSelfSpotSettings(s SelfSpotSettings) error {
if a.settings == nil {
return fmt.Errorf("db not initialized")
}
if s.Minutes < selfSpotMinMinutes {
s.Minutes = selfSpotMinMinutes
}
if err := a.settings.Set(a.ctx, keyClusterSelfSpot, boolStr(s.Enabled)); err != nil {
return err
}
return a.settings.Set(a.ctx, keyClusterSelfSpotMin, strconv.Itoa(s.Minutes))
}
// maybeSelfSpot announces the station on the master cluster after a QSO, so a
// run is findable without waiting for someone else to spot it.
//
// The DX call and the spotter are both us: the node fills the "DE" from the
// login, and the QSO's own station callsign is the DX — which is the right
// source, not the profile, because that is the call the contact was made under.
//
// Fires on the FIRST QSO of a frequency, then at most once per configured gap.
// Called from both log paths (manual entry and UDP auto-log), off the critical
// path — a cluster that is slow or down must never hold up logging.
func (a *App) maybeSelfSpot(q qso.QSO) {
cfg, err := a.GetSelfSpotSettings()
if err != nil || !cfg.Enabled {
return
}
call := strings.ToUpper(strings.TrimSpace(q.StationCallsign))
if call == "" {
return
}
// The QSO's frequency, not the rig's: by the time this runs the operator may
// already have tuned away, and a spot has to say where the contact happened.
var hz int64
if q.FreqHz != nil {
hz = *q.FreqHz
}
if hz <= 0 && a.cat != nil {
hz = a.cat.State().FreqHz // no frequency on the QSO (paper-style entry) — fall back to CAT
}
if hz <= 0 {
return
}
a.selfSpotMu.Lock()
sameRun := a.selfSpotHz != 0 && absInt64(hz-a.selfSpotHz) <= selfSpotSameRunHz
if sameRun && time.Since(a.selfSpotAt) < time.Duration(cfg.Minutes)*time.Minute {
a.selfSpotMu.Unlock()
return
}
prevAt, prevHz := a.selfSpotAt, a.selfSpotHz
// Claim the slot BEFORE sending: the two log paths can run concurrently and
// would otherwise both pass the check and spot twice.
a.selfSpotAt, a.selfSpotHz = time.Now(), hz
a.selfSpotMu.Unlock()
khz := math.Round(float64(hz)/100) / 10 // 0.1 kHz — the resolution a cluster keeps
if err := a.SendClusterSpot(call, khz, strings.ToUpper(strings.TrimSpace(q.Mode))); err != nil {
// Not sent (no enabled server, node not connected). Restore the previous
// state so the NEXT QSO tries again, instead of sitting out a gap that
// never produced a spot.
a.selfSpotMu.Lock()
a.selfSpotAt, a.selfSpotHz = prevAt, prevHz
a.selfSpotMu.Unlock()
applog.Printf("cluster: self-spot %s on %.1f kHz failed: %v", call, khz, err)
return
}
applog.Printf("cluster: self-spot %s on %.1f kHz sent", call, khz)
}
func absInt64(n int64) int64 {
if n < 0 {
return -n
}
return n
}
// ConnectClusterServer opens a session for one specific saved server.
func (a *App) ConnectClusterServer(id int64) error {
if a.cluster == nil {
+4 -2
View File
@@ -16,7 +16,8 @@
"Entry form and QSO editor: Name and QTH are capitalised word by word (\"JEAN-PIERRE\" → \"Jean-Pierre\"), Comment and Note get a capital first letter. Applied when you leave the field, so typing is never interrupted; the rest of a comment is left alone, since it usually holds callsigns and modes.",
"Recent QSOs: the search box only ever takes callsigns, so it now upper-cases as you type and carries a small clear button.",
"CAT Elecraft K3/K4: the rig's mode is read back correctly. MD6 is DATA on a K3 but FSK on a Kenwood, and OpsLog decoded it as RTTY — so a K3 running FT8 showed RTTY, logged its QSOs as RTTY, and told WSJT-X/JTDX over the shared CAT link that the rig was in RTTY while they had asked for a data mode. Applies to the Elecraft K3/K4 backend and to the \"DATA A — MD6\" option; a plain Kenwood still reads MD6 as RTTY.",
"CAT sharing: the transmitter is dropped when the program keying it goes away. If WSJT-X/JTDX crashed, was closed, or simply had its connection cut — which is what saving the settings does, since the sharing server is rebuilt — the rig stayed on air with nobody watching: the Kenwood/Elecraft link stops polling while PTT is held, so nothing noticed. A K3 was seen transmitting for 29 seconds."
"CAT sharing: the transmitter is dropped when the program keying it goes away. If WSJT-X/JTDX crashed, was closed, or simply had its connection cut — which is what saving the settings does, since the sharing server is rebuilt — the rig stayed on air with nobody watching: the Kenwood/Elecraft link stops polling while PTT is held, so nothing noticed. A K3 was seen transmitting for 29 seconds.",
"DX cluster: new \"Self-spot while I log\" option (Settings → DX Cluster). When you log a QSO, OpsLog announces YOU on the master cluster — your station callsign as the DX, on the frequency you just worked — so callers find you without waiting for someone else to spot you. It fires on the first QSO of a frequency, then no more often than the interval you set; five minutes is the minimum, since a self-spot is traffic every user of the node sees."
],
"fr": [
"Sauvegarde : nouvelle option \"Sauvegarder à chaque fermeture\" (Réglages → Sauvegarde). Activée, OpsLog sauvegarde la base à chaque fois que tu quittes au lieu d'une seule fois par jour — les QSO d'une seconde session sont ainsi toujours pris.",
@@ -32,7 +33,8 @@
"Saisie et éditeur de QSO : Name et QTH sont capitalisés mot par mot (« JEAN-PIERRE » → « Jean-Pierre »), Comment et Note prennent une majuscule en première lettre. Appliqué quand tu quittes le champ, donc la frappe n'est jamais perturbée ; la suite d'un commentaire est laissée telle quelle, puisqu'elle contient souvent des indicatifs et des modes.",
"QSO récents : le champ de recherche ne prend que des indicatifs — il passe donc en majuscules à la frappe et reçoit un petit bouton d'effacement.",
"CAT Elecraft K3/K4 : le mode de la radio est relu correctement. MD6 vaut DATA sur un K3 mais FSK sur un Kenwood, et OpsLog le décodait en RTTY — un K3 en FT8 affichait donc RTTY, enregistrait ses QSO en RTTY, et annonçait RTTY à WSJT-X/JTDX via le partage CAT alors qu'ils avaient demandé un mode data. Vaut pour le backend Elecraft K3/K4 et pour l'option « DATA A — MD6 » ; un Kenwood classique lit toujours MD6 comme RTTY.",
"Partage CAT : l'émission est coupée quand le logiciel qui la commandait disparaît. Si WSJT-X/JTDX plantait, était fermé, ou voyait simplement sa connexion coupée — ce que fait un enregistrement des réglages, puisque le serveur de partage est reconstruit — la radio restait en émission sans surveillance : la liaison Kenwood/Elecraft cesse d'interroger le poste tant que le PTT est tenu, donc rien ne s'en apercevait. Un K3 a été vu en émission pendant 29 secondes."
"Partage CAT : l'émission est coupée quand le logiciel qui la commandait disparaît. Si WSJT-X/JTDX plantait, était fermé, ou voyait simplement sa connexion coupée — ce que fait un enregistrement des réglages, puisque le serveur de partage est reconstruit — la radio restait en émission sans surveillance : la liaison Kenwood/Elecraft cesse d'interroger le poste tant que le PTT est tenu, donc rien ne s'en apercevait. Un K3 a été vu en émission pendant 29 secondes.",
"Cluster DX : nouvelle option « M'auto-spotter quand j'enregistre » (Réglages → DX Cluster). À chaque QSO enregistré, OpsLog t'annonce sur le cluster maître — ton indicatif de station en DX, sur la fréquence que tu viens de travailler — pour qu'on te trouve sans attendre que quelqu'un te spotte. Déclenché au premier QSO d'une fréquence, puis pas plus souvent que l'intervalle choisi ; cinq minutes est le minimum, un auto-spot étant du trafic que voient tous les utilisateurs du nœud."
]
},
{
+45 -2
View File
@@ -26,7 +26,7 @@ import {
AudioStartMonitor, AudioStopMonitor, AudioMonitorActive,
AudioStartTX, AudioStopTX, AudioTXActive,
ListClusterServers, SaveClusterServer, DeleteClusterServer,
GetClusterAutoConnect, SetClusterAutoConnect,
GetClusterAutoConnect, SetClusterAutoConnect, GetSelfSpotSettings, SaveSelfSpotSettings,
ConnectClusterServer, DisconnectClusterServer,
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
@@ -1524,19 +1524,28 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
const [clusterAutoConnect, setClusterAutoConnectState] = useState(false);
// Self-spot. SELF_SPOT_MIN_MIN mirrors the backend floor — the input clamps on
// blur, not per keystroke, or typing "10" would be rewritten to "5" the moment
// the "1" landed and the field would fight the operator.
const SELF_SPOT_MIN_MIN = 5;
const [selfSpot, setSelfSpot] = useState({ enabled: false, minutes: SELF_SPOT_MIN_MIN });
const [selfSpotText, setSelfSpotText] = useState(String(SELF_SPOT_MIN_MIN));
const [clusterStatuses, setClusterStatuses] = useState<ClusterServerStatus[]>([]);
const [editingServer, setEditingServer] = useState<ClusterServer | null>(null);
async function reloadClusterServers() {
try {
const [list, ac, st] = await Promise.all([
const [list, ac, st, ss] = await Promise.all([
ListClusterServers(),
GetClusterAutoConnect(),
GetClusterStatus(),
GetSelfSpotSettings(),
]);
setClusterServers((list ?? []) as ClusterServer[]);
setClusterAutoConnectState(ac);
setClusterStatuses((st ?? []) as ClusterServerStatus[]);
setSelfSpot(ss as any);
setSelfSpotText(String((ss as any).minutes ?? SELF_SPOT_MIN_MIN));
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
@@ -1849,6 +1858,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
await SaveQSLDefaults(qslDefaults as any);
await SaveExternalServices(extSvc as any);
await SetClusterAutoConnect(clusterAutoConnect);
await SaveSelfSpotSettings(selfSpot as any);
setMsg('Settings saved.');
onSaved();
@@ -4114,6 +4124,39 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
onCheckedChange={(c) => { const v = !!c; setClusterWorkedSameSlot(v); writeUiPref('opslog.clusterWorkedSameSlot', v ? '1' : '0'); }} />
<span>{t('clu.workedSameSlot')} <span className="text-xs text-muted-foreground">{t('clu.workedSameSlotHint')}</span></span>
</label>
{/* Self-spot. The interval only shows once it's on an interval for
something switched off is just a question the operator can't act on. */}
<div className="border-t border-border/60 pt-3 space-y-2">
<div className="flex items-center gap-3 flex-wrap">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={selfSpot.enabled}
onCheckedChange={(c) => setSelfSpot((s) => ({ ...s, enabled: !!c }))} />
{t('clu.selfSpot')}
</label>
{selfSpot.enabled && (
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">{t('clu.selfSpotEvery')}</span>
<Input
type="number"
min={SELF_SPOT_MIN_MIN}
max={240}
className="w-20 h-8 font-mono text-xs"
value={selfSpotText}
onChange={(e) => setSelfSpotText(e.target.value)}
onBlur={() => {
const n = Math.floor(Number(selfSpotText));
const v = Number.isFinite(n) && n > SELF_SPOT_MIN_MIN ? Math.min(n, 240) : SELF_SPOT_MIN_MIN;
setSelfSpotText(String(v));
setSelfSpot((s) => ({ ...s, minutes: v }));
}}
/>
<span className="text-muted-foreground">{t('clu.selfSpotMinutes')}</span>
</div>
)}
</div>
<p className="text-xs text-muted-foreground leading-relaxed">{t('clu.selfSpotHint')}</p>
</div>
</div>
{editingServer && (
+4
View File
@@ -262,6 +262,8 @@ const en: Dict = {
'clu.name': 'Name', 'clu.hostPort': 'Host:port', 'clu.status': 'Status', 'clu.actions': 'Actions',
'clu.moveUp': 'Move up', 'clu.moveDown': 'Move down', 'clu.edit': 'Edit', 'clu.delete': 'Delete', 'clu.none': 'No cluster nodes saved yet.', 'clu.connect': 'Connect', 'clu.disconnect': 'Disconnect',
'clu.add': 'Add cluster', 'clu.connectAll': 'Connect all', 'clu.disconnectAll': 'Disconnect all', 'clu.autoConnect': 'Auto-connect all enabled on app start',
'clu.selfSpot': 'Self-spot while I log', 'clu.selfSpotEvery': 'at most every', 'clu.selfSpotMinutes': 'min',
'clu.selfSpotHint': 'Announces YOU on the master cluster when you log a QSO — the spot carries your station callsign and the frequency you just worked on, so callers find you without waiting for someone else to spot you. Sent on the first QSO of a frequency, then no more often than the gap below. Five minutes is the floor: a self-spot is traffic every user of the node sees.',
'clu.freeNodes': 'Free public nodes:',
'clu.workedSameSlot': 'Already worked only on the same slot',
'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere. Combines with digital-mode grouping (Settings → General): with it on, a call worked on 20m FT8 also counts as worked for a 20m FT4 spot; with it off, FT8 and FT4 are separate slots.',
@@ -675,6 +677,8 @@ const fr: Dict = {
'clu.name': 'Nom', 'clu.hostPort': 'Hôte:port', 'clu.status': 'Statut', 'clu.actions': 'Actions',
'clu.moveUp': 'Monter', 'clu.moveDown': 'Descendre', 'clu.edit': 'Éditer', 'clu.delete': 'Supprimer', 'clu.none': 'Aucun nœud cluster enregistré.', 'clu.connect': 'Connecter', 'clu.disconnect': 'Déconnecter',
'clu.add': 'Ajouter cluster', 'clu.connectAll': 'Tout connecter', 'clu.disconnectAll': 'Tout déconnecter', 'clu.autoConnect': 'Connexion auto de tous les activés au démarrage',
'clu.selfSpot': "M'auto-spotter quand j'enregistre", 'clu.selfSpotEvery': 'au plus toutes les', 'clu.selfSpotMinutes': 'min',
'clu.selfSpotHint': "Annonce TON indicatif sur le cluster maître quand tu enregistres un QSO — le spot porte l'indicatif de station et la fréquence que tu viens de travailler, pour qu'on te trouve sans attendre que quelqu'un te spotte. Envoyé au premier QSO d'une fréquence, puis pas plus souvent que l'intervalle ci-dessous. Cinq minutes est le plancher : un auto-spot est du trafic que voient tous les utilisateurs du nœud.",
'clu.freeNodes': 'Nœuds publics gratuits :',
'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot',
'clu.workedSameSlotHint': '— un spot n\'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode, pas juste n\'importe où. Se combine avec le groupage des modes numériques (Réglages → Général) : activé, un indicatif contacté en 20m FT8 compte aussi comme contacté pour un spot 20m FT4 ; désactivé, FT8 et FT4 sont des slots distincts.',
+4
View File
@@ -481,6 +481,8 @@ export function GetScpStatus():Promise<main.ScpStatus>;
export function GetSecretStatus():Promise<main.SecretStatus>;
export function GetSelfSpotSettings():Promise<main.SelfSpotSettings>;
export function GetSlotStats():Promise<qso.SlotStats>;
export function GetSolarData():Promise<solar.Data>;
@@ -895,6 +897,8 @@ export function SaveRelayAuto(arg1:main.RelayAutoConfig):Promise<void>;
export function SaveRotators(arg1:Array<main.RotatorDevice>):Promise<void>;
export function SaveSelfSpotSettings(arg1:main.SelfSpotSettings):Promise<void>;
export function SaveStationDevices(arg1:Array<main.StationDevice>):Promise<void>;
export function SaveStationSettings(arg1:main.StationSettings):Promise<void>;
+8
View File
@@ -910,6 +910,10 @@ export function GetSecretStatus() {
return window['go']['main']['App']['GetSecretStatus']();
}
export function GetSelfSpotSettings() {
return window['go']['main']['App']['GetSelfSpotSettings']();
}
export function GetSlotStats() {
return window['go']['main']['App']['GetSlotStats']();
}
@@ -1738,6 +1742,10 @@ export function SaveRotators(arg1) {
return window['go']['main']['App']['SaveRotators'](arg1);
}
export function SaveSelfSpotSettings(arg1) {
return window['go']['main']['App']['SaveSelfSpotSettings'](arg1);
}
export function SaveStationDevices(arg1) {
return window['go']['main']['App']['SaveStationDevices'](arg1);
}
+14
View File
@@ -2914,6 +2914,20 @@ export namespace main {
this.unlocked = source["unlocked"];
}
}
export class SelfSpotSettings {
enabled: boolean;
minutes: number;
static createFrom(source: any = {}) {
return new SelfSpotSettings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.minutes = source["minutes"];
}
}
export class SpotQuery {
call: string;
band: string;