fix: release TX when the elements stop; WSJT-X colours, and worked beats the watch list

The motorised antenna gagged the transmitter for a second or two after it
had finished moving. Three delays were stacked: the antenna polled every
two seconds, the gag held for three after the command whatever the
antenna said, and the widget refreshed every three. The antenna is now
asked four times a second WHILE IT MOVES — an idle one has nothing to say
and stays at two seconds — the gag only bridges the command itself
(900 ms), and the widget follows at half a second while moving.

WSJT-X highlighting:

- A watch-list station already worked on this band and mode is no longer
  painted as one to call. The list is a statement of intent, not of what
  is left to do, and its pink outranked every other verdict including the
  log's, so a worked station stayed pink for the session with nothing to
  tell it from one still needed.
- The four colours are the operator's to choose (Settings → UDP). Only
  the background: the text colour is derived by luma, so a dark blue
  cannot come back as black-on-black in somebody else's window. Changing
  one clears the installed highlights, or the de-duplication would keep
  showing yesterday's colour until a callsign changed verdict.
This commit is contained in:
2026-09-06 18:43:08 +02:00
parent 07ee48e20c
commit d001616767
11 changed files with 306 additions and 31 deletions
+7 -1
View File
@@ -18342,7 +18342,13 @@ func (a *App) applyMotorInhibit(on bool) {
// OR within a grace window after a commanded move — and always releases the // 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). // inhibit when it stops (so a settings change / shutdown never leaves TX blocked).
func (a *App) motorTXInhibitLoop(c motorAntenna, bands []string, stop <-chan struct{}) { func (a *App) motorTXInhibitLoop(c motorAntenna, bands []string, stop <-chan struct{}) {
const grace = 3 * time.Second // cover the poll latency at the very start of a move // Just long enough to bridge ONE fast poll at the start of a move, when the
// antenna has been told to go but has not yet said it is going. It was three
// seconds, from when the status poll ran every two: a move that took a second
// left the transmitter gagged for two more, with the elements already in
// place. The antenna is now polled four times a second while it travels, so
// this only has to cover the command itself.
const grace = 900 * time.Millisecond
ticker := time.NewTicker(300 * time.Millisecond) ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
defer a.applyMotorInhibit(false) // never leave TX blocked when the loop ends defer a.applyMotorInhibit(false) // never leave TX blocked when the loop ends
+115 -23
View File
@@ -7,6 +7,8 @@ package main
// the spot grid, so the two windows can never disagree. // the spot grid, so the two windows can never disagree.
import ( import (
"fmt"
"strconv"
"strings" "strings"
"hamlog/internal/applog" "hamlog/internal/applog"
@@ -18,6 +20,13 @@ const (
keyWsjtHighlight = "udp.wsjt.highlight" keyWsjtHighlight = "udp.wsjt.highlight"
keyWsjtFollowMode = "udp.wsjt.followmode" // spot clicks switch the decoder's mode keyWsjtFollowMode = "udp.wsjt.followmode" // spot clicks switch the decoder's mode
keyWsjtHLWorked = "udp.wsjt.highlight_worked" keyWsjtHLWorked = "udp.wsjt.highlight_worked"
// One key per verdict, holding "#RRGGBB". Only the BACKGROUND is stored: the
// text colour is computed from it, so a chosen colour can never come out
// unreadable in somebody else's window.
keyWsjtColWatchlist = "udp.wsjt.colour.watchlist"
keyWsjtColNewDXCC = "udp.wsjt.colour.new_dxcc"
keyWsjtColNewBand = "udp.wsjt.colour.new_band"
keyWsjtColWorked = "udp.wsjt.colour.worked"
) )
// wsjtModes are the modes a Configure message can meaningfully ask for — the // wsjtModes are the modes a Configure message can meaningfully ask for — the
@@ -53,8 +62,9 @@ func (a *App) ConfigureDecoderMode(mode string) {
a.udp.SendConfigureMode(mode) a.udp.SendConfigureMode(mode)
} }
// The palette. Fixed colours, not theme tokens — they are painted into another // The DEFAULT palette. Fixed colours, not theme tokens — they are painted into
// application's window, which has no idea what theme OpsLog wears. // another application's window, which has no idea what theme OpsLog wears, and
// the operator can change each of them (see WsjtHighlightColours).
var ( var (
hlWatchlist = udp.RGB{R: 244, G: 114, B: 182} // the watchlist pink hlWatchlist = udp.RGB{R: 244, G: 114, B: 182} // the watchlist pink
hlNewDXCC = udp.RGB{R: 22, G: 130, B: 60} // green hlNewDXCC = udp.RGB{R: 22, G: 130, B: 60} // green
@@ -65,9 +75,81 @@ var (
// only DIM colour of the four: the others say "look at this", and this one // only DIM colour of the four: the others say "look at this", and this one
// says the opposite — it has to recede, not compete with them. // says the opposite — it has to recede, not compete with them.
hlWorked = udp.RGB{R: 75, G: 85, B: 99} hlWorked = udp.RGB{R: 75, G: 85, B: 99}
hlWorkedFg = udp.RGB{R: 203, G: 213, B: 225}
) )
// WsjtHighlightColours is the operator's palette, one background per verdict.
type WsjtHighlightColours struct {
Watchlist string `json:"watchlist"`
NewDXCC string `json:"new_dxcc"`
NewBand string `json:"new_band"`
Worked string `json:"worked"`
}
// GetWsjtHighlightColours returns the palette in "#RRGGBB", defaults included.
func (a *App) GetWsjtHighlightColours() WsjtHighlightColours {
return WsjtHighlightColours{
Watchlist: a.settingOr(keyWsjtColWatchlist, hexOfRGB(hlWatchlist)),
NewDXCC: a.settingOr(keyWsjtColNewDXCC, hexOfRGB(hlNewDXCC)),
NewBand: a.settingOr(keyWsjtColNewBand, hexOfRGB(hlNewBand)),
Worked: a.settingOr(keyWsjtColWorked, hexOfRGB(hlWorked)),
}
}
// SetWsjtHighlightColours stores the palette and repaints.
//
// The repaint is the whole point of clearing: the de-duplication remembers what
// it has already told each decoder, so without this a callsign keeps yesterday's
// colour until it changes verdict — and the operator, having just picked a new
// one, sees nothing happen.
func (a *App) SetWsjtHighlightColours(c WsjtHighlightColours) {
set := func(key, v, def string) {
if _, ok := parseHexRGB(v); !ok {
v = def
}
a.setSetting(key, strings.ToUpper(strings.TrimSpace(v)))
}
set(keyWsjtColWatchlist, c.Watchlist, hexOfRGB(hlWatchlist))
set(keyWsjtColNewDXCC, c.NewDXCC, hexOfRGB(hlNewDXCC))
set(keyWsjtColNewBand, c.NewBand, hexOfRGB(hlNewBand))
set(keyWsjtColWorked, c.Worked, hexOfRGB(hlWorked))
a.clearWsjtHighlights()
applog.Printf("wsjt highlight: palette changed — repainting")
}
// colourFor reads one verdict's background and picks a legible foreground.
//
// The text colour is DERIVED, never stored: an operator choosing a dark blue
// would otherwise get black text on it in somebody else's window and conclude
// the feature is broken. Rec. 601 luma, the same rule a browser's contrast
// checker uses, with the threshold where black stops being readable.
func (a *App) colourFor(key, def string) (udp.RGB, udp.RGB) {
bg, ok := parseHexRGB(a.settingOr(key, def))
if !ok {
bg, _ = parseHexRGB(def)
}
luma := (299*int(bg.R) + 587*int(bg.G) + 114*int(bg.B)) / 1000
if luma < 140 {
return bg, hlWhite
}
return bg, hlBlack
}
func hexOfRGB(c udp.RGB) string { return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B) }
// parseHexRGB reads "#RRGGBB" (or "RRGGBB"). Anything else is refused rather
// than half-read: a colour that silently becomes black is worse than a default.
func parseHexRGB(s string) (udp.RGB, bool) {
s = strings.TrimPrefix(strings.TrimSpace(s), "#")
if len(s) != 6 {
return udp.RGB{}, false
}
v, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return udp.RGB{}, false
}
return udp.RGB{R: byte(v >> 16), G: byte(v >> 8), B: byte(v)}, true
}
// GetWsjtHighlightWorked reports whether stations already worked on this band // GetWsjtHighlightWorked reports whether stations already worked on this band
// and mode are greyed out as well. // and mode are greyed out as well.
// //
@@ -174,42 +256,52 @@ func (a *App) maybeHighlightDecode(instance, call, band, mode string) {
// Anything else is "no colour", and the empty verdict doubles as the clear // Anything else is "no colour", and the empty verdict doubles as the clear
// signal in maybeHighlightDecode. // signal in maybeHighlightDecode.
func (a *App) decodeHighlightVerdict(call, band, mode string) (bg, fg *udp.RGB, verdict string) { func (a *App) decodeHighlightVerdict(call, band, mode string) (bg, fg *udp.RGB, verdict string) {
if a.watchlist != nil {
if _, ok := a.watchlist.Match(call); ok {
c := hlWatchlist
f := hlBlack
return &c, &f, "watchlist"
}
}
c := a.clusterStatusMaps() c := a.clusterStatusMaps()
// ALREADY WORKED HERE, whatever else the station is.
//
// Settled first because it is the one fact that cancels the others. A watch
// list entry worked on this band and mode stayed pink for the rest of the
// session — the list is a statement of intent, not of what is left to do, and
// the colour that means "call this one" was being shown for a station already
// in the log. From the operator's side there was no way to tell the two
// apart, which is the only thing the colours are for.
workedHere := false
if band != "" && mode != "" && c.workedCallSlots != nil {
m := strings.ToUpper(strings.TrimSpace(mode))
if c.normMode != nil {
m = c.normMode(m)
}
_, workedHere = c.workedCallSlots[strings.ToUpper(call)+"|"+strings.ToLower(band)+"|"+m]
}
if a.watchlist != nil && !workedHere {
if _, ok := a.watchlist.Match(call); ok {
bgc, fgc := a.colourFor(keyWsjtColWatchlist, hexOfRGB(hlWatchlist))
return &bgc, &fgc, "watchlist"
}
}
if a.dxcc != nil { if a.dxcc != nil {
if m, ok := a.dxcc.Lookup(call); ok && m.Entity != nil { if m, ok := a.dxcc.Lookup(call); ok && m.Entity != nil {
num := dxcc.EntityDXCC(m.Entity.Name) num := dxcc.EntityDXCC(m.Entity.Name)
ent := c.entities[num] ent := c.entities[num]
if ent == nil { if ent == nil {
bgc, fgc := hlNewDXCC, hlWhite bgc, fgc := a.colourFor(keyWsjtColNewDXCC, hexOfRGB(hlNewDXCC))
return &bgc, &fgc, "new-dxcc" return &bgc, &fgc, "new-dxcc"
} }
if band != "" { if band != "" {
if _, workedBand := ent.Bands[strings.ToLower(band)]; !workedBand { if _, workedBand := ent.Bands[strings.ToLower(band)]; !workedBand {
bgc, fgc := hlNewBand, hlBlack bgc, fgc := a.colourFor(keyWsjtColNewBand, hexOfRGB(hlNewBand))
return &bgc, &fgc, "new-band" return &bgc, &fgc, "new-band"
} }
} }
} }
} }
// Worked already, this exact callsign on this band in this mode — a dupe, // A dupe, judged by the same ledger and the same digital-mode grouping the
// judged by the same ledger and the same digital-mode grouping the cluster // cluster uses, so the two windows cannot disagree about what "worked" means.
// uses, so the two windows cannot disagree about what "worked" means. // Its own option: on a well-filled log this matches most of a period, and a
if a.wsjtHLWorkedOn.Load() && band != "" && mode != "" { // screen where nearly every line is coloured has stopped saying anything.
m := strings.ToUpper(strings.TrimSpace(mode)) if workedHere && a.wsjtHLWorkedOn.Load() {
if c.normMode != nil { bgc, fgc := a.colourFor(keyWsjtColWorked, hexOfRGB(hlWorked))
m = c.normMode(m)
}
if _, ok := c.workedCallSlots[strings.ToUpper(call)+"|"+strings.ToLower(band)+"|"+m]; ok {
bgc, fgc := hlWorked, hlWorkedFg
return &bgc, &fgc, "worked" return &bgc, &fgc, "worked"
} }
}
return nil, nil, "" return nil, nil, ""
} }
+8 -2
View File
@@ -11,7 +11,10 @@
"Club Log uploads are no longer refused as “not configured”. The check added for services with no credentials demanded a Club Log API key, which nobody has ever set — OpsLog carries its own application key — so an operator whose live upload had worked for months was turned away when sending QSOs by hand. Each services requirements now live beside the uploader that enforces them, and the message names the fields that are actually missing.", "Club Log uploads are no longer refused as “not configured”. The check added for services with no credentials demanded a Club Log API key, which nobody has ever set — OpsLog carries its own application key — so an operator whose live upload had worked for months was turned away when sending QSOs by hand. Each services requirements now live beside the uploader that enforces them, and the message names the fields that are actually missing.",
"Audio: the Listening device now says when it cannot be opened. A device unplugged, renamed by Windows or unable to run at 16 kHz failed silently while everything upstream reported success — the stream up, the packets arriving, the monitor started — which is the whole of “I turned the sound on and nothing comes out”. The log also says, once, whether the network RX audio is reaching the speakers or arriving with nobody listening.", "Audio: the Listening device now says when it cannot be opened. A device unplugged, renamed by Windows or unable to run at 16 kHz failed silently while everything upstream reported success — the stream up, the packets arriving, the monitor started — which is the whole of “I turned the sound on and nothing comes out”. The log also says, once, whether the network RX audio is reaching the speakers or arriving with nobody listening.",
"Icom network audio starts at once instead of half a minute later. The message that authorises the stream is sent during the login, before the audio socket exists — so the rig was told to send audio to a port nothing was bound to, got a port-unreachable back, and only resumed when its own retry timer came round. It is sent once more as soon as the port is listening.", "Icom network audio starts at once instead of half a minute later. The message that authorises the stream is sent during the login, before the audio socket exists — so the rig was told to send audio to a port nothing was bound to, got a port-unreachable back, and only resumed when its own retry timer came round. It is sent once more as soon as the port is listening.",
"Rotor dial: a circular scale and a beam instead of an arrow. The square ring made a marker at 45° sit further from the centre than one at north — a dial is read by angle, so the ring it is read against is now the same distance away all the way round. The antenna is drawn as a sector that fades outwards, which is the shape of the thing it stands for; where the mouse would send it appears in the same shape in orange, and its azimuth in place of the current heading while you aim. Green for where the antenna is, orange for where it would go, yellow for what was ordered — the second lobe of a bidirectional Ultrabeam and the dashed boom are unchanged. Design from EC1KD again." "Rotor dial: a circular scale and a beam instead of an arrow. The square ring made a marker at 45° sit further from the centre than one at north — a dial is read by angle, so the ring it is read against is now the same distance away all the way round. The antenna is drawn as a sector that fades outwards, which is the shape of the thing it stands for; where the mouse would send it appears in the same shape in orange, and its azimuth in place of the current heading while you aim. Green for where the antenna is, orange for where it would go, yellow for what was ordered — the second lobe of a bidirectional Ultrabeam and the dashed boom are unchanged. Design from EC1KD again.",
"Motorised antenna: the transmitter is released as soon as the elements stop. Three delays were stacked between the antenna finishing and the operator being allowed to call — the antenna polled every two seconds, the transmit gag held for three after the command whatever the antenna said, and the screen refreshed every three. The antenna is now polled four times a second WHILE IT MOVES (and left at two seconds when it is still, where nothing changes), the gag only bridges the command itself, and the widget follows at half a second.",
"[NEW] The WSJT-X / JTDX highlight colours are yours to choose (Settings → UDP), one per verdict — watch list, new DXCC, new band, worked. Only the background is set: the text colour is worked out from it, so a chosen colour cannot come back unreadable in the decoders window.",
"WSJT-X highlighting: a watch-list station already worked on this band and mode is no longer painted as one to call. The list is a statement of intent, not of what is left to do, and its pink outranked everything — including the log — so a station already worked stayed pink for the session with no way to tell it from one still needed."
], ],
"fr": [ "fr": [
"Carte FTx : lindicatif, le locator et le report réapparaissent au survol. Le cercle invisible qui capte les clics est au-dessus du point, donc il capte aussi le survol — et l’étiquette n’était liée quau point du dessous, ce qui rendait la carte muette dès que les stations sont devenues cliquables.", "Carte FTx : lindicatif, le locator et le report réapparaissent au survol. Le cercle invisible qui capte les clics est au-dessus du point, donc il capte aussi le survol — et l’étiquette n’était liée quau point du dessous, ce qui rendait la carte muette dès que les stations sont devenues cliquables.",
@@ -22,7 +25,10 @@
"Les envois vers Club Log ne sont plus refusés comme « non configuré ». Le contrôle ajouté pour les services sans identifiants réclamait une clé API Club Log que personne na jamais saisie — OpsLog embarque la sienne — et un opérateur dont lenvoi automatique fonctionnait depuis des mois se voyait éconduit au moment denvoyer des QSO à la main. Les exigences de chaque service vivent désormais à côté du code qui les applique, et le message nomme les champs réellement manquants.", "Les envois vers Club Log ne sont plus refusés comme « non configuré ». Le contrôle ajouté pour les services sans identifiants réclamait une clé API Club Log que personne na jamais saisie — OpsLog embarque la sienne — et un opérateur dont lenvoi automatique fonctionnait depuis des mois se voyait éconduit au moment denvoyer des QSO à la main. Les exigences de chaque service vivent désormais à côté du code qui les applique, et le message nomme les champs réellement manquants.",
"Audio : le périphérique d’écoute signale désormais quand il ne peut pas souvrir. Un périphérique débranché, renommé par Windows ou incapable de fonctionner en 16 kHz échouait en silence pendant que tout en amont annonçait le succès — flux ouvert, paquets reçus, moniteur démarré — ce qui est exactement le « jai remis le son et rien ne sort ». Le journal dit aussi, une fois, si laudio réseau atteint les haut-parleurs ou arrive sans que personne n’écoute.", "Audio : le périphérique d’écoute signale désormais quand il ne peut pas souvrir. Un périphérique débranché, renommé par Windows ou incapable de fonctionner en 16 kHz échouait en silence pendant que tout en amont annonçait le succès — flux ouvert, paquets reçus, moniteur démarré — ce qui est exactement le « jai remis le son et rien ne sort ». Le journal dit aussi, une fois, si laudio réseau atteint les haut-parleurs ou arrive sans que personne n’écoute.",
"Laudio réseau Icom démarre tout de suite au lieu dune demi-minute plus tard. Le message qui autorise le flux part pendant la connexion, avant que la prise audio nexiste : le poste se voyait donc demander d’émettre vers un port où personne n’écoutait, recevait un « port injoignable » en retour, et ne reprenait quau tour suivant de son propre minuteur. Il est renvoyé dès que le port écoute.", "Laudio réseau Icom démarre tout de suite au lieu dune demi-minute plus tard. Le message qui autorise le flux part pendant la connexion, avant que la prise audio nexiste : le poste se voyait donc demander d’émettre vers un port où personne n’écoutait, recevait un « port injoignable » en retour, et ne reprenait quau tour suivant de son propre minuteur. Il est renvoyé dès que le port écoute.",
"Cadran rotor : échelle circulaire et faisceau au lieu dune flèche. Lanneau carré plaçait un repère à 45° plus loin du centre quun repère au nord — un cadran se lit par langle, donc lanneau qui sert de référence est désormais à la même distance tout autour. Lantenne est dessinée comme un secteur qui sestompe vers lextérieur, ce qui est la forme de ce quil représente ; là où la souris lenverrait apparaît dans la même forme en orange, et son azimut à la place du cap courant pendant quon vise. Vert pour où lantenne est, orange pour où elle irait, jaune pour ce qui a été demandé — le deuxième lobe dun Ultrabeam bidirectionnel et le boom en pointillés sont inchangés. Design dEC1KD, encore." "Cadran rotor : échelle circulaire et faisceau au lieu dune flèche. Lanneau carré plaçait un repère à 45° plus loin du centre quun repère au nord — un cadran se lit par langle, donc lanneau qui sert de référence est désormais à la même distance tout autour. Lantenne est dessinée comme un secteur qui sestompe vers lextérieur, ce qui est la forme de ce quil représente ; là où la souris lenverrait apparaît dans la même forme en orange, et son azimut à la place du cap courant pendant quon vise. Vert pour où lantenne est, orange pour où elle irait, jaune pour ce qui a été demandé — le deuxième lobe dun Ultrabeam bidirectionnel et le boom en pointillés sont inchangés. Design dEC1KD, encore.",
"Antenne motorisée : l’émission est rendue dès que les éléments sarrêtent. Trois délais sajoutaient entre la fin du mouvement et le droit dappeler — lantenne interrogée toutes les deux secondes, le blocage d’émission maintenu trois secondes après la commande quoi quen dise lantenne, et l’écran rafraîchi toutes les trois. Lantenne est désormais interrogée quatre fois par seconde PENDANT quelle bouge (et laissée à deux secondes à larrêt, où rien ne change), le blocage ne couvre plus que la commande elle-même, et le widget suit à la demi-seconde.",
"[NEW] Les couleurs de mise en évidence WSJT-X / JTDX sont au choix (Réglages → UDP), une par verdict — watchlist, nouveau DXCC, nouvelle bande, contactée. Seul le fond se règle : la couleur du texte en est déduite, pour quune couleur choisie ne revienne jamais illisible dans la fenêtre du décodeur.",
"Mise en évidence WSJT-X : une station de la watchlist déjà contactée sur cette bande et ce mode nest plus peinte comme une station à appeler. La liste dit une intention, pas ce quil reste à faire, et son rose passait devant tout — y compris le carnet — si bien quune station déjà faite restait rose toute la session, sans moyen de la distinguer dune station encore à faire."
] ]
}, },
{ {
+9 -2
View File
@@ -3100,15 +3100,22 @@ export default function App() {
const pokeUbStatus = useCallback(async () => { const pokeUbStatus = useCallback(async () => {
try { const s: any = await GetUltrabeamStatus(); if (s) setUbStatus(s); } catch { /* transient */ } try { const s: any = await GetUltrabeamStatus(); if (s) setUbStatus(s); } catch { /* transient */ }
}, []); }, []);
// Three seconds while it sits still, half a second while it moves.
//
// The moving flag gags the transmitter, and this poll is the last of three
// delays between the elements stopping and the operator being allowed to call
// again — the antenna's own poll and the inhibit loop are the other two. At
// three seconds it was the largest of them: the antenna had finished, the
// radio was free, and the screen still said orange.
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;
const tick = async () => { const tick = async () => {
try { const s: any = await GetUltrabeamStatus(); if (alive) setUbStatus(s); } catch {} try { const s: any = await GetUltrabeamStatus(); if (alive) setUbStatus(s); } catch {}
}; };
tick(); tick();
const id = window.setInterval(tick, 3000); const id = window.setInterval(tick, ubStatus.moving ? 500 : 3000);
return () => { alive = false; window.clearInterval(id); }; return () => { alive = false; window.clearInterval(id); };
}, []); }, [ubStatus.moving]);
// Poll the Antenna Genius switch for active antenna per port + the list. // Poll the Antenna Genius switch for active antenna per port + the list.
// Re-read the enabled flag each tick so toggling it in Settings makes the // Re-read the enabled flag each tick so toggling it in Settings makes the
@@ -3,6 +3,7 @@ import { Plus, Trash2, Edit2, RefreshCcw, ArrowDownToLine, ArrowUpFromLine } fro
import { import {
ListUDPIntegrations, SaveUDPIntegration, DeleteUDPIntegration, ReloadUDPIntegrations, ListUDPIntegrations, SaveUDPIntegration, DeleteUDPIntegration, ReloadUDPIntegrations,
GetWsjtHighlight, SetWsjtHighlight, GetWsjtHighlightWorked, SetWsjtHighlightWorked, GetWsjtFollowMode, SetWsjtFollowMode, GetWsjtHighlight, SetWsjtHighlight, GetWsjtHighlightWorked, SetWsjtHighlightWorked, GetWsjtFollowMode, SetWsjtFollowMode,
GetWsjtHighlightColours, SetWsjtHighlightColours,
} from '../../wailsjs/go/main/App'; } from '../../wailsjs/go/main/App';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -161,9 +162,13 @@ type Props = { onError: (msg: string) => void };
export function UDPIntegrationsPanel({ onError }: Props) { export function UDPIntegrationsPanel({ onError }: Props) {
const [highlightOn, setHighlightOn] = useState(false); const [highlightOn, setHighlightOn] = useState(false);
const [hlWorked, setHlWorked] = useState(false); const [hlWorked, setHlWorked] = useState(false);
// The palette, as chosen. Background per verdict; the text colour is the
// backends business (see colourFor).
const [colours, setColours] = useState({ watchlist: '#F472B6', new_dxcc: '#16823C', new_band: '#E27A18', worked: '#4B5563' });
const [followMode, setFollowMode] = useState(true); const [followMode, setFollowMode] = useState(true);
useEffect(() => { useEffect(() => {
GetWsjtHighlight().then((v) => setHighlightOn(!!v)).catch(() => {}); GetWsjtHighlight().then((v) => setHighlightOn(!!v)).catch(() => {});
GetWsjtHighlightColours().then((c: any) => { if (c) setColours(c); }).catch(() => {});
GetWsjtHighlightWorked().then((v) => setHlWorked(!!v)).catch(() => {}); GetWsjtHighlightWorked().then((v) => setHlWorked(!!v)).catch(() => {});
GetWsjtHighlightWorked().then((v) => setHlWorked(!!v)).catch(() => {}); GetWsjtHighlightWorked().then((v) => setHlWorked(!!v)).catch(() => {});
GetWsjtFollowMode().then((v) => setFollowMode(!!v)).catch(() => {}); GetWsjtFollowMode().then((v) => setFollowMode(!!v)).catch(() => {});
@@ -249,6 +254,45 @@ export function UDPIntegrationsPanel({ onError }: Props) {
<span className="block text-[11px] text-muted-foreground">{t('udpp.highlightHint')}</span> <span className="block text-[11px] text-muted-foreground">{t('udpp.highlightHint')}</span>
</span> </span>
</label> </label>
{/* The palette, nested under the switch for the same reason as the box
below it. One colour per verdict, and the background only: the text
colour is worked out from it, so a chosen colour cannot come back
unreadable in the decoder's window. */}
{highlightOn && (
<div className="pl-6 max-w-2xl space-y-1.5">
<div className="text-[11px] text-muted-foreground">{t('udpp.hlColours')}</div>
<div className="flex flex-wrap gap-3">
{([
['watchlist', t('udpp.hlWatchlist')],
['new_dxcc', t('udpp.hlNewDxcc')],
['new_band', t('udpp.hlNewBand')],
['worked', t('udpp.hlWorkedC')],
] as const).map(([k, label]) => (
<label key={k} className="inline-flex items-center gap-1.5 text-xs">
<input
type="color"
value={(colours as any)[k] || '#000000'}
onChange={(e) => {
const next = { ...colours, [k]: e.target.value.toUpperCase() };
setColours(next);
void SetWsjtHighlightColours(next as any);
}}
className="h-6 w-8 rounded border border-border bg-background p-0.5 cursor-pointer"
/>
{label}
</label>
))}
<button type="button" className="text-xs text-muted-foreground underline hover:text-foreground"
onClick={() => {
const def = { watchlist: '#F472B6', new_dxcc: '#16823C', new_band: '#E27A18', worked: '#4B5563' };
setColours(def as any);
void SetWsjtHighlightColours(def as any);
}}>
{t('udpp.hlReset')}
</button>
</div>
</div>
)}
{/* Nested under the switch above: the same feature, and meaningless {/* Nested under the switch above: the same feature, and meaningless
while that one is off. */} while that one is off. */}
{highlightOn && ( {highlightOn && (
+2 -2
View File
@@ -508,7 +508,7 @@ const en: Dict = {
'ncp.newNetPrompt': 'New NET name:', 'ncp.renamePrompt': 'Rename NET:', 'ncp.deleteConfirm': 'Delete NET "{name}" and its roster? This cannot be undone.', 'ncp.closeConfirm': "{n} station(s) still on the air will be dropped WITHOUT logging. Close anyway?", 'ncp.removeConfirm': "Remove {n} station(s) from this NET's roster?", 'ncp.colCallsign': 'Callsign', 'ncp.colName': 'Name', 'ncp.colTimeOn': 'Time on', 'ncp.colBand': 'Band', 'ncp.colMode': 'Mode', 'ncp.colComment': 'Comment', 'ncp.colCountry': 'Country', 'ncp.colDate': 'Date', 'ncp.workedBefore': 'Worked before', 'ncp.wbHint': 'Click a station (on air or roster) to see prior QSOs', 'ncp.wbNone': 'No prior QSO with', 'ncp.wbFirst': 'first', 'ncp.wbLast': 'last', 'ncp.wbResize': 'Drag to resize', 'ncp.newNet': 'New NET', 'ncp.closeToSwitch': 'Close the NET to switch', 'ncp.selectNetTitle': 'Select a NET', 'ncp.selectNetOption': '— select a NET —', 'ncp.closeNet': 'Close NET', 'ncp.openNet': 'Open NET', 'ncp.rename': 'Rename', 'ncp.delete': 'Delete', 'ncp.netOpenBadge': 'NET OPEN', 'ncp.onAir': 'On air:', 'ncp.roster': 'Roster:', 'ncp.onAirActive': 'On air — active QSOs', 'ncp.activeHint': 'mic-pass order · ⬆⬇ to reorder · double-click → edit · "Log & end" to save', 'ncp.moveUp': 'Move up the mic-pass order', 'ncp.moveDown': 'Move down the mic-pass order', 'ncp.logEndSelected': 'Log & end selected', 'ncp.logAll': 'Log everyone ({n})', 'ncp.logAllConfirm': 'Log all {n} on-air station(s) to the logbook?', 'ncp.netUsersRoster': 'NET users — roster', 'ncp.rosterHint': 'double-click → put on air', 'ncp.addContact': 'Add contact', 'ncp.remove': 'Remove', 'ncp.putOnAir': 'Put selected on air', 'ncp.addContactTitle': 'Add contact to NET', 'ncp.addContactDesc': "Saved in this NET's roster (reused next time you open it).", 'ncp.callsign': 'Callsign', 'ncp.search': 'Search', 'ncp.name': 'Name', 'ncp.country': 'Country', 'ncp.cancel': 'Cancel', 'ncp.saveInNet': 'Save in NET', 'ncp.newNetPrompt': 'New NET name:', 'ncp.renamePrompt': 'Rename NET:', 'ncp.deleteConfirm': 'Delete NET "{name}" and its roster? This cannot be undone.', 'ncp.closeConfirm': "{n} station(s) still on the air will be dropped WITHOUT logging. Close anyway?", 'ncp.removeConfirm': "Remove {n} station(s) from this NET's roster?", 'ncp.colCallsign': 'Callsign', 'ncp.colName': 'Name', 'ncp.colTimeOn': 'Time on', 'ncp.colBand': 'Band', 'ncp.colMode': 'Mode', 'ncp.colComment': 'Comment', 'ncp.colCountry': 'Country', 'ncp.colDate': 'Date', 'ncp.workedBefore': 'Worked before', 'ncp.wbHint': 'Click a station (on air or roster) to see prior QSOs', 'ncp.wbNone': 'No prior QSO with', 'ncp.wbFirst': 'first', 'ncp.wbLast': 'last', 'ncp.wbResize': 'Drag to resize', 'ncp.newNet': 'New NET', 'ncp.closeToSwitch': 'Close the NET to switch', 'ncp.selectNetTitle': 'Select a NET', 'ncp.selectNetOption': '— select a NET —', 'ncp.closeNet': 'Close NET', 'ncp.openNet': 'Open NET', 'ncp.rename': 'Rename', 'ncp.delete': 'Delete', 'ncp.netOpenBadge': 'NET OPEN', 'ncp.onAir': 'On air:', 'ncp.roster': 'Roster:', 'ncp.onAirActive': 'On air — active QSOs', 'ncp.activeHint': 'mic-pass order · ⬆⬇ to reorder · double-click → edit · "Log & end" to save', 'ncp.moveUp': 'Move up the mic-pass order', 'ncp.moveDown': 'Move down the mic-pass order', 'ncp.logEndSelected': 'Log & end selected', 'ncp.logAll': 'Log everyone ({n})', 'ncp.logAllConfirm': 'Log all {n} on-air station(s) to the logbook?', 'ncp.netUsersRoster': 'NET users — roster', 'ncp.rosterHint': 'double-click → put on air', 'ncp.addContact': 'Add contact', 'ncp.remove': 'Remove', 'ncp.putOnAir': 'Put selected on air', 'ncp.addContactTitle': 'Add contact to NET', 'ncp.addContactDesc': "Saved in this NET's roster (reused next time you open it).", 'ncp.callsign': 'Callsign', 'ncp.search': 'Search', 'ncp.name': 'Name', 'ncp.country': 'Country', 'ncp.cancel': 'Cancel', 'ncp.saveInNet': 'Save in NET',
'udpp.relayInstead': 'For an antenna switch or a relay board, use Station Control → relays instead: it holds the state, reads the boards at startup and does not re-switch while you tune inside a band. A home-made switch is the “HTTP relay” type there.', 'udpp.relayInstead': 'For an antenna switch or a relay board, use Station Control → relays instead: it holds the state, reads the boards at startup and does not re-switch while you tune inside a band. A home-made switch is the “HTTP relay” type there.',
'udpp.svcCustomLabel': 'Custom message', 'udpp.svcCustomHint': 'You choose what fires it and what it says. A UDP datagram or an HTTP request — the latter is how most antenna switches are driven.', 'udpp.trigger': 'Fires on', 'udpp.trgBand': 'Band change (radio)', 'udpp.trgQso': 'QSO logged', 'udpp.trgRotator': 'Rotator command', 'udpp.trgLookup': 'Callsign lookup', 'udpp.transport': 'Sends as', 'udpp.transportUdp': 'UDP message', 'udpp.transportUrl': 'URL (HTTP GET)', 'udpp.url': 'URL', 'udpp.urlHint': 'Values are URL-encoded. Credentials may be included as http://user:pass@host/… — stored as typed, so keep it to your own network.', 'udpp.template': 'Message', 'udpp.lineEnd': 'Line end', 'udpp.lineEndNone': 'None', 'udpp.fieldsAvailable': 'Fields for this trigger', 'udpp.fieldsHint': 'Anything else renders empty.', 'udpp.svcCustomLabel': 'Custom message', 'udpp.svcCustomHint': 'You choose what fires it and what it says. A UDP datagram or an HTTP request — the latter is how most antenna switches are driven.', 'udpp.trigger': 'Fires on', 'udpp.trgBand': 'Band change (radio)', 'udpp.trgQso': 'QSO logged', 'udpp.trgRotator': 'Rotator command', 'udpp.trgLookup': 'Callsign lookup', 'udpp.transport': 'Sends as', 'udpp.transportUdp': 'UDP message', 'udpp.transportUrl': 'URL (HTTP GET)', 'udpp.url': 'URL', 'udpp.urlHint': 'Values are URL-encoded. Credentials may be included as http://user:pass@host/… — stored as typed, so keep it to your own network.', 'udpp.template': 'Message', 'udpp.lineEnd': 'Line end', 'udpp.lineEndNone': 'None', 'udpp.fieldsAvailable': 'Fields for this trigger', 'udpp.fieldsHint': 'Anything else renders empty.',
'udpp.svcWsjtLabel': 'WSJT-X / JTDX / MSHV', 'udpp.svcWsjtHint': 'Auto-logs FT8/FT4/etc. QSOs and fills the entry callsign live.', 'udpp.svcAdifLabel': 'ADIF message (JTAlert, GridTracker)', 'udpp.svcAdifHint': 'Receives a single ADIF record per packet and logs it.', 'udpp.svcN1mmLabel': 'N1MM Logger+ (contest XML)', 'udpp.svcN1mmHint': 'Receives contest QSOs as XML messages.', 'udpp.svcRemoteLabel': 'Remote callsign (DXHunter, custom)', 'udpp.svcRemoteHint': 'A short text packet containing just a callsign — fills the entry field.', 'udpp.svcWsjtRelayLabel': 'Relay the WSJT-X stream', 'udpp.svcWsjtRelayHint': 'Re-sends every datagram received from WSJT-X / JTDX / MSHV, byte for byte, to another program — JTAlert, GridTracker, a second logger. The sender only talks to one address, so this is what lets them run alongside OpsLog. Point it at the OTHER programs port, never at one of OpsLogs own.', 'udpp.svcWsjtLogLabel': 'WSJT-X logged QSO', 'udpp.svcWsjtLogHint': 'Announces each logged QSO on the WSJT-X UDP interface — both messages WSJT-X itself sends. For any logger that listens there rather than for plain-text ADIF (Logger32s additional UDP sockets, for one).', 'udpp.svcDbLabel': 'ADIF Message', 'udpp.svcDbHint': 'Sends the ADIF of every QSO you log to a remote listener (Cloudlog UDP, N1MM, …).', 'udpp.svcPstLabel': 'PstRotator frequency', 'udpp.svcPstHint': 'Sends the rig frequency as <PST><FREQUENCY> whenever it changes — set PstRotatorAz tracker to DXLog.net (default port 12040).', 'udpp.svcN1mmRadioLabel': 'N1MM RadioInfo (freq + mode)', 'udpp.svcN1mmRadioHint': 'Broadcasts the rig frequency/mode as N1MM Logger+ RadioInfo XML on every change — consumed by PstRotator (N1MM tracker) and many other tools.', 'udpp.deleteConfirm': 'Delete this UDP connection?', 'udpp.loading': 'Loading…', 'udpp.intro': 'Connections let OpsLog talk to other ham radio software. Inbound connections receive QSOs or callsigns and update the logbook live; outbound connections notify other apps when you log a QSO locally. Enable multicast to share a port with another listener without conflict — required for the typical WSJT-X 2237 setup.', 'udpp.highlight': 'Highlight decodes in WSJT-X / JTDX', 'udpp.hlWorked': 'Grey out stations already worked', 'udpp.hlWorkedHint': 'The same callsign already in your log on this band AND in this mode — a duplicate. Grey, not a colour: the other three say look at this, and this one says the opposite. Off by default, because on a well-filled log it can match most of a period.', 'udpp.highlightHint': 'Colours callsigns in the decoders own Band Activity window from your log: watchlist members pink, a new DXCC green, a new band for its entity orange. Applied live as decodes arrive.', 'udpp.followMode': 'Switch the decoder\u2019s mode from spots', 'udpp.followModeHint': 'Clicking an FT4 spot while WSJT-X / JTDX sits in FT8 switches its mode too (Configure message).', 'udpp.inboundTitle': 'Inbound — OpsLog listens', 'udpp.outboundTitle': 'Outbound — OpsLog sends', 'udpp.reloadAll': 'Reload all', 'udpp.reloadHint': 'Restarts every enabled listener after a manual change.', 'udpp.add': 'Add', 'udpp.noConnection': 'No connection.', 'udpp.unnamed': '(unnamed)', 'udpp.dialogTitle': '{action} {direction} connection', 'udpp.new': 'New', 'udpp.edit': 'Edit', 'udpp.directionInbound': 'inbound', 'udpp.directionOutbound': 'outbound', 'udpp.name': 'Name', 'udpp.namePhInbound': 'WSJT-X log', 'udpp.namePhOutbound': 'Cloudlog notify', 'udpp.serviceType': 'Service type', 'udpp.port': 'Port', 'udpp.multicast': 'Multicast', 'udpp.multicastGroup': 'Multicast group', 'udpp.multicastHint': 'Use the same group address as the sending app. WSJT-X default is 224.0.0.1.', 'udpp.destinationIp': 'Destination IP', 'udpp.enabled': 'Enabled', 'udpp.cancel': 'Cancel', 'udpp.save': 'Save', 'udpp.svcWsjtLabel': 'WSJT-X / JTDX / MSHV', 'udpp.svcWsjtHint': 'Auto-logs FT8/FT4/etc. QSOs and fills the entry callsign live.', 'udpp.svcAdifLabel': 'ADIF message (JTAlert, GridTracker)', 'udpp.svcAdifHint': 'Receives a single ADIF record per packet and logs it.', 'udpp.svcN1mmLabel': 'N1MM Logger+ (contest XML)', 'udpp.svcN1mmHint': 'Receives contest QSOs as XML messages.', 'udpp.svcRemoteLabel': 'Remote callsign (DXHunter, custom)', 'udpp.svcRemoteHint': 'A short text packet containing just a callsign — fills the entry field.', 'udpp.svcWsjtRelayLabel': 'Relay the WSJT-X stream', 'udpp.svcWsjtRelayHint': 'Re-sends every datagram received from WSJT-X / JTDX / MSHV, byte for byte, to another program — JTAlert, GridTracker, a second logger. The sender only talks to one address, so this is what lets them run alongside OpsLog. Point it at the OTHER programs port, never at one of OpsLogs own.', 'udpp.svcWsjtLogLabel': 'WSJT-X logged QSO', 'udpp.svcWsjtLogHint': 'Announces each logged QSO on the WSJT-X UDP interface — both messages WSJT-X itself sends. For any logger that listens there rather than for plain-text ADIF (Logger32s additional UDP sockets, for one).', 'udpp.svcDbLabel': 'ADIF Message', 'udpp.svcDbHint': 'Sends the ADIF of every QSO you log to a remote listener (Cloudlog UDP, N1MM, …).', 'udpp.svcPstLabel': 'PstRotator frequency', 'udpp.svcPstHint': 'Sends the rig frequency as <PST><FREQUENCY> whenever it changes — set PstRotatorAz tracker to DXLog.net (default port 12040).', 'udpp.svcN1mmRadioLabel': 'N1MM RadioInfo (freq + mode)', 'udpp.svcN1mmRadioHint': 'Broadcasts the rig frequency/mode as N1MM Logger+ RadioInfo XML on every change — consumed by PstRotator (N1MM tracker) and many other tools.', 'udpp.deleteConfirm': 'Delete this UDP connection?', 'udpp.loading': 'Loading…', 'udpp.intro': 'Connections let OpsLog talk to other ham radio software. Inbound connections receive QSOs or callsigns and update the logbook live; outbound connections notify other apps when you log a QSO locally. Enable multicast to share a port with another listener without conflict — required for the typical WSJT-X 2237 setup.', 'udpp.highlight': 'Highlight decodes in WSJT-X / JTDX', 'udpp.hlWorked': 'Grey out stations already worked', 'udpp.hlColours': 'Colours — the background of each verdict; the text colour follows it.', 'udpp.hlWatchlist': 'Watch list', 'udpp.hlNewDxcc': 'New DXCC', 'udpp.hlNewBand': 'New band', 'udpp.hlWorkedC': 'Worked', 'udpp.hlReset': 'Reset to the defaults', 'udpp.hlWorkedHint': 'The same callsign already in your log on this band AND in this mode — a duplicate. Grey, not a colour: the other three say look at this, and this one says the opposite. Off by default, because on a well-filled log it can match most of a period.', 'udpp.highlightHint': 'Colours callsigns in the decoders own Band Activity window from your log: watchlist members pink, a new DXCC green, a new band for its entity orange. Applied live as decodes arrive.', 'udpp.followMode': 'Switch the decoder\u2019s mode from spots', 'udpp.followModeHint': 'Clicking an FT4 spot while WSJT-X / JTDX sits in FT8 switches its mode too (Configure message).', 'udpp.inboundTitle': 'Inbound — OpsLog listens', 'udpp.outboundTitle': 'Outbound — OpsLog sends', 'udpp.reloadAll': 'Reload all', 'udpp.reloadHint': 'Restarts every enabled listener after a manual change.', 'udpp.add': 'Add', 'udpp.noConnection': 'No connection.', 'udpp.unnamed': '(unnamed)', 'udpp.dialogTitle': '{action} {direction} connection', 'udpp.new': 'New', 'udpp.edit': 'Edit', 'udpp.directionInbound': 'inbound', 'udpp.directionOutbound': 'outbound', 'udpp.name': 'Name', 'udpp.namePhInbound': 'WSJT-X log', 'udpp.namePhOutbound': 'Cloudlog notify', 'udpp.serviceType': 'Service type', 'udpp.port': 'Port', 'udpp.multicast': 'Multicast', 'udpp.multicastGroup': 'Multicast group', 'udpp.multicastHint': 'Use the same group address as the sending app. WSJT-X default is 224.0.0.1.', 'udpp.destinationIp': 'Destination IP', 'udpp.enabled': 'Enabled', 'udpp.cancel': 'Cancel', 'udpp.save': 'Save',
'fltb.fCallsign': 'Callsign', 'fltb.fCreated': 'Added to the log on', 'fltb.fDate': 'Date / time (UTC)', 'fltb.fEndDate': 'End date / time', 'fltb.fBand': 'Band', 'fltb.fRxBand': 'RX band', 'fltb.fMode': 'Mode', 'fltb.fSubmode': 'Submode', 'fltb.fFreq': 'Frequency (Hz)', 'fltb.fRxFreq': 'RX frequency (Hz)', 'fltb.fRstSent': 'RST sent', 'fltb.fRstRcvd': 'RST rcvd', 'fltb.fName': 'Name', 'fltb.fQth': 'QTH', 'fltb.fAddress': 'Address', 'fltb.fEmail': 'E-mail', 'fltb.fGrid': 'Grid', 'fltb.fCountry': 'Country', 'fltb.fState': 'State', 'fltb.fCounty': 'County', 'fltb.fDxcc': 'DXCC #', 'fltb.fContinent': 'Continent', 'fltb.fCqz': 'CQ zone', 'fltb.fItuz': 'ITU zone', 'fltb.fIota': 'IOTA', 'fltb.fSota': 'SOTA ref', 'fltb.fPota': 'POTA ref', 'fltb.fWwff': 'WWFF ref', 'fltb.fRig': 'Rig', 'fltb.fAntenna': 'Antenna', 'fltb.fQslSent': 'Paper QSL sent status', 'fltb.fQslSentDate': 'Paper QSL sent date', 'fltb.fQslRcvd': 'Paper QSL received status', 'fltb.fQslRcvdDate': 'Paper QSL received date', 'fltb.fQslSentVia': 'QSL sent via', 'fltb.fQslRcvdVia': 'QSL rcvd via', 'fltb.fQslVia': 'QSL via', 'fltb.fLotwSent': 'LoTW sent status', 'fltb.fLotwSentDate': 'LoTW sent date', 'fltb.fLotwRcvd': 'LoTW received status', 'fltb.fLotwRcvdDate': 'LoTW received date', 'fltb.fEqslSent': 'eQSL sent status', 'fltb.fEqslSentDate': 'eQSL sent date', 'fltb.fEqslRcvd': 'eQSL received status', 'fltb.fEqslRcvdDate': 'eQSL received date', 'fltb.fQrzSent': 'QRZ.com sent status', 'fltb.fQrzSentDate': 'QRZ.com sent date', 'fltb.fQrzRcvd': 'QRZ.com received status', 'fltb.fQrzRcvdDate': 'QRZ.com received date', 'fltb.fClublogSent': 'Club Log sent status', 'fltb.fClublogSentDate': 'Club Log sent date', 'fltb.fClublogRcvd': 'Club Log match status', 'fltb.fClublogRcvdDate': 'Club Log match date', 'fltb.fHrdlogSent': 'HRDLog sent status', 'fltb.fHrdlogSentDate': 'HRDLog sent date', 'fltb.fHamqthSent': 'HamQTH sent status', 'fltb.fHamqthSentDate': 'HamQTH sent date', 'bulk.fHamqthSent': 'HamQTH sent status', 'bulk.fHamqthSentDate': 'HamQTH sent date', 'rqg.c.hamqth_sent': 'HamQTH sent', 'rqg.h.hamqth_sent': 'HamQTH sent', 'rqg.c.hamqth_sent_date': 'HamQTH sent date', 'rqg.h.hamqth_sent_date': 'HamQTH S date', 'fltb.fHamlogSent': 'HAMLOG.online sent', 'fltb.fHamlogSentDate': 'HAMLOG.online sent date', 'fltb.fHamlogRcvd': 'HAMLOG.online received', 'fltb.fHamlogRcvdDate': 'HAMLOG.online received date', 'fltb.fContestId': 'Contest ID', 'fltb.fSerialRcvd': 'Serial rcvd', 'fltb.fSerialSent': 'Serial sent', 'fltb.fPropMode': 'Propagation mode', 'fltb.fSatellite': 'Satellite', 'fltb.fMyCallsign': 'Station callsign (my call)', 'fltb.fOperator': 'Operator', 'fltb.fOwnerCallsign': 'Owner callsign', 'fltb.fMyGrid': 'My grid', 'fltb.fMyCountry': 'My country', 'fltb.fMyState': 'My state', 'fltb.fMyCounty': 'My county', 'fltb.fMyIota': 'My IOTA', 'fltb.fMySota': 'My SOTA ref', 'fltb.fMyPota': 'My POTA ref', 'fltb.fMyWwff': 'My WWFF ref', 'fltb.fMyStreet': 'My street', 'fltb.fMyCity': 'My city', 'fltb.fMyPostal': 'My postal code', 'fltb.fMyRig': 'My rig', 'fltb.fMyAntenna': 'My antenna', 'fltb.fTxPower': 'TX power (W)', 'fltb.fComment': 'Comment', 'fltb.fNotes': 'Notes', 'fltb.opEq': 'equals (=)', 'fltb.opNe': 'not equal (≠)', 'fltb.opContains': 'contains', 'fltb.opStartsWith': 'starts with', 'fltb.opEndsWith': 'ends with', 'fltb.opGt': 'greater than (>)', 'fltb.opLt': 'less than (<)', 'fltb.opGe': 'greater or equal (≥)', 'fltb.opLe': 'less or equal (≤)', 'fltb.opEmpty': 'is empty', 'fltb.opIn': 'is one of', 'fltb.opNotIn': 'is none of', 'fltb.listPh': '2m, 70cm — comma separated', 'fltb.opNotEmpty': 'is not empty', 'fltb.title': 'QSO filter', 'fltb.match': 'Match', 'fltb.all': 'ALL (AND)', 'fltb.any': 'ANY (OR)', 'fltb.loadPreset': 'Load preset…', 'fltb.noConditions': 'No conditions — the list shows all QSOs. Add one below.', 'fltb.where': 'WHERE', 'fltb.valuePh': 'value', 'fltb.remove': 'Remove', 'fltb.addCondition': 'Add condition', 'fltb.presetNamePh': 'Preset name…', 'fltb.presetSaved': 'Filter “{name}” saved', 'fltb.savePreset': 'Save preset', 'fltb.clear': 'Clear', 'fltb.cancel': 'Cancel', 'fltb.applyClose': 'Apply & close', 'fltb.fCallsign': 'Callsign', 'fltb.fCreated': 'Added to the log on', 'fltb.fDate': 'Date / time (UTC)', 'fltb.fEndDate': 'End date / time', 'fltb.fBand': 'Band', 'fltb.fRxBand': 'RX band', 'fltb.fMode': 'Mode', 'fltb.fSubmode': 'Submode', 'fltb.fFreq': 'Frequency (Hz)', 'fltb.fRxFreq': 'RX frequency (Hz)', 'fltb.fRstSent': 'RST sent', 'fltb.fRstRcvd': 'RST rcvd', 'fltb.fName': 'Name', 'fltb.fQth': 'QTH', 'fltb.fAddress': 'Address', 'fltb.fEmail': 'E-mail', 'fltb.fGrid': 'Grid', 'fltb.fCountry': 'Country', 'fltb.fState': 'State', 'fltb.fCounty': 'County', 'fltb.fDxcc': 'DXCC #', 'fltb.fContinent': 'Continent', 'fltb.fCqz': 'CQ zone', 'fltb.fItuz': 'ITU zone', 'fltb.fIota': 'IOTA', 'fltb.fSota': 'SOTA ref', 'fltb.fPota': 'POTA ref', 'fltb.fWwff': 'WWFF ref', 'fltb.fRig': 'Rig', 'fltb.fAntenna': 'Antenna', 'fltb.fQslSent': 'Paper QSL sent status', 'fltb.fQslSentDate': 'Paper QSL sent date', 'fltb.fQslRcvd': 'Paper QSL received status', 'fltb.fQslRcvdDate': 'Paper QSL received date', 'fltb.fQslSentVia': 'QSL sent via', 'fltb.fQslRcvdVia': 'QSL rcvd via', 'fltb.fQslVia': 'QSL via', 'fltb.fLotwSent': 'LoTW sent status', 'fltb.fLotwSentDate': 'LoTW sent date', 'fltb.fLotwRcvd': 'LoTW received status', 'fltb.fLotwRcvdDate': 'LoTW received date', 'fltb.fEqslSent': 'eQSL sent status', 'fltb.fEqslSentDate': 'eQSL sent date', 'fltb.fEqslRcvd': 'eQSL received status', 'fltb.fEqslRcvdDate': 'eQSL received date', 'fltb.fQrzSent': 'QRZ.com sent status', 'fltb.fQrzSentDate': 'QRZ.com sent date', 'fltb.fQrzRcvd': 'QRZ.com received status', 'fltb.fQrzRcvdDate': 'QRZ.com received date', 'fltb.fClublogSent': 'Club Log sent status', 'fltb.fClublogSentDate': 'Club Log sent date', 'fltb.fClublogRcvd': 'Club Log match status', 'fltb.fClublogRcvdDate': 'Club Log match date', 'fltb.fHrdlogSent': 'HRDLog sent status', 'fltb.fHrdlogSentDate': 'HRDLog sent date', 'fltb.fHamqthSent': 'HamQTH sent status', 'fltb.fHamqthSentDate': 'HamQTH sent date', 'bulk.fHamqthSent': 'HamQTH sent status', 'bulk.fHamqthSentDate': 'HamQTH sent date', 'rqg.c.hamqth_sent': 'HamQTH sent', 'rqg.h.hamqth_sent': 'HamQTH sent', 'rqg.c.hamqth_sent_date': 'HamQTH sent date', 'rqg.h.hamqth_sent_date': 'HamQTH S date', 'fltb.fHamlogSent': 'HAMLOG.online sent', 'fltb.fHamlogSentDate': 'HAMLOG.online sent date', 'fltb.fHamlogRcvd': 'HAMLOG.online received', 'fltb.fHamlogRcvdDate': 'HAMLOG.online received date', 'fltb.fContestId': 'Contest ID', 'fltb.fSerialRcvd': 'Serial rcvd', 'fltb.fSerialSent': 'Serial sent', 'fltb.fPropMode': 'Propagation mode', 'fltb.fSatellite': 'Satellite', 'fltb.fMyCallsign': 'Station callsign (my call)', 'fltb.fOperator': 'Operator', 'fltb.fOwnerCallsign': 'Owner callsign', 'fltb.fMyGrid': 'My grid', 'fltb.fMyCountry': 'My country', 'fltb.fMyState': 'My state', 'fltb.fMyCounty': 'My county', 'fltb.fMyIota': 'My IOTA', 'fltb.fMySota': 'My SOTA ref', 'fltb.fMyPota': 'My POTA ref', 'fltb.fMyWwff': 'My WWFF ref', 'fltb.fMyStreet': 'My street', 'fltb.fMyCity': 'My city', 'fltb.fMyPostal': 'My postal code', 'fltb.fMyRig': 'My rig', 'fltb.fMyAntenna': 'My antenna', 'fltb.fTxPower': 'TX power (W)', 'fltb.fComment': 'Comment', 'fltb.fNotes': 'Notes', 'fltb.opEq': 'equals (=)', 'fltb.opNe': 'not equal (≠)', 'fltb.opContains': 'contains', 'fltb.opStartsWith': 'starts with', 'fltb.opEndsWith': 'ends with', 'fltb.opGt': 'greater than (>)', 'fltb.opLt': 'less than (<)', 'fltb.opGe': 'greater or equal (≥)', 'fltb.opLe': 'less or equal (≤)', 'fltb.opEmpty': 'is empty', 'fltb.opIn': 'is one of', 'fltb.opNotIn': 'is none of', 'fltb.listPh': '2m, 70cm — comma separated', 'fltb.opNotEmpty': 'is not empty', 'fltb.title': 'QSO filter', 'fltb.match': 'Match', 'fltb.all': 'ALL (AND)', 'fltb.any': 'ANY (OR)', 'fltb.loadPreset': 'Load preset…', 'fltb.noConditions': 'No conditions — the list shows all QSOs. Add one below.', 'fltb.where': 'WHERE', 'fltb.valuePh': 'value', 'fltb.remove': 'Remove', 'fltb.addCondition': 'Add condition', 'fltb.presetNamePh': 'Preset name…', 'fltb.presetSaved': 'Filter “{name}” saved', 'fltb.savePreset': 'Save preset', 'fltb.clear': 'Clear', 'fltb.cancel': 'Cancel', 'fltb.applyClose': 'Apply & close',
'detp.propAS': 'Aircraft Scatter', 'detp.propAUR': 'Aurora', 'detp.propAUE': 'Aurora-E', 'detp.propBS': 'Back Scatter', 'detp.propEME': 'Earth-Moon-Earth', 'detp.propES': 'Sporadic E', 'detp.propFAI': 'Field Aligned Irregularities', 'detp.propF2': 'F2 Reflection', 'detp.propGWAVE': 'Ground Wave', 'detp.propINTERNET': 'Internet-assisted', 'detp.propION': 'Ionoscatter', 'detp.propLOS': 'Line of Sight', 'detp.propMS': 'Meteor Scatter', 'detp.propRPT': 'Terrestrial / atmospheric repeater', 'detp.propRS': 'Rain Scatter', 'detp.propSAT': 'Satellite', 'detp.propTEP': 'Trans-Equatorial', 'detp.propTR': 'Tropospheric Ducting', 'detp.pathShort': 'Short Path', 'detp.pathLong': 'Long Path', 'detp.pathGrayline': 'Grayline', 'detp.pathOther': 'Other', 'detp.tabStats': 'Stats', 'detp.tabInfo': 'Info', 'detp.tabAwards': 'Awards', 'detp.tabMy': 'My', 'detp.tabExtended': 'Extended', 'detp.statePref': 'State / pref', 'detp.county': 'County', 'detp.newCounty': 'NEW', 'detp.newCountyTip': 'County never worked before', 'detp.prefix': 'Prefix', 'detp.cqZone': 'CQ', 'detp.ituZone': 'ITU', 'detp.dxcc': 'DXCC #', 'detp.azimuthLp': 'Azimuth LP', 'detp.distanceSp': 'Distance SP', 'detp.distanceLp': 'Distance LP', 'detp.address': 'Address', 'detp.qslMessage': 'QSL message', 'detp.qslVia': 'QSL via (manager)', 'detp.detected': 'Detected — this contact will count for:', 'detp.ambiguous': 'Ambiguous — pick one:', 'detp.azimuth': 'Azimuth (°)', 'detp.elevation': 'Elevation (°)', 'detp.txPower': 'TX power (W)', 'detp.satelliteMode': 'Satellite mode', 'detp.antPath': 'Ant. path', 'detp.propagation': 'Propagation', 'detp.rig': 'Rig', 'detp.antenna': 'Antenna', 'detp.satName': 'Satellite name', 'detp.contestId': 'Contest ID', 'detp.rcvdExchangePh': 'rcvd exchange', 'detp.sentExchangePh': 'sent exchange', 'detp.contactedEmail': 'Contacted email', 'detp.contactedWeb': 'Website', 'detp.propAS': 'Aircraft Scatter', 'detp.propAUR': 'Aurora', 'detp.propAUE': 'Aurora-E', 'detp.propBS': 'Back Scatter', 'detp.propEME': 'Earth-Moon-Earth', 'detp.propES': 'Sporadic E', 'detp.propFAI': 'Field Aligned Irregularities', 'detp.propF2': 'F2 Reflection', 'detp.propGWAVE': 'Ground Wave', 'detp.propINTERNET': 'Internet-assisted', 'detp.propION': 'Ionoscatter', 'detp.propLOS': 'Line of Sight', 'detp.propMS': 'Meteor Scatter', 'detp.propRPT': 'Terrestrial / atmospheric repeater', 'detp.propRS': 'Rain Scatter', 'detp.propSAT': 'Satellite', 'detp.propTEP': 'Trans-Equatorial', 'detp.propTR': 'Tropospheric Ducting', 'detp.pathShort': 'Short Path', 'detp.pathLong': 'Long Path', 'detp.pathGrayline': 'Grayline', 'detp.pathOther': 'Other', 'detp.tabStats': 'Stats', 'detp.tabInfo': 'Info', 'detp.tabAwards': 'Awards', 'detp.tabMy': 'My', 'detp.tabExtended': 'Extended', 'detp.statePref': 'State / pref', 'detp.county': 'County', 'detp.newCounty': 'NEW', 'detp.newCountyTip': 'County never worked before', 'detp.prefix': 'Prefix', 'detp.cqZone': 'CQ', 'detp.ituZone': 'ITU', 'detp.dxcc': 'DXCC #', 'detp.azimuthLp': 'Azimuth LP', 'detp.distanceSp': 'Distance SP', 'detp.distanceLp': 'Distance LP', 'detp.address': 'Address', 'detp.qslMessage': 'QSL message', 'detp.qslVia': 'QSL via (manager)', 'detp.detected': 'Detected — this contact will count for:', 'detp.ambiguous': 'Ambiguous — pick one:', 'detp.azimuth': 'Azimuth (°)', 'detp.elevation': 'Elevation (°)', 'detp.txPower': 'TX power (W)', 'detp.satelliteMode': 'Satellite mode', 'detp.antPath': 'Ant. path', 'detp.propagation': 'Propagation', 'detp.rig': 'Rig', 'detp.antenna': 'Antenna', 'detp.satName': 'Satellite name', 'detp.contestId': 'Contest ID', 'detp.rcvdExchangePh': 'rcvd exchange', 'detp.sentExchangePh': 'sent exchange', 'detp.contactedEmail': 'Contacted email', 'detp.contactedWeb': 'Website',
// Awards (ref picker / ref selector / awards panel / award editor) // Awards (ref picker / ref selector / awards panel / award editor)
@@ -1050,7 +1050,7 @@ const fr: Dict = {
'ncp.newNetPrompt': 'Nom du nouveau NET :', 'ncp.renamePrompt': 'Renommer le NET :', 'ncp.deleteConfirm': 'Supprimer le NET « {name} » et son répertoire ? Cette action est irréversible.', 'ncp.closeConfirm': "{n} station(s) encore en l'air seront retirées SANS être enregistrées. Fermer quand même ?", 'ncp.removeConfirm': 'Retirer {n} station(s) du répertoire de ce NET ?', 'ncp.colCallsign': 'Indicatif', 'ncp.colName': 'Nom', 'ncp.colTimeOn': 'Heure début', 'ncp.colBand': 'Bande', 'ncp.colMode': 'Mode', 'ncp.colComment': 'Commentaire', 'ncp.colCountry': 'Pays', 'ncp.colDate': 'Date', 'ncp.workedBefore': 'Déjà contacté', 'ncp.wbHint': 'Cliquer une station (on air ou roster) pour voir les QSO précédents', 'ncp.wbNone': 'Aucun QSO précédent avec', 'ncp.wbFirst': 'premier', 'ncp.wbLast': 'dernier', 'ncp.wbResize': 'Glisser pour redimensionner', 'ncp.newNet': 'Nouveau NET', 'ncp.closeToSwitch': 'Ferme le NET pour changer', 'ncp.selectNetTitle': 'Sélectionne un NET', 'ncp.selectNetOption': '— sélectionner un NET —', 'ncp.closeNet': 'Fermer le NET', 'ncp.openNet': 'Ouvrir le NET', 'ncp.rename': 'Renommer', 'ncp.delete': 'Supprimer', 'ncp.netOpenBadge': 'NET OUVERT', 'ncp.onAir': "En l'air :", 'ncp.roster': 'Répertoire :', 'ncp.onAirActive': "En l'air — QSO actifs", 'ncp.activeHint': 'ordre de passage du micro · ⬆⬇ pour réordonner · double-clic → éditer · « Logger & terminer »', 'ncp.moveUp': "Monter dans l'ordre de passage", 'ncp.moveDown': "Descendre dans l'ordre de passage", 'ncp.logEndSelected': 'Logger & terminer la sélection', 'ncp.logAll': 'Logger tout le monde ({n})', 'ncp.logAllConfirm': 'Logger les {n} station(s) on air dans le logbook ?', 'ncp.netUsersRoster': 'Membres du NET — répertoire', 'ncp.rosterHint': "double-clic → mettre en l'air", 'ncp.addContact': 'Ajouter un contact', 'ncp.remove': 'Retirer', 'ncp.putOnAir': "Mettre la sélection en l'air", 'ncp.addContactTitle': 'Ajouter un contact au NET', 'ncp.addContactDesc': 'Enregistré dans le répertoire de ce NET (réutilisé à la prochaine ouverture).', 'ncp.callsign': 'Indicatif', 'ncp.search': 'Rechercher', 'ncp.name': 'Nom', 'ncp.country': 'Pays', 'ncp.cancel': 'Annuler', 'ncp.saveInNet': 'Enregistrer dans le NET', 'ncp.newNetPrompt': 'Nom du nouveau NET :', 'ncp.renamePrompt': 'Renommer le NET :', 'ncp.deleteConfirm': 'Supprimer le NET « {name} » et son répertoire ? Cette action est irréversible.', 'ncp.closeConfirm': "{n} station(s) encore en l'air seront retirées SANS être enregistrées. Fermer quand même ?", 'ncp.removeConfirm': 'Retirer {n} station(s) du répertoire de ce NET ?', 'ncp.colCallsign': 'Indicatif', 'ncp.colName': 'Nom', 'ncp.colTimeOn': 'Heure début', 'ncp.colBand': 'Bande', 'ncp.colMode': 'Mode', 'ncp.colComment': 'Commentaire', 'ncp.colCountry': 'Pays', 'ncp.colDate': 'Date', 'ncp.workedBefore': 'Déjà contacté', 'ncp.wbHint': 'Cliquer une station (on air ou roster) pour voir les QSO précédents', 'ncp.wbNone': 'Aucun QSO précédent avec', 'ncp.wbFirst': 'premier', 'ncp.wbLast': 'dernier', 'ncp.wbResize': 'Glisser pour redimensionner', 'ncp.newNet': 'Nouveau NET', 'ncp.closeToSwitch': 'Ferme le NET pour changer', 'ncp.selectNetTitle': 'Sélectionne un NET', 'ncp.selectNetOption': '— sélectionner un NET —', 'ncp.closeNet': 'Fermer le NET', 'ncp.openNet': 'Ouvrir le NET', 'ncp.rename': 'Renommer', 'ncp.delete': 'Supprimer', 'ncp.netOpenBadge': 'NET OUVERT', 'ncp.onAir': "En l'air :", 'ncp.roster': 'Répertoire :', 'ncp.onAirActive': "En l'air — QSO actifs", 'ncp.activeHint': 'ordre de passage du micro · ⬆⬇ pour réordonner · double-clic → éditer · « Logger & terminer »', 'ncp.moveUp': "Monter dans l'ordre de passage", 'ncp.moveDown': "Descendre dans l'ordre de passage", 'ncp.logEndSelected': 'Logger & terminer la sélection', 'ncp.logAll': 'Logger tout le monde ({n})', 'ncp.logAllConfirm': 'Logger les {n} station(s) on air dans le logbook ?', 'ncp.netUsersRoster': 'Membres du NET — répertoire', 'ncp.rosterHint': "double-clic → mettre en l'air", 'ncp.addContact': 'Ajouter un contact', 'ncp.remove': 'Retirer', 'ncp.putOnAir': "Mettre la sélection en l'air", 'ncp.addContactTitle': 'Ajouter un contact au NET', 'ncp.addContactDesc': 'Enregistré dans le répertoire de ce NET (réutilisé à la prochaine ouverture).', 'ncp.callsign': 'Indicatif', 'ncp.search': 'Rechercher', 'ncp.name': 'Nom', 'ncp.country': 'Pays', 'ncp.cancel': 'Annuler', 'ncp.saveInNet': 'Enregistrer dans le NET',
'udpp.relayInstead': 'Pour un commutateur dantennes ou une carte de relais, préférez Station Control → relais : il tient l’état, relit les cartes au démarrage et ne recommute pas quand vous bougez dans la même bande. Un commutateur fait main sy déclare en type « Relais HTTP ».', 'udpp.relayInstead': 'Pour un commutateur dantennes ou une carte de relais, préférez Station Control → relais : il tient l’état, relit les cartes au démarrage et ne recommute pas quand vous bougez dans la même bande. Un commutateur fait main sy déclare en type « Relais HTTP ».',
'udpp.svcCustomLabel': 'Message personnalisé', 'udpp.svcCustomHint': 'Vous choisissez ce qui le déclenche et ce quil dit. Datagramme UDP ou requête HTTP — cette dernière est la façon dont se pilotent la plupart des commutateurs dantennes.', 'udpp.trigger': 'Déclencheur', 'udpp.trgBand': 'Changement de bande (radio)', 'udpp.trgQso': 'QSO enregistré', 'udpp.trgRotator': 'Commande de rotor', 'udpp.trgLookup': 'Recherche dindicatif', 'udpp.transport': 'Envoi', 'udpp.transportUdp': 'Message UDP', 'udpp.transportUrl': 'URL (HTTP GET)', 'udpp.url': 'URL', 'udpp.urlHint': 'Les valeurs sont encodées pour lURL. Les identifiants peuvent s’écrire http://user:pass@hôte/… — stockés tels quels, à réserver à votre réseau local.', 'udpp.template': 'Message', 'udpp.lineEnd': 'Fin de ligne', 'udpp.lineEndNone': 'Aucune', 'udpp.fieldsAvailable': 'Champs de ce déclencheur', 'udpp.fieldsHint': 'Tout autre champ rendra du vide.', 'udpp.svcCustomLabel': 'Message personnalisé', 'udpp.svcCustomHint': 'Vous choisissez ce qui le déclenche et ce quil dit. Datagramme UDP ou requête HTTP — cette dernière est la façon dont se pilotent la plupart des commutateurs dantennes.', 'udpp.trigger': 'Déclencheur', 'udpp.trgBand': 'Changement de bande (radio)', 'udpp.trgQso': 'QSO enregistré', 'udpp.trgRotator': 'Commande de rotor', 'udpp.trgLookup': 'Recherche dindicatif', 'udpp.transport': 'Envoi', 'udpp.transportUdp': 'Message UDP', 'udpp.transportUrl': 'URL (HTTP GET)', 'udpp.url': 'URL', 'udpp.urlHint': 'Les valeurs sont encodées pour lURL. Les identifiants peuvent s’écrire http://user:pass@hôte/… — stockés tels quels, à réserver à votre réseau local.', 'udpp.template': 'Message', 'udpp.lineEnd': 'Fin de ligne', 'udpp.lineEndNone': 'Aucune', 'udpp.fieldsAvailable': 'Champs de ce déclencheur', 'udpp.fieldsHint': 'Tout autre champ rendra du vide.',
'udpp.svcWsjtLabel': 'WSJT-X / JTDX / MSHV', 'udpp.svcWsjtHint': "Logue automatiquement les QSO FT8/FT4/etc. et remplit l'indicatif de saisie en direct.", 'udpp.svcAdifLabel': 'Message ADIF (JTAlert, GridTracker)', 'udpp.svcAdifHint': 'Reçoit un seul enregistrement ADIF par paquet et le logue.', 'udpp.svcN1mmLabel': 'N1MM Logger+ (XML contest)', 'udpp.svcN1mmHint': 'Reçoit les QSO de contest sous forme de messages XML.', 'udpp.svcRemoteLabel': 'Indicatif distant (DXHunter, personnalisé)', 'udpp.svcRemoteHint': 'Un court paquet texte contenant juste un indicatif — remplit le champ de saisie.', 'udpp.svcWsjtRelayLabel': 'Relayer le flux WSJT-X', 'udpp.svcWsjtRelayHint': 'Réémet chaque datagramme reçu de WSJT-X / JTDX / MSHV, octet pour octet, vers un autre logiciel — JTAlert, GridTracker, un second carnet. L’émetteur ne parle qu’à une seule adresse : cest ce qui permet de les faire tourner à côté dOpsLog. À pointer sur le port de lAUTRE logiciel, jamais sur un port d’écoute dOpsLog.', 'udpp.svcWsjtLogLabel': 'QSO enregistré WSJT-X', 'udpp.svcWsjtLogHint': 'Annonce chaque QSO enregistré sur linterface UDP WSJT-X — les deux messages que WSJT-X émet lui-même. Pour tout logger qui écoute là plutôt que lADIF en texte brut (les sockets UDP supplémentaires de Logger32, par exemple).', 'udpp.svcDbLabel': "ADIF Message", 'udpp.svcDbHint': "Envoie l'ADIF de chaque QSO enregistré vers un écouteur distant (Cloudlog UDP, N1MM…).", 'udpp.svcPstLabel': 'Fréquence PstRotator', 'udpp.svcPstHint': "Envoie la fréquence du poste en <PST><FREQUENCY> à chaque changement — règle le tracker de PstRotatorAz sur DXLog.net (port 12040 par défaut).", 'udpp.svcN1mmRadioLabel': 'N1MM RadioInfo (fréq + mode)', 'udpp.svcN1mmRadioHint': "Diffuse la fréquence/mode du poste en XML RadioInfo N1MM Logger+ à chaque changement — lu par PstRotator (tracker N1MM) et beaucoup d'autres outils.", 'udpp.deleteConfirm': 'Supprimer cette connexion UDP ?', 'udpp.loading': 'Chargement…', 'udpp.intro': "Les connexions permettent à OpsLog de dialoguer avec d'autres logiciels radioamateurs. Les connexions entrantes reçoivent des QSO ou des indicatifs et mettent le journal à jour en direct ; les connexions sortantes notifient d'autres apps quand tu enregistres un QSO localement. Active le multicast pour partager un port avec un autre écouteur sans conflit — nécessaire pour la config WSJT-X 2237 classique.", 'udpp.highlight': 'Surligner les décodages dans WSJT-X / JTDX', 'udpp.hlWorked': 'Griser les stations déjà travaillées', 'udpp.hlWorkedHint': 'Le même indicatif déjà au log sur cette bande ET dans ce mode — un doublon. En gris, pas en couleur : les trois autres disent « regarde », celle-ci dit linverse. Désactivé par défaut, car sur un log bien rempli elle peut concerner la majorité dune période.', 'udpp.highlightHint': 'Colore les indicatifs dans la fenêtre Band Activity du décodeur selon votre log : watchlist en rose, nouveau DXCC en vert, nouvelle bande pour son entité en orange. Appliqué en direct à larrivée des décodages.', 'udpp.followMode': 'Changer le mode du décodeur depuis les spots', 'udpp.followModeHint': 'Cliquer un spot FT4 pendant que WSJT-X / JTDX est en FT8 change aussi son mode.', 'udpp.inboundTitle': 'Entrant — OpsLog écoute', 'udpp.outboundTitle': 'Sortant — OpsLog envoie', 'udpp.reloadAll': 'Tout recharger', 'udpp.reloadHint': 'Redémarre chaque écouteur activé après une modification manuelle.', 'udpp.add': 'Ajouter', 'udpp.noConnection': 'Aucune connexion.', 'udpp.unnamed': '(sans nom)', 'udpp.dialogTitle': '{action} connexion {direction}', 'udpp.new': 'Nouvelle', 'udpp.edit': 'Modifier', 'udpp.directionInbound': 'entrante', 'udpp.directionOutbound': 'sortante', 'udpp.name': 'Nom', 'udpp.namePhInbound': 'Log WSJT-X', 'udpp.namePhOutbound': 'Notification Cloudlog', 'udpp.serviceType': 'Type de service', 'udpp.port': 'Port', 'udpp.multicast': 'Multicast', 'udpp.multicastGroup': 'Groupe multicast', 'udpp.multicastHint': "Utilise la même adresse de groupe que l'app émettrice. Le défaut WSJT-X est 224.0.0.1.", 'udpp.destinationIp': 'IP de destination', 'udpp.enabled': 'Activé', 'udpp.cancel': 'Annuler', 'udpp.save': 'Enregistrer', 'udpp.svcWsjtLabel': 'WSJT-X / JTDX / MSHV', 'udpp.svcWsjtHint': "Logue automatiquement les QSO FT8/FT4/etc. et remplit l'indicatif de saisie en direct.", 'udpp.svcAdifLabel': 'Message ADIF (JTAlert, GridTracker)', 'udpp.svcAdifHint': 'Reçoit un seul enregistrement ADIF par paquet et le logue.', 'udpp.svcN1mmLabel': 'N1MM Logger+ (XML contest)', 'udpp.svcN1mmHint': 'Reçoit les QSO de contest sous forme de messages XML.', 'udpp.svcRemoteLabel': 'Indicatif distant (DXHunter, personnalisé)', 'udpp.svcRemoteHint': 'Un court paquet texte contenant juste un indicatif — remplit le champ de saisie.', 'udpp.svcWsjtRelayLabel': 'Relayer le flux WSJT-X', 'udpp.svcWsjtRelayHint': 'Réémet chaque datagramme reçu de WSJT-X / JTDX / MSHV, octet pour octet, vers un autre logiciel — JTAlert, GridTracker, un second carnet. L’émetteur ne parle qu’à une seule adresse : cest ce qui permet de les faire tourner à côté dOpsLog. À pointer sur le port de lAUTRE logiciel, jamais sur un port d’écoute dOpsLog.', 'udpp.svcWsjtLogLabel': 'QSO enregistré WSJT-X', 'udpp.svcWsjtLogHint': 'Annonce chaque QSO enregistré sur linterface UDP WSJT-X — les deux messages que WSJT-X émet lui-même. Pour tout logger qui écoute là plutôt que lADIF en texte brut (les sockets UDP supplémentaires de Logger32, par exemple).', 'udpp.svcDbLabel': "ADIF Message", 'udpp.svcDbHint': "Envoie l'ADIF de chaque QSO enregistré vers un écouteur distant (Cloudlog UDP, N1MM…).", 'udpp.svcPstLabel': 'Fréquence PstRotator', 'udpp.svcPstHint': "Envoie la fréquence du poste en <PST><FREQUENCY> à chaque changement — règle le tracker de PstRotatorAz sur DXLog.net (port 12040 par défaut).", 'udpp.svcN1mmRadioLabel': 'N1MM RadioInfo (fréq + mode)', 'udpp.svcN1mmRadioHint': "Diffuse la fréquence/mode du poste en XML RadioInfo N1MM Logger+ à chaque changement — lu par PstRotator (tracker N1MM) et beaucoup d'autres outils.", 'udpp.deleteConfirm': 'Supprimer cette connexion UDP ?', 'udpp.loading': 'Chargement…', 'udpp.intro': "Les connexions permettent à OpsLog de dialoguer avec d'autres logiciels radioamateurs. Les connexions entrantes reçoivent des QSO ou des indicatifs et mettent le journal à jour en direct ; les connexions sortantes notifient d'autres apps quand tu enregistres un QSO localement. Active le multicast pour partager un port avec un autre écouteur sans conflit — nécessaire pour la config WSJT-X 2237 classique.", 'udpp.highlight': 'Surligner les décodages dans WSJT-X / JTDX', 'udpp.hlWorked': 'Griser les stations déjà travaillées', 'udpp.hlColours': 'Couleurs — le fond de chaque verdict ; la couleur du texte en découle.', 'udpp.hlWatchlist': 'Watchlist', 'udpp.hlNewDxcc': 'Nouveau DXCC', 'udpp.hlNewBand': 'Nouvelle bande', 'udpp.hlWorkedC': 'Contactée', 'udpp.hlReset': 'Rétablir les couleurs dorigine', 'udpp.hlWorkedHint': 'Le même indicatif déjà au log sur cette bande ET dans ce mode — un doublon. En gris, pas en couleur : les trois autres disent « regarde », celle-ci dit linverse. Désactivé par défaut, car sur un log bien rempli elle peut concerner la majorité dune période.', 'udpp.highlightHint': 'Colore les indicatifs dans la fenêtre Band Activity du décodeur selon votre log : watchlist en rose, nouveau DXCC en vert, nouvelle bande pour son entité en orange. Appliqué en direct à larrivée des décodages.', 'udpp.followMode': 'Changer le mode du décodeur depuis les spots', 'udpp.followModeHint': 'Cliquer un spot FT4 pendant que WSJT-X / JTDX est en FT8 change aussi son mode.', 'udpp.inboundTitle': 'Entrant — OpsLog écoute', 'udpp.outboundTitle': 'Sortant — OpsLog envoie', 'udpp.reloadAll': 'Tout recharger', 'udpp.reloadHint': 'Redémarre chaque écouteur activé après une modification manuelle.', 'udpp.add': 'Ajouter', 'udpp.noConnection': 'Aucune connexion.', 'udpp.unnamed': '(sans nom)', 'udpp.dialogTitle': '{action} connexion {direction}', 'udpp.new': 'Nouvelle', 'udpp.edit': 'Modifier', 'udpp.directionInbound': 'entrante', 'udpp.directionOutbound': 'sortante', 'udpp.name': 'Nom', 'udpp.namePhInbound': 'Log WSJT-X', 'udpp.namePhOutbound': 'Notification Cloudlog', 'udpp.serviceType': 'Type de service', 'udpp.port': 'Port', 'udpp.multicast': 'Multicast', 'udpp.multicastGroup': 'Groupe multicast', 'udpp.multicastHint': "Utilise la même adresse de groupe que l'app émettrice. Le défaut WSJT-X est 224.0.0.1.", 'udpp.destinationIp': 'IP de destination', 'udpp.enabled': 'Activé', 'udpp.cancel': 'Annuler', 'udpp.save': 'Enregistrer',
'fltb.fCallsign': 'Callsign', 'fltb.fCreated': 'Ajouté au journal le', 'fltb.fDate': 'Date / time (UTC)', 'fltb.fEndDate': 'End date / time', 'fltb.fBand': 'Band', 'fltb.fRxBand': 'RX band', 'fltb.fMode': 'Mode', 'fltb.fSubmode': 'Submode', 'fltb.fFreq': 'Frequency (Hz)', 'fltb.fRxFreq': 'RX frequency (Hz)', 'fltb.fRstSent': 'RST sent', 'fltb.fRstRcvd': 'RST rcvd', 'fltb.fName': 'Name', 'fltb.fQth': 'QTH', 'fltb.fAddress': 'Address', 'fltb.fEmail': 'E-mail', 'fltb.fGrid': 'Grid', 'fltb.fCountry': 'Country', 'fltb.fState': 'State', 'fltb.fCounty': 'County', 'fltb.fDxcc': 'DXCC #', 'fltb.fContinent': 'Continent', 'fltb.fCqz': 'CQ zone', 'fltb.fItuz': 'ITU zone', 'fltb.fIota': 'IOTA', 'fltb.fSota': 'SOTA ref', 'fltb.fPota': 'POTA ref', 'fltb.fWwff': 'WWFF ref', 'fltb.fRig': 'Rig', 'fltb.fAntenna': 'Antenna', 'fltb.fQslSent': 'Paper QSL sent status', 'fltb.fQslSentDate': 'Paper QSL sent date', 'fltb.fQslRcvd': 'Paper QSL received status', 'fltb.fQslRcvdDate': 'Paper QSL received date', 'fltb.fQslSentVia': 'QSL envoyée via', 'fltb.fQslRcvdVia': 'QSL reçue via', 'fltb.fQslVia': 'QSL via', 'fltb.fLotwSent': 'LoTW sent status', 'fltb.fLotwSentDate': 'LoTW sent date', 'fltb.fLotwRcvd': 'LoTW received status', 'fltb.fLotwRcvdDate': 'LoTW received date', 'fltb.fEqslSent': 'eQSL sent status', 'fltb.fEqslSentDate': 'eQSL sent date', 'fltb.fEqslRcvd': 'eQSL received status', 'fltb.fEqslRcvdDate': 'eQSL received date', 'fltb.fQrzSent': 'QRZ.com sent status', 'fltb.fQrzSentDate': 'QRZ.com sent date', 'fltb.fQrzRcvd': 'QRZ.com received status', 'fltb.fQrzRcvdDate': 'QRZ.com received date', 'fltb.fClublogSent': 'Club Log sent status', 'fltb.fClublogSentDate': 'Club Log sent date', 'fltb.fClublogRcvd': 'Statut match Club Log', 'fltb.fClublogRcvdDate': 'Date match Club Log', 'fltb.fHrdlogSent': 'HRDLog sent status', 'fltb.fHrdlogSentDate': 'HRDLog sent date', 'fltb.fHamqthSent': 'Statut envoi HamQTH', 'fltb.fHamqthSentDate': 'Date denvoi HamQTH', 'bulk.fHamqthSent': 'Statut envoi HamQTH', 'bulk.fHamqthSentDate': 'Date denvoi HamQTH', 'rqg.c.hamqth_sent': 'HamQTH env', 'rqg.h.hamqth_sent': 'HamQTH env.', 'rqg.c.hamqth_sent_date': 'Date env HamQTH', 'rqg.h.hamqth_sent_date': 'HamQTH env.', 'fltb.fHamlogSent': 'HAMLOG.online envoyé', 'fltb.fHamlogSentDate': "HAMLOG.online date d'envoi", 'fltb.fHamlogRcvd': 'HAMLOG.online reçu', 'fltb.fHamlogRcvdDate': 'HAMLOG.online date de réception', 'fltb.fContestId': 'Contest ID', 'fltb.fSerialRcvd': 'Serial rcvd', 'fltb.fSerialSent': 'Serial sent', 'fltb.fPropMode': 'Propagation mode', 'fltb.fSatellite': 'Satellite', 'fltb.fMyCallsign': 'Station callsign (my call)', 'fltb.fOperator': 'Operator', 'fltb.fOwnerCallsign': 'Owner callsign', 'fltb.fMyGrid': 'My grid', 'fltb.fMyCountry': 'My country', 'fltb.fMyState': 'My state', 'fltb.fMyCounty': 'My county', 'fltb.fMyIota': 'My IOTA', 'fltb.fMySota': 'My SOTA ref', 'fltb.fMyPota': 'My POTA ref', 'fltb.fMyWwff': 'My WWFF ref', 'fltb.fMyStreet': 'My street', 'fltb.fMyCity': 'My city', 'fltb.fMyPostal': 'My postal code', 'fltb.fMyRig': 'My rig', 'fltb.fMyAntenna': 'My antenna', 'fltb.fTxPower': 'TX power (W)', 'fltb.fComment': 'Comment', 'fltb.fNotes': 'Notes', 'fltb.opEq': 'égal (=)', 'fltb.opNe': 'différent (≠)', 'fltb.opContains': 'contient', 'fltb.opStartsWith': 'commence par', 'fltb.opEndsWith': 'finit par', 'fltb.opGt': 'supérieur à (>)', 'fltb.opLt': 'inférieur à (<)', 'fltb.opGe': 'supérieur ou égal (≥)', 'fltb.opLe': 'inférieur ou égal (≤)', 'fltb.opEmpty': 'est vide', 'fltb.opIn': 'est parmi', 'fltb.opNotIn': 'n est pas parmi', 'fltb.listPh': '2m, 70cm — séparés par des virgules', 'fltb.opNotEmpty': "n'est pas vide", 'fltb.title': 'Filtre QSO', 'fltb.match': 'Correspondance', 'fltb.all': 'TOUS (ET)', 'fltb.any': 'AU MOINS UN (OU)', 'fltb.loadPreset': 'Charger un préréglage…', 'fltb.noConditions': 'Aucune condition — la liste affiche tous les QSO. Ajoutes-en une ci-dessous.', 'fltb.where': 'OÙ', 'fltb.valuePh': 'valeur', 'fltb.remove': 'Retirer', 'fltb.addCondition': 'Ajouter une condition', 'fltb.presetNamePh': 'Nom du préréglage…', 'fltb.presetSaved': 'Filtre « {name} » enregistré', 'fltb.savePreset': 'Enregistrer le préréglage', 'fltb.clear': 'Effacer', 'fltb.cancel': 'Annuler', 'fltb.applyClose': 'Appliquer & fermer', 'fltb.fCallsign': 'Callsign', 'fltb.fCreated': 'Ajouté au journal le', 'fltb.fDate': 'Date / time (UTC)', 'fltb.fEndDate': 'End date / time', 'fltb.fBand': 'Band', 'fltb.fRxBand': 'RX band', 'fltb.fMode': 'Mode', 'fltb.fSubmode': 'Submode', 'fltb.fFreq': 'Frequency (Hz)', 'fltb.fRxFreq': 'RX frequency (Hz)', 'fltb.fRstSent': 'RST sent', 'fltb.fRstRcvd': 'RST rcvd', 'fltb.fName': 'Name', 'fltb.fQth': 'QTH', 'fltb.fAddress': 'Address', 'fltb.fEmail': 'E-mail', 'fltb.fGrid': 'Grid', 'fltb.fCountry': 'Country', 'fltb.fState': 'State', 'fltb.fCounty': 'County', 'fltb.fDxcc': 'DXCC #', 'fltb.fContinent': 'Continent', 'fltb.fCqz': 'CQ zone', 'fltb.fItuz': 'ITU zone', 'fltb.fIota': 'IOTA', 'fltb.fSota': 'SOTA ref', 'fltb.fPota': 'POTA ref', 'fltb.fWwff': 'WWFF ref', 'fltb.fRig': 'Rig', 'fltb.fAntenna': 'Antenna', 'fltb.fQslSent': 'Paper QSL sent status', 'fltb.fQslSentDate': 'Paper QSL sent date', 'fltb.fQslRcvd': 'Paper QSL received status', 'fltb.fQslRcvdDate': 'Paper QSL received date', 'fltb.fQslSentVia': 'QSL envoyée via', 'fltb.fQslRcvdVia': 'QSL reçue via', 'fltb.fQslVia': 'QSL via', 'fltb.fLotwSent': 'LoTW sent status', 'fltb.fLotwSentDate': 'LoTW sent date', 'fltb.fLotwRcvd': 'LoTW received status', 'fltb.fLotwRcvdDate': 'LoTW received date', 'fltb.fEqslSent': 'eQSL sent status', 'fltb.fEqslSentDate': 'eQSL sent date', 'fltb.fEqslRcvd': 'eQSL received status', 'fltb.fEqslRcvdDate': 'eQSL received date', 'fltb.fQrzSent': 'QRZ.com sent status', 'fltb.fQrzSentDate': 'QRZ.com sent date', 'fltb.fQrzRcvd': 'QRZ.com received status', 'fltb.fQrzRcvdDate': 'QRZ.com received date', 'fltb.fClublogSent': 'Club Log sent status', 'fltb.fClublogSentDate': 'Club Log sent date', 'fltb.fClublogRcvd': 'Statut match Club Log', 'fltb.fClublogRcvdDate': 'Date match Club Log', 'fltb.fHrdlogSent': 'HRDLog sent status', 'fltb.fHrdlogSentDate': 'HRDLog sent date', 'fltb.fHamqthSent': 'Statut envoi HamQTH', 'fltb.fHamqthSentDate': 'Date denvoi HamQTH', 'bulk.fHamqthSent': 'Statut envoi HamQTH', 'bulk.fHamqthSentDate': 'Date denvoi HamQTH', 'rqg.c.hamqth_sent': 'HamQTH env', 'rqg.h.hamqth_sent': 'HamQTH env.', 'rqg.c.hamqth_sent_date': 'Date env HamQTH', 'rqg.h.hamqth_sent_date': 'HamQTH env.', 'fltb.fHamlogSent': 'HAMLOG.online envoyé', 'fltb.fHamlogSentDate': "HAMLOG.online date d'envoi", 'fltb.fHamlogRcvd': 'HAMLOG.online reçu', 'fltb.fHamlogRcvdDate': 'HAMLOG.online date de réception', 'fltb.fContestId': 'Contest ID', 'fltb.fSerialRcvd': 'Serial rcvd', 'fltb.fSerialSent': 'Serial sent', 'fltb.fPropMode': 'Propagation mode', 'fltb.fSatellite': 'Satellite', 'fltb.fMyCallsign': 'Station callsign (my call)', 'fltb.fOperator': 'Operator', 'fltb.fOwnerCallsign': 'Owner callsign', 'fltb.fMyGrid': 'My grid', 'fltb.fMyCountry': 'My country', 'fltb.fMyState': 'My state', 'fltb.fMyCounty': 'My county', 'fltb.fMyIota': 'My IOTA', 'fltb.fMySota': 'My SOTA ref', 'fltb.fMyPota': 'My POTA ref', 'fltb.fMyWwff': 'My WWFF ref', 'fltb.fMyStreet': 'My street', 'fltb.fMyCity': 'My city', 'fltb.fMyPostal': 'My postal code', 'fltb.fMyRig': 'My rig', 'fltb.fMyAntenna': 'My antenna', 'fltb.fTxPower': 'TX power (W)', 'fltb.fComment': 'Comment', 'fltb.fNotes': 'Notes', 'fltb.opEq': 'égal (=)', 'fltb.opNe': 'différent (≠)', 'fltb.opContains': 'contient', 'fltb.opStartsWith': 'commence par', 'fltb.opEndsWith': 'finit par', 'fltb.opGt': 'supérieur à (>)', 'fltb.opLt': 'inférieur à (<)', 'fltb.opGe': 'supérieur ou égal (≥)', 'fltb.opLe': 'inférieur ou égal (≤)', 'fltb.opEmpty': 'est vide', 'fltb.opIn': 'est parmi', 'fltb.opNotIn': 'n est pas parmi', 'fltb.listPh': '2m, 70cm — séparés par des virgules', 'fltb.opNotEmpty': "n'est pas vide", 'fltb.title': 'Filtre QSO', 'fltb.match': 'Correspondance', 'fltb.all': 'TOUS (ET)', 'fltb.any': 'AU MOINS UN (OU)', 'fltb.loadPreset': 'Charger un préréglage…', 'fltb.noConditions': 'Aucune condition — la liste affiche tous les QSO. Ajoutes-en une ci-dessous.', 'fltb.where': 'OÙ', 'fltb.valuePh': 'valeur', 'fltb.remove': 'Retirer', 'fltb.addCondition': 'Ajouter une condition', 'fltb.presetNamePh': 'Nom du préréglage…', 'fltb.presetSaved': 'Filtre « {name} » enregistré', 'fltb.savePreset': 'Enregistrer le préréglage', 'fltb.clear': 'Effacer', 'fltb.cancel': 'Annuler', 'fltb.applyClose': 'Appliquer & fermer',
'detp.propAS': 'Diffusion par avion', 'detp.propAUR': 'Aurore', 'detp.propAUE': 'Aurore-E', 'detp.propBS': 'Rétrodiffusion', 'detp.propEME': 'Terre-Lune-Terre', 'detp.propES': 'Sporadique E', 'detp.propFAI': 'Irrégularités alignées au champ', 'detp.propF2': 'Réflexion F2', 'detp.propGWAVE': 'Onde de sol', 'detp.propINTERNET': 'Assisté par Internet', 'detp.propION': 'Diffusion ionosphérique', 'detp.propLOS': 'Vue directe', 'detp.propMS': 'Diffusion météoritique', 'detp.propRPT': 'Répéteur terrestre / atmosphérique', 'detp.propRS': 'Diffusion par la pluie', 'detp.propSAT': 'Satellite', 'detp.propTEP': 'Trans-équatorial', 'detp.propTR': 'Conduit troposphérique', 'detp.pathShort': 'Chemin court', 'detp.pathLong': 'Chemin long', 'detp.pathGrayline': 'Ligne grise', 'detp.pathOther': 'Autre', 'detp.tabStats': 'Stats', 'detp.tabInfo': 'Info', 'detp.tabAwards': 'Diplômes', 'detp.tabMy': 'Moi', 'detp.tabExtended': 'Étendu', 'detp.statePref': 'État / préf', 'detp.county': 'Comté', 'detp.newCounty': 'NOUV', 'detp.newCountyTip': 'Comté jamais contacté', 'detp.prefix': 'Préfixe', 'detp.cqZone': 'CQ', 'detp.ituZone': 'ITU', 'detp.dxcc': 'DXCC #', 'detp.azimuthLp': 'Azimut LP', 'detp.distanceSp': 'Distance SP', 'detp.distanceLp': 'Distance LP', 'detp.address': 'Adresse', 'detp.qslMessage': 'Message QSL', 'detp.qslVia': 'QSL via (manager)', 'detp.detected': 'Détecté — ce contact comptera pour :', 'detp.ambiguous': 'Ambigu — choisissez :', 'detp.azimuth': 'Azimut (°)', 'detp.elevation': 'Élévation (°)', 'detp.txPower': 'Puissance TX (W)', 'detp.satelliteMode': 'Mode satellite', 'detp.antPath': 'Chemin ant.', 'detp.propagation': 'Propagation', 'detp.rig': 'Station', 'detp.antenna': 'Antenne', 'detp.satName': 'Nom du satellite', 'detp.contestId': 'ID contest', 'detp.rcvdExchangePh': 'échange reçu', 'detp.sentExchangePh': 'échange envoyé', 'detp.contactedEmail': 'E-mail du contact', 'detp.contactedWeb': 'Site web', 'detp.propAS': 'Diffusion par avion', 'detp.propAUR': 'Aurore', 'detp.propAUE': 'Aurore-E', 'detp.propBS': 'Rétrodiffusion', 'detp.propEME': 'Terre-Lune-Terre', 'detp.propES': 'Sporadique E', 'detp.propFAI': 'Irrégularités alignées au champ', 'detp.propF2': 'Réflexion F2', 'detp.propGWAVE': 'Onde de sol', 'detp.propINTERNET': 'Assisté par Internet', 'detp.propION': 'Diffusion ionosphérique', 'detp.propLOS': 'Vue directe', 'detp.propMS': 'Diffusion météoritique', 'detp.propRPT': 'Répéteur terrestre / atmosphérique', 'detp.propRS': 'Diffusion par la pluie', 'detp.propSAT': 'Satellite', 'detp.propTEP': 'Trans-équatorial', 'detp.propTR': 'Conduit troposphérique', 'detp.pathShort': 'Chemin court', 'detp.pathLong': 'Chemin long', 'detp.pathGrayline': 'Ligne grise', 'detp.pathOther': 'Autre', 'detp.tabStats': 'Stats', 'detp.tabInfo': 'Info', 'detp.tabAwards': 'Diplômes', 'detp.tabMy': 'Moi', 'detp.tabExtended': 'Étendu', 'detp.statePref': 'État / préf', 'detp.county': 'Comté', 'detp.newCounty': 'NOUV', 'detp.newCountyTip': 'Comté jamais contacté', 'detp.prefix': 'Préfixe', 'detp.cqZone': 'CQ', 'detp.ituZone': 'ITU', 'detp.dxcc': 'DXCC #', 'detp.azimuthLp': 'Azimut LP', 'detp.distanceSp': 'Distance SP', 'detp.distanceLp': 'Distance LP', 'detp.address': 'Adresse', 'detp.qslMessage': 'Message QSL', 'detp.qslVia': 'QSL via (manager)', 'detp.detected': 'Détecté — ce contact comptera pour :', 'detp.ambiguous': 'Ambigu — choisissez :', 'detp.azimuth': 'Azimut (°)', 'detp.elevation': 'Élévation (°)', 'detp.txPower': 'Puissance TX (W)', 'detp.satelliteMode': 'Mode satellite', 'detp.antPath': 'Chemin ant.', 'detp.propagation': 'Propagation', 'detp.rig': 'Station', 'detp.antenna': 'Antenne', 'detp.satName': 'Nom du satellite', 'detp.contestId': 'ID contest', 'detp.rcvdExchangePh': 'échange reçu', 'detp.sentExchangePh': 'échange envoyé', 'detp.contactedEmail': 'E-mail du contact', 'detp.contactedWeb': 'Site web',
'awrp.remove': 'Retirer', 'awrp.searchLabel': 'Rechercher {label}…', 'awrp.searching': 'Recherche…', 'awrp.noMatch': 'Aucune correspondance.', 'awrp.noMatchDxcc': 'Aucune correspondance pour ce DXCC.', 'awrp.remove': 'Retirer', 'awrp.searchLabel': 'Rechercher {label}…', 'awrp.searching': 'Recherche…', 'awrp.noMatch': 'Aucune correspondance.', 'awrp.noMatchDxcc': 'Aucune correspondance pour ce DXCC.',
+4
View File
@@ -648,6 +648,8 @@ export function GetWsjtFollowMode():Promise<boolean>;
export function GetWsjtHighlight():Promise<boolean>; export function GetWsjtHighlight():Promise<boolean>;
export function GetWsjtHighlightColours():Promise<main.WsjtHighlightColours>;
export function GetWsjtHighlightWorked():Promise<boolean>; export function GetWsjtHighlightWorked():Promise<boolean>;
export function GetYaesuBandAntennas():Promise<Record<string, number>>; export function GetYaesuBandAntennas():Promise<Record<string, number>>;
@@ -1308,6 +1310,8 @@ export function SetWsjtFollowMode(arg1:boolean):Promise<void>;
export function SetWsjtHighlight(arg1:boolean):Promise<void>; export function SetWsjtHighlight(arg1:boolean):Promise<void>;
export function SetWsjtHighlightColours(arg1:main.WsjtHighlightColours):Promise<void>;
export function SetWsjtHighlightWorked(arg1:boolean):Promise<void>; export function SetWsjtHighlightWorked(arg1:boolean):Promise<void>;
export function SetYaesuAFGain(arg1:number):Promise<void>; export function SetYaesuAFGain(arg1:number):Promise<void>;
+8
View File
@@ -1230,6 +1230,10 @@ export function GetWsjtHighlight() {
return window['go']['main']['App']['GetWsjtHighlight'](); return window['go']['main']['App']['GetWsjtHighlight']();
} }
export function GetWsjtHighlightColours() {
return window['go']['main']['App']['GetWsjtHighlightColours']();
}
export function GetWsjtHighlightWorked() { export function GetWsjtHighlightWorked() {
return window['go']['main']['App']['GetWsjtHighlightWorked'](); return window['go']['main']['App']['GetWsjtHighlightWorked']();
} }
@@ -2550,6 +2554,10 @@ export function SetWsjtHighlight(arg1) {
return window['go']['main']['App']['SetWsjtHighlight'](arg1); return window['go']['main']['App']['SetWsjtHighlight'](arg1);
} }
export function SetWsjtHighlightColours(arg1) {
return window['go']['main']['App']['SetWsjtHighlightColours'](arg1);
}
export function SetWsjtHighlightWorked(arg1) { export function SetWsjtHighlightWorked(arg1) {
return window['go']['main']['App']['SetWsjtHighlightWorked'](arg1); return window['go']['main']['App']['SetWsjtHighlightWorked'](arg1);
} }
+18
View File
@@ -4592,6 +4592,24 @@ export namespace main {
return a; return a;
} }
} }
export class WsjtHighlightColours {
watchlist: string;
new_dxcc: string;
new_band: string;
worked: string;
static createFrom(source: any = {}) {
return new WsjtHighlightColours(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.watchlist = source["watchlist"];
this.new_dxcc = source["new_dxcc"];
this.new_band = source["new_band"];
this.worked = source["worked"];
}
}
} }
+44 -1
View File
@@ -134,6 +134,23 @@ type Client struct {
// clears rather than latching the TX-inhibit on for ever. // clears rather than latching the TX-inhibit on for ever.
const ubMoveOptimisticWindow = 3 * time.Second const ubMoveOptimisticWindow = 3 * time.Second
// Poll cadences: what an idle antenna is worth, and what a moving one is worth.
// See the poll loop.
const (
ubPollIdle = 2 * time.Second
ubPollMoving = 250 * time.Millisecond
)
// movingOptimistically reports whether a move was commanded so recently that the
// antenna cannot have answered yet — the same window GetStatus reports motion
// for, so the fast poll starts with the movement rather than one interval into
// it.
func (c *Client) movingOptimistically() bool {
c.statusMu.RLock()
defer c.statusMu.RUnlock()
return !c.moveCmdAt.IsZero() && time.Since(c.moveCmdAt) < ubMoveOptimisticWindow
}
// LastSetKHz returns the frequency (kHz) most recently commanded to the antenna, // LastSetKHz returns the frequency (kHz) most recently commanded to the antenna,
// or 0 if none yet. // or 0 if none yet.
func (c *Client) LastSetKHz() int { func (c *Client) LastSetKHz() int {
@@ -312,8 +329,22 @@ func (c *Client) pollLoop() {
close(c.done) close(c.done)
} }
}() }()
ticker := time.NewTicker(2 * time.Second) // Increased from 500ms to 2s // TWO CADENCES, and the fast one is what matters.
//
// An idle antenna has nothing to say, so two seconds is generous. A MOVING
// one has one thing to say and it is urgent: transmit is inhibited while the
// elements travel, so every poll interval between the motors stopping and
// this loop noticing is a second the operator cannot call — with the antenna
// already in place and the rig still gagged. Reported from the air as "it
// stays orange one or two seconds after it has finished".
//
// While the motors run — or a move has just been commanded — the antenna is
// asked four times a second. The controller answers a status query in
// milliseconds; this is nothing to it, and it lasts only as long as the
// movement does.
ticker := time.NewTicker(ubPollIdle)
defer ticker.Stop() defer ticker.Stop()
fast := false
pollCount := 0 pollCount := 0
pollFails := 0 // consecutive failed status polls (transient timeouts tolerated) pollFails := 0 // consecutive failed status polls (transient timeouts tolerated)
@@ -436,6 +467,18 @@ func (c *Client) pollLoop() {
c.lastStatus = status c.lastStatus = status
c.statusMu.Unlock() c.statusMu.Unlock()
// Follow the motors with the poll rate. Changed only when it actually
// changes: Reset on a ticker is cheap but not free, and the antenna is
// polled for hours on end.
if moving := status.MotorsMoving != 0 || c.movingOptimistically(); moving != fast {
fast = moving
if fast {
ticker.Reset(ubPollMoving)
} else {
ticker.Reset(ubPollIdle)
}
}
case <-c.stopChan: case <-c.stopChan:
return return
} }
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"testing"
udp "hamlog/internal/integrations/udp"
)
// The text colour is derived from the background, never stored: an operator who
// picks a dark blue must not end up with black text on it in somebody else's
// window and conclude the feature is broken.
func TestHighlightForegroundFollowsTheBackground(t *testing.T) {
var a App
for _, tc := range []struct {
bg string
want udp.RGB
}{
{"#111827", hlWhite}, // near-black
{"#16823C", hlWhite}, // the new-DXCC green
{"#F472B6", hlBlack}, // the watchlist pink
{"#FFFFFF", hlBlack},
{"#E27A18", hlBlack}, // orange
} {
bg, fg := a.colourFor("", tc.bg)
if got, ok := parseHexRGB(tc.bg); !ok || got != bg {
t.Errorf("%s: background came back as %v", tc.bg, bg)
}
if fg != tc.want {
t.Errorf("%s: foreground %v, want %v", tc.bg, fg, tc.want)
}
}
}
// A colour that cannot be read is refused rather than half-read: silently
// becoming black is worse than keeping the default.
func TestHighlightColourParsing(t *testing.T) {
for _, s := range []string{"#F472B6", "f472b6", " #F472B6 "} {
if c, ok := parseHexRGB(s); !ok || c != (udp.RGB{R: 244, G: 114, B: 182}) {
t.Errorf("%q → %v ok=%v", s, c, ok)
}
}
for _, s := range []string{"", "#FFF", "pink", "#GGGGGG", "#F472B6F"} {
if _, ok := parseHexRGB(s); ok {
t.Errorf("%q was accepted", s)
}
}
}