Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3014796c1d | ||
|
|
405b0e24cf |
@@ -716,6 +716,11 @@ type App struct {
|
|||||||
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
|
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
|
||||||
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
|
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
|
||||||
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
||||||
|
// qsoRecPushed records which SOURCE the recorder was started on: the network
|
||||||
|
// stream, or a sound device. Switching CAT backends changes the answer, and a
|
||||||
|
// recorder left capturing a device that no longer carries the radio records
|
||||||
|
// silence without saying so.
|
||||||
|
qsoRecPushed bool
|
||||||
motorStartMu sync.Mutex // serialises startUltrabeam: two restarts at once left two poll loops on one COM port
|
motorStartMu sync.Mutex // serialises startUltrabeam: two restarts at once left two poll loops on one COM port
|
||||||
motorInhibStop chan struct{} // stops the "inhibit TX while moving" 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)
|
motorMoveCmdNs atomic.Int64 // unixnano of the last commanded antenna move (grace window)
|
||||||
@@ -8321,7 +8326,19 @@ func (a *App) startQSORecorderIfEnabled() {
|
|||||||
if !cfg.QSORecord {
|
if !cfg.QSORecord {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := a.qsoRec.Start(cfg.FromRadio, cfg.RecordingDevice, cfg.PrerollSeconds); err != nil {
|
// Over the network there IS no radio sound device.
|
||||||
|
//
|
||||||
|
// An IC-705 or IC-7610 reached by LAN streams its receive audio through the
|
||||||
|
// Icom protocol, and Windows sees nothing: the operator's audio settings can
|
||||||
|
// only offer the PC's own microphone and speakers, so "From radio" has
|
||||||
|
// nothing right to point at. The stream is pushed into the recorder instead
|
||||||
|
// — same samples, no sound card in the middle, and no virtual cable to set up.
|
||||||
|
from := cfg.FromRadio
|
||||||
|
a.qsoRecPushed = a.icomNetAudioActive()
|
||||||
|
if a.qsoRecPushed {
|
||||||
|
from = audio.PushedSource
|
||||||
|
}
|
||||||
|
if err := a.qsoRec.Start(from, cfg.RecordingDevice, cfg.PrerollSeconds); err != nil {
|
||||||
applog.Printf("qso-rec: start failed: %v", err)
|
applog.Printf("qso-rec: start failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -8336,6 +8353,17 @@ func (a *App) startQSORecorderIfEnabled() {
|
|||||||
applog.Printf("qso-rec: running (preroll %ds, mix=%v, gains rx=%.2f mic=%.2f)", cfg.PrerollSeconds, cfg.RecordingDevice != "" && cfg.RecordingDevice != cfg.FromRadio, fromGain, micGain)
|
applog.Printf("qso-rec: running (preroll %ds, mix=%v, gains rx=%.2f mic=%.2f)", cfg.PrerollSeconds, cfg.RecordingDevice != "" && cfg.RecordingDevice != cfg.FromRadio, fromGain, micGain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// icomNetAudioActive reports whether the receive audio is arriving over the
|
||||||
|
// network rather than from a sound card — the Icom LAN backend with its audio
|
||||||
|
// option on.
|
||||||
|
func (a *App) icomNetAudioActive() bool {
|
||||||
|
s, err := a.GetCATSettings()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.Enabled && s.Backend == "icom-net" && s.IcomNetAudio
|
||||||
|
}
|
||||||
|
|
||||||
// qsoRecDir returns the configured recordings folder, defaulting to
|
// qsoRecDir returns the configured recordings folder, defaulting to
|
||||||
// <dataDir>/Recordings, and ensures it exists.
|
// <dataDir>/Recordings, and ensures it exists.
|
||||||
func (a *App) qsoRecDir() string {
|
func (a *App) qsoRecDir() string {
|
||||||
@@ -14892,6 +14920,13 @@ func (a *App) reloadCAT() {
|
|||||||
a.catFlexDecodeSpots = s.Enabled && s.Backend == "flex" && s.FlexDecodeSpots
|
a.catFlexDecodeSpots = s.Enabled && s.Backend == "flex" && s.FlexDecodeSpots
|
||||||
a.catFlexDecodeSecs = s.FlexDecodeSecs
|
a.catFlexDecodeSecs = s.FlexDecodeSecs
|
||||||
a.catFlexDVKDax = s.Enabled && s.Backend == "flex" && s.FlexDVKDax
|
a.catFlexDVKDax = s.Enabled && s.Backend == "flex" && s.FlexDVKDax
|
||||||
|
// The recorder's source depends on the backend: over the Icom LAN there is no
|
||||||
|
// sound card to capture, the audio is pushed in. Restarted only when the
|
||||||
|
// answer CHANGES, so an ordinary settings save never interrupts a recording.
|
||||||
|
if want := s.Enabled && s.Backend == "icom-net" && s.IcomNetAudio; want != a.qsoRecPushed {
|
||||||
|
applog.Printf("qso-rec: audio source changes (network=%v) — restarting the recorder", want)
|
||||||
|
go a.startQSORecorderIfEnabled()
|
||||||
|
}
|
||||||
a.reloadCATShare(s)
|
a.reloadCATShare(s)
|
||||||
// Nothing about the link changed → leave it connected. See catLinkSig.
|
// Nothing about the link changed → leave it connected. See catLinkSig.
|
||||||
if sig := catLinkSig(s); sig == a.catSig {
|
if sig := catLinkSig(s); sig == a.catSig {
|
||||||
@@ -15000,8 +15035,16 @@ func (a *App) reloadCAT() {
|
|||||||
} else {
|
} else {
|
||||||
codec := audio.NewPCM16Codec()
|
codec := audio.NewPCM16Codec()
|
||||||
audioSink = func(payload []byte) {
|
audioSink = func(payload []byte) {
|
||||||
if pcm, err := codec.Decode(payload); err == nil {
|
pcm, err := codec.Decode(payload)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
a.audioMgr.PushMonitorAudio(pcm)
|
a.audioMgr.PushMonitorAudio(pcm)
|
||||||
|
// And to the QSO recorder, which has no device to capture from
|
||||||
|
// on this backend. It drops the samples unless a recording is
|
||||||
|
// actually running, so this costs a function call when idle.
|
||||||
|
if a.qsoRec != nil {
|
||||||
|
a.qsoRec.PushRX(pcm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applog.Printf("icom-net audio: RX audio streaming ENABLED (experimental) → %q", acfg.ListeningDevice)
|
applog.Printf("icom-net audio: RX audio streaming ENABLED (experimental) → %q", acfg.ListeningDevice)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var deniedCallHashes = map[string]struct{}{
|
|||||||
"d1ae6212ec057f9d5c6f379fb57acedac7c94142c9d49667dc04b2016d03d1b6": {},
|
"d1ae6212ec057f9d5c6f379fb57acedac7c94142c9d49667dc04b2016d03d1b6": {},
|
||||||
"0741c9e394b42f43191899105553b47155ddc3026da12b5360701f9c181ff123": {},
|
"0741c9e394b42f43191899105553b47155ddc3026da12b5360701f9c181ff123": {},
|
||||||
"ab4926a3a0ab76d41b5b99cd3ad0683584970c341c29427c1dfa4b3c329ce415": {},
|
"ab4926a3a0ab76d41b5b99cd3ad0683584970c341c29427c1dfa4b3c329ce415": {},
|
||||||
|
"9d17c9c213a6cc89c12d7520bcf21c86a0cf43d17e82e74f33f3a77cb865d28a": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// callDenied reports whether a callsign is on deniedCallHashes. The call is
|
// callDenied reports whether a callsign is on deniedCallHashes. The call is
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.26.7",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"The NEW badge on a county now shows in the entry form itself, inside the field, and not only on the Info tab — it answers the question at the moment the operator is deciding whether to call.",
|
||||||
|
"OmniRig: a Yaesu is no longer sent OmniRig's SetSimplexMode when tuning. It is a no-op on some (an FT-891 never moved) and harmful on others — on an FT-2000 it turned split on and moved reception to VFO B, which is why OpsLog then showed B instead of A. The direct frequency write, which is what tunes these rigs, is unchanged.",
|
||||||
|
"The basemap buttons on the main map are shifted clear of the zoom controls — Light sat a few pixels from the minus button and was being clicked by mistake.",
|
||||||
|
"Cluster, French interface: the WKD CALL status now reads DÉJÀ QSO instead of DÉJÀ CTC.",
|
||||||
|
"Icom over LAN: the receive audio streamed by the rig now feeds the QSO recorder as well as the speakers. On a network connection Windows sees no radio sound card at all — the audio settings could only offer the PC microphone — so the stream is pushed straight into the recorder, with no virtual cable to set up."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Le badge NOUV du comté s'affiche maintenant dans le formulaire de saisie lui-même, à l'intérieur du champ, et plus seulement dans l'onglet Info — il répond au moment où l'opérateur décide d'appeler ou non.",
|
||||||
|
"OmniRig : le SetSimplexMode d'OmniRig n'est plus envoyé aux Yaesu lors d'un changement de fréquence. Il est sans effet sur certains (un FT-891 ne bougeait pas) et nuisible sur d'autres — sur un FT-2000 il activait le split et faisait passer la réception sur le VFO B, d'où l'affichage du B au lieu du A. L'écriture directe de la fréquence, qui est ce qui accorde réellement ces postes, ne change pas.",
|
||||||
|
"Les boutons de fond de carte sont décalés à l'écart des commandes de zoom — Light était à quelques pixels du bouton moins et se faisait cliquer par erreur.",
|
||||||
|
"Cluster, interface française : le statut « DÉJÀ CTC » devient « DÉJÀ QSO ».",
|
||||||
|
"Icom en réseau : l'audio reçu diffusé par le poste alimente maintenant l'enregistreur de QSO en plus des haut-parleurs. Sur une liaison réseau, Windows ne voit aucune carte son de la radio — les réglages audio ne proposaient que le micro du PC — donc le flux est injecté directement dans l'enregistreur, sans câble virtuel à installer."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.26.6",
|
"version": "0.26.6",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
+14
-2
@@ -5214,9 +5214,21 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
const geoDetailRow = (
|
const geoDetailRow = (
|
||||||
<div className="flex gap-4 items-end">
|
<div className="flex gap-4 items-end">
|
||||||
<div className="flex flex-col flex-1 min-w-0"><Label className="mb-1 h-3.5">{t('field.cnty')}</Label>
|
{/* The badge sits INSIDE the field: this row is already tight, and a
|
||||||
<Input value={details.cnty ?? ''}
|
county name is short enough to leave room at its right. It answers the
|
||||||
|
question at the moment the operator is deciding whether to call — the
|
||||||
|
same answer the Info tab gives, one tab away from where they are. */}
|
||||||
|
<div className="flex flex-col flex-1 min-w-0 relative"><Label className="mb-1 h-3.5">{t('field.cnty')}</Label>
|
||||||
|
<Input value={details.cnty ?? ''} className={cn(entryNewCounty && 'pr-12')}
|
||||||
onChange={(e) => setDetails((d) => ({ ...d, cnty: e.target.value }))} />
|
onChange={(e) => setDetails((d) => ({ ...d, cnty: e.target.value }))} />
|
||||||
|
{entryNewCounty && (
|
||||||
|
<span
|
||||||
|
title={t('detp.newCountyTip')}
|
||||||
|
className="absolute right-1.5 bottom-1.5 rounded px-1.5 py-0.5 text-[10px] font-bold tracking-wide bg-success text-success-foreground pointer-events-none"
|
||||||
|
>
|
||||||
|
{t('detp.newCounty')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-[64px] shrink-0"><Label className="mb-1 h-3.5">{t('field.cqz')}</Label>
|
<div className="flex flex-col w-[64px] shrink-0"><Label className="mb-1 h-3.5">{t('field.cqz')}</Label>
|
||||||
<Input value={details.cqz ?? ''} className="font-mono"
|
<Input value={details.cqz ?? ''} className="font-mono"
|
||||||
|
|||||||
@@ -366,7 +366,10 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
|
|||||||
<div className="relative isolate h-full w-full rounded-lg overflow-hidden border border-border">
|
<div className="relative isolate h-full w-full rounded-lg overflow-hidden border border-border">
|
||||||
<div ref={worldRef} className="absolute inset-0" />
|
<div ref={worldRef} className="absolute inset-0" />
|
||||||
{/* Basemap picker — Light / Street / Satellite (key-free tiles). */}
|
{/* Basemap picker — Light / Street / Satellite (key-free tiles). */}
|
||||||
<div className="absolute top-1 left-12 z-[500] flex rounded-md overflow-hidden shadow border border-border backdrop-blur">
|
{/* Clear of the zoom buttons, not merely next to them: at left-12 the
|
||||||
|
first basemap sat a few pixels from the − button and operators kept
|
||||||
|
hitting Light when they meant to zoom out. */}
|
||||||
|
<div className="absolute top-1 left-[4.5rem] z-[500] flex rounded-md overflow-hidden shadow border border-border backdrop-blur">
|
||||||
{(Object.keys(BASEMAPS) as BasemapKey[]).map((k) => (
|
{(Object.keys(BASEMAPS) as BasemapKey[]).map((k) => (
|
||||||
<button
|
<button
|
||||||
key={k}
|
key={k}
|
||||||
|
|||||||
@@ -957,7 +957,7 @@ const fr: Dict = {
|
|||||||
'wbg.awardTip': '{name} — référence comptée pour ce QSO', 'wbg.typeCall': 'Saisissez un indicatif dans la barre pour voir les contacts précédents.', 'wbg.checking': 'vérification…', 'wbg.new': 'NOUVEAU', 'wbg.noPriorPre': 'Aucun QSO précédent avec ', 'wbg.noPriorPost': '.', 'wbg.workedBefore': 'Déjà contacté', 'wbg.first': 'Premier :', 'wbg.last': 'Dernier :', 'wbg.dxcc': 'DXCC :', 'wbg.entityQsos': '{n} QSO avec cette entité', 'wbg.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'wbg.clearFilters': 'Effacer les filtres', 'wbg.columns': 'Colonnes', 'wbg.olderQsos': '+ {n} QSO plus anciens (non affichés — limités pour la performance)', 'wbg.pickerTitle': 'Colonnes « Déjà contacté »', 'wbg.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau « Déjà contacté ».', 'wbg.allGroups': 'Tous les groupes :', 'wbg.all': 'tout', 'wbg.none': 'aucun', 'wbg.grpAwards': 'Diplômes', 'wbg.resetDefaults': 'Réinitialiser', 'wbg.done': 'Terminé',
|
'wbg.awardTip': '{name} — référence comptée pour ce QSO', 'wbg.typeCall': 'Saisissez un indicatif dans la barre pour voir les contacts précédents.', 'wbg.checking': 'vérification…', 'wbg.new': 'NOUVEAU', 'wbg.noPriorPre': 'Aucun QSO précédent avec ', 'wbg.noPriorPost': '.', 'wbg.workedBefore': 'Déjà contacté', 'wbg.first': 'Premier :', 'wbg.last': 'Dernier :', 'wbg.dxcc': 'DXCC :', 'wbg.entityQsos': '{n} QSO avec cette entité', 'wbg.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'wbg.clearFilters': 'Effacer les filtres', 'wbg.columns': 'Colonnes', 'wbg.olderQsos': '+ {n} QSO plus anciens (non affichés — limités pour la performance)', 'wbg.pickerTitle': 'Colonnes « Déjà contacté »', 'wbg.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau « Déjà contacté ».', 'wbg.allGroups': 'Tous les groupes :', 'wbg.all': 'tout', 'wbg.none': 'aucun', 'wbg.grpAwards': 'Diplômes', 'wbg.resetDefaults': 'Réinitialiser', 'wbg.done': 'Terminé',
|
||||||
'chn.title': 'Chasse au nouveau', 'chn.close': 'Masquer le panneau', 'chn.filterHint': 'Afficher ou masquer ce type', 'chn.allFiltered': 'Tout ce qui est entendu est filtré — réactivez une catégorie ci-dessus.', 'chn.toggle': 'Chasse au nouveau', 'chn.count': '{n} entendus', 'chn.loading': 'En attente du flux…', 'chn.empty': 'Rien de nouveau décodé près de vous pour le moment.', 'chn.digitalOnly': 'PSK Reporter — modes numériques uniquement, entendus à moins de ~300 km.', 'chn.option': 'Chasse au nouveau (PSK Reporter)', 'chn.optionHelp': 'Liste les stations décodées près de chez vous qui sont nouvelles par rapport à votre log — entité, bande, mode, créneau, préfixe ou carré. Modes numériques uniquement.', 'chn.show': 'Panneau chasse au nouveau',
|
'chn.title': 'Chasse au nouveau', 'chn.close': 'Masquer le panneau', 'chn.filterHint': 'Afficher ou masquer ce type', 'chn.allFiltered': 'Tout ce qui est entendu est filtré — réactivez une catégorie ci-dessus.', 'chn.toggle': 'Chasse au nouveau', 'chn.count': '{n} entendus', 'chn.loading': 'En attente du flux…', 'chn.empty': 'Rien de nouveau décodé près de vous pour le moment.', 'chn.digitalOnly': 'PSK Reporter — modes numériques uniquement, entendus à moins de ~300 km.', 'chn.option': 'Chasse au nouveau (PSK Reporter)', 'chn.optionHelp': 'Liste les stations décodées près de chez vous qui sont nouvelles par rapport à votre log — entité, bande, mode, créneau, préfixe ou carré. Modes numériques uniquement.', 'chn.show': 'Panneau chasse au nouveau',
|
||||||
'clg2.allFiltered': '{n} spots reçus, aucun affiché — vos filtres les masquent tous.', 'clg2.activeFilters': 'Actifs :', 'clg2.clearAllFilters': 'Effacer tous les filtres', 'clg2.fBandLock': 'bande verrouillée sur la radio', 'clg2.fBands': 'bandes {list}', 'clg2.fModeLock': 'mode verrouillé sur la radio', 'clg2.fModes': 'modes {list}', 'clg2.fStatus': 'pastilles de statut', 'clg2.fHideWorked': 'masquer les contactés', 'clg2.fLotwOnly': 'utilisateurs LoTW uniquement', 'clg2.fSpotterCont': 'continent du spotteur', 'clg2.fSource': 'un seul nœud source', 'clg2.fSearch': 'recherche « {q} »',
|
'clg2.allFiltered': '{n} spots reçus, aucun affiché — vos filtres les masquent tous.', 'clg2.activeFilters': 'Actifs :', 'clg2.clearAllFilters': 'Effacer tous les filtres', 'clg2.fBandLock': 'bande verrouillée sur la radio', 'clg2.fBands': 'bandes {list}', 'clg2.fModeLock': 'mode verrouillé sur la radio', 'clg2.fModes': 'modes {list}', 'clg2.fStatus': 'pastilles de statut', 'clg2.fHideWorked': 'masquer les contactés', 'clg2.fLotwOnly': 'utilisateurs LoTW uniquement', 'clg2.fSpotterCont': 'continent du spotteur', 'clg2.fSource': 'un seul nœud source', 'clg2.fSearch': 'recherche « {q} »',
|
||||||
'clg2.c.time': 'Heure', 'clg2.c.call': 'Indicatif', 'clg2.c.status': 'Statut', 'clg2.c.pota': 'POTA', 'clg2.c.freq': 'Fréq', 'clg2.c.band': 'Bande', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Préf.', 'clg2.c.cqz': 'Zone CQ', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'Zone ITU', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Chemin court (°)', 'clg2.h.sp_deg': 'CC°', 'clg2.c.lp_deg': 'Chemin long (°)', 'clg2.h.lp_deg': 'CL°', 'clg2.c.country': 'Pays', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Locator du spotter', 'clg2.h.locator': 'Loc spotter', 'clg2.c.county': 'Comté US', 'clg2.tipNewCounty': 'NOUVEAU COMTÉ — jamais contacté', 'clg2.tipNewPfx': "NOUVEAU PRÉFIXE — ce préfixe WPX n'a jamais été contacté", 'clg2.c.comment': 'Commentaire', 'clg2.c.received_at': 'Reçu le', 'clg2.h.received_at': 'Reçu UTC', 'clg2.c.raw': 'Brut', 'clg2.newDxcc': 'NOUV DXCC', 'clg2.newBandMode': 'NOUV B+M', 'clg2.newBand': 'NOUV BANDE', 'clg2.newMode': 'NOUV MODE', 'clg2.newSlot': 'NOUV SLOT', 'clg2.newCall': 'CALL NEUF', 'clg2.wkdCall': 'DÉJÀ CTC', 'clg2.newCounty': 'NOUV CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newGrid': 'NOUV GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NOUVEAU GRID — ce carré n a jamais été contacté (grid entendu dans un CQ sur le lien UDP)', 'clg2.newPfx': "NOUVEAU PFX", 'clg2.newPota': 'NOUV POTA', 'clg2.tipNewDxcc': 'NOUVEAU DXCC : {country}', 'clg2.tipWorkedCall': 'Indicatif déjà contacté', 'clg2.tipNewBandMode': 'NOUVELLE BANDE ET NOUVEAU MODE pour cette entité — aucun des deux n’a été fait avec elle', 'clg2.tipNewBand': 'NOUVELLE BANDE pour cette entité', 'clg2.tipNewSlotBand': 'NOUVEAU SLOT (mode pas encore contacté sur cette bande)', 'clg2.tipNewMode': 'NOUVEAU MODE (ce mode jamais contacté sur cette entité)', 'clg2.tipNewSlot': 'NOUVEAU SLOT (cette bande+mode pas encore contactée)', 'clg2.tipNewCall': "CALL NEUF — cet indicatif n a jamais été contacté sur cette bande et ce mode (l entité, si)", 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Géo', 'clg2.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'clg2.clearFilters': 'Effacer les filtres', 'clg2.columns': 'Colonnes', 'clg2.pickerTitle': 'Colonnes du cluster', 'clg2.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau du cluster.', 'clg2.allGroups': 'Tous les groupes :', 'clg2.all': 'tout', 'clg2.none': 'aucun', 'clg2.resetDefaults': 'Réinitialiser', 'clg2.done': 'Terminé',
|
'clg2.c.time': 'Heure', 'clg2.c.call': 'Indicatif', 'clg2.c.status': 'Statut', 'clg2.c.pota': 'POTA', 'clg2.c.freq': 'Fréq', 'clg2.c.band': 'Bande', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Préf.', 'clg2.c.cqz': 'Zone CQ', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'Zone ITU', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Chemin court (°)', 'clg2.h.sp_deg': 'CC°', 'clg2.c.lp_deg': 'Chemin long (°)', 'clg2.h.lp_deg': 'CL°', 'clg2.c.country': 'Pays', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Locator du spotter', 'clg2.h.locator': 'Loc spotter', 'clg2.c.county': 'Comté US', 'clg2.tipNewCounty': 'NOUVEAU COMTÉ — jamais contacté', 'clg2.tipNewPfx': "NOUVEAU PRÉFIXE — ce préfixe WPX n'a jamais été contacté", 'clg2.c.comment': 'Commentaire', 'clg2.c.received_at': 'Reçu le', 'clg2.h.received_at': 'Reçu UTC', 'clg2.c.raw': 'Brut', 'clg2.newDxcc': 'NOUV DXCC', 'clg2.newBandMode': 'NOUV B+M', 'clg2.newBand': 'NOUV BANDE', 'clg2.newMode': 'NOUV MODE', 'clg2.newSlot': 'NOUV SLOT', 'clg2.newCall': 'CALL NEUF', 'clg2.wkdCall': 'DÉJÀ QSO', 'clg2.newCounty': 'NOUV CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newGrid': 'NOUV GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NOUVEAU GRID — ce carré n a jamais été contacté (grid entendu dans un CQ sur le lien UDP)', 'clg2.newPfx': "NOUVEAU PFX", 'clg2.newPota': 'NOUV POTA', 'clg2.tipNewDxcc': 'NOUVEAU DXCC : {country}', 'clg2.tipWorkedCall': 'Indicatif déjà contacté', 'clg2.tipNewBandMode': 'NOUVELLE BANDE ET NOUVEAU MODE pour cette entité — aucun des deux n’a été fait avec elle', 'clg2.tipNewBand': 'NOUVELLE BANDE pour cette entité', 'clg2.tipNewSlotBand': 'NOUVEAU SLOT (mode pas encore contacté sur cette bande)', 'clg2.tipNewMode': 'NOUVEAU MODE (ce mode jamais contacté sur cette entité)', 'clg2.tipNewSlot': 'NOUVEAU SLOT (cette bande+mode pas encore contactée)', 'clg2.tipNewCall': "CALL NEUF — cet indicatif n a jamais été contacté sur cette bande et ce mode (l entité, si)", 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Géo', 'clg2.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'clg2.clearFilters': 'Effacer les filtres', 'clg2.columns': 'Colonnes', 'clg2.pickerTitle': 'Colonnes du cluster', 'clg2.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau du cluster.', 'clg2.allGroups': 'Tous les groupes :', 'clg2.all': 'tout', 'clg2.none': 'aucun', 'clg2.resetDefaults': 'Réinitialiser', 'clg2.done': 'Terminé',
|
||||||
// Périphériques audio et manipulateur vocal (Préférences → Périphériques audio).
|
// Périphériques audio et manipulateur vocal (Préférences → Périphériques audio).
|
||||||
'aud.refreshDevices': 'Actualiser les périphériques', 'aud.fromRadio': 'Depuis la radio (entrée RX)', 'aud.toRadio': 'Vers la radio (sortie TX)', 'aud.recMic': "Micro d'enregistrement", 'aud.listening': 'Écoute (pré-écoute)',
|
'aud.refreshDevices': 'Actualiser les périphériques', 'aud.fromRadio': 'Depuis la radio (entrée RX)', 'aud.toRadio': 'Vers la radio (sortie TX)', 'aud.recMic': "Micro d'enregistrement", 'aud.listening': 'Écoute (pré-écoute)',
|
||||||
'aud.phFromRadio': 'Sortie audio du poste → entrée carte son', 'aud.phToRadio': 'Sortie carte son → entrée micro/data du poste', 'aud.phRecMic': 'Votre microphone (enregistrer les messages vocaux)', 'aud.phListening': 'Haut-parleurs locaux pour la pré-écoute',
|
'aud.phFromRadio': 'Sortie audio du poste → entrée carte son', 'aud.phToRadio': 'Sortie carte son → entrée micro/data du poste', 'aud.phRecMic': 'Votre microphone (enregistrer les messages vocaux)', 'aud.phListening': 'Haut-parleurs locaux pour la pré-écoute',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Single source of truth for the app version shown in the UI (header + About).
|
// Single source of truth for the app version shown in the UI (header + About).
|
||||||
// Bump this on a release (the release script updates it alongside telemetry.go).
|
// Bump this on a release (the release script updates it alongside telemetry.go).
|
||||||
export const APP_VERSION = '0.26.6';
|
export const APP_VERSION = '0.26.7';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
@@ -168,7 +168,48 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
|||||||
twoSrc := r.twoSrc
|
twoSrc := r.twoSrc
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
|
|
||||||
// Capture goroutine(s) feed the per-source queues.
|
// The radio side is either CAPTURED from a sound device or PUSHED in from
|
||||||
|
// somewhere else. Over the network there is no sound device at all: an
|
||||||
|
// IC-705 reached by LAN streams its receive audio over the Icom protocol,
|
||||||
|
// and the operator's audio settings can only offer the PC's own microphone
|
||||||
|
// and speakers. fromDev == PushedSource says "someone else will feed me",
|
||||||
|
// and PushRX is how they do it.
|
||||||
|
if fromDev == PushedSource {
|
||||||
|
LogSink("recorder: radio audio is pushed in (network), not captured from a device")
|
||||||
|
} else {
|
||||||
|
r.startRadioCapture(fromDev, stop)
|
||||||
|
}
|
||||||
|
if twoSrc {
|
||||||
|
r.startMicCapture(micDev, stop)
|
||||||
|
}
|
||||||
|
r.finishStart(fromDev, micDev, twoSrc, stop)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushedSource is the "device" name that means the radio audio arrives through
|
||||||
|
// PushRX instead of a capture stream.
|
||||||
|
const PushedSource = "\x00pushed"
|
||||||
|
|
||||||
|
// PushRX feeds one chunk of 16-bit mono PCM from a non-device source.
|
||||||
|
//
|
||||||
|
// Safe to call when nothing is recording — the samples are dropped, which is
|
||||||
|
// what should happen: the network stream runs whenever the rig is connected,
|
||||||
|
// and the recorder only wants it between BeginQSO and the save.
|
||||||
|
func (r *Recorder) PushRX(pcm []byte) {
|
||||||
|
r.mu.Lock()
|
||||||
|
running := r.running
|
||||||
|
r.mu.Unlock()
|
||||||
|
if !running || len(pcm) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sm := bytesToInt16(pcm)
|
||||||
|
r.srcMu.Lock()
|
||||||
|
r.bufA = append(r.bufA, sm...)
|
||||||
|
r.lastA = time.Now()
|
||||||
|
r.srcMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) startRadioCapture(fromDev string, stop chan struct{}) {
|
||||||
r.wg.Add(1)
|
r.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
@@ -187,7 +228,9 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
|||||||
LogSink("recorder: capture from %q failed: %v", DeviceName(fromDev), err)
|
LogSink("recorder: capture from %q failed: %v", DeviceName(fromDev), err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if twoSrc {
|
}
|
||||||
|
|
||||||
|
func (r *Recorder) startMicCapture(micDev string, stop chan struct{}) {
|
||||||
r.wg.Add(1)
|
r.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
@@ -202,8 +245,10 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
|||||||
LogSink("recorder: capture from %q failed: %v", DeviceName(micDev), err)
|
LogSink("recorder: capture from %q failed: %v", DeviceName(micDev), err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finishStart logs what is being recorded and arms the silence watchdog.
|
||||||
|
func (r *Recorder) finishStart(fromDev, micDev string, twoSrc bool, stop chan struct{}) {
|
||||||
// Name the devices being recorded FROM, once, at the start.
|
// Name the devices being recorded FROM, once, at the start.
|
||||||
//
|
//
|
||||||
// A station with two radios has two sets of endpoints, and the recorder will
|
// A station with two radios has two sets of endpoints, and the recorder will
|
||||||
@@ -262,7 +307,6 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mixTick drains the source queues, mixes what's available, and appends to the
|
// mixTick drains the source queues, mixes what's available, and appends to the
|
||||||
|
|||||||
@@ -500,10 +500,30 @@ func (o *OmniRig) SetFrequency(hz int64) error {
|
|||||||
// deliberately; a spot click is a request to change frequency, not to change
|
// deliberately; a spot click is a request to change frequency, not to change
|
||||||
// VFO. On SUB the direct FreqB write below does the whole job.
|
// VFO. On SUB the direct FreqB write below does the whole job.
|
||||||
onSubVFO := vfo == "B" || vfo == "BB" || vfo == "BA"
|
onSubVFO := vfo == "B" || vfo == "BB" || vfo == "BA"
|
||||||
|
// …and NEVER on a Yaesu.
|
||||||
|
//
|
||||||
|
// On Yaesu the call is useless at best: an FT-891 logged "SetSimplexMode OK"
|
||||||
|
// on every spot click while FreqA never moved (see below — the direct property
|
||||||
|
// write is what actually tunes these rigs). On an FT-2000 it is worse than
|
||||||
|
// useless. From a log of one QSY, before and after the call:
|
||||||
|
//
|
||||||
|
// Vfo="AB"(0x80) Split=0x10000 (off) → the operator's state
|
||||||
|
// Vfo="BA"(0x100) Split=0x8000 (ON) → after SetSimplexMode
|
||||||
|
//
|
||||||
|
// The rig-agnostic "receive and transmit HERE, simplex" method turned split ON
|
||||||
|
// and moved reception to VFO B, which is exactly what the operator reported:
|
||||||
|
// OpsLog showing B instead of A. OpsLog was not misreading the radio — it was
|
||||||
|
// reading it correctly after having moved it itself.
|
||||||
|
//
|
||||||
|
// Restricted to Yaesu because that is where the evidence is: on Icom the call
|
||||||
|
// is authoritative and the direct write is the unreliable one.
|
||||||
|
isYaesu := isYaesuRig(rigType)
|
||||||
simplexOK := false
|
simplexOK := false
|
||||||
switch {
|
switch {
|
||||||
case onSubVFO:
|
case onSubVFO:
|
||||||
debugLog.Printf("OmniRig.SetFrequency: on VFO %q — skipping SetSimplexMode so the rig stays on SUB", vfo)
|
debugLog.Printf("OmniRig.SetFrequency: on VFO %q — skipping SetSimplexMode so the rig stays on SUB", vfo)
|
||||||
|
case isYaesu:
|
||||||
|
debugLog.Printf("OmniRig.SetFrequency: %q is a Yaesu — skipping SetSimplexMode (a no-op on some, and on an FT-2000 it turns split on and jumps to VFO B); the direct write below does the work", rigType)
|
||||||
default:
|
default:
|
||||||
if _, err := oleutil.CallMethod(o.rig, "SetSimplexMode", int32(hz32)); err == nil {
|
if _, err := oleutil.CallMethod(o.rig, "SetSimplexMode", int32(hz32)); err == nil {
|
||||||
simplexOK = true
|
simplexOK = true
|
||||||
|
|||||||
@@ -125,3 +125,24 @@ func TestOmniRigWriteTarget(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSimplexMode is skipped on Yaesu, and that is not a preference.
|
||||||
|
//
|
||||||
|
// On an FT-891 it returned OK on every spot click while the rig never moved. On
|
||||||
|
// an FT-2000 it did something worse — a log of one QSY shows the state going
|
||||||
|
// from Vfo="AB" split off to Vfo="BA" split ON, so the operator's reception
|
||||||
|
// jumped to VFO B and OpsLog, reading the radio correctly, displayed B. The
|
||||||
|
// direct FreqA/Freq write does the tuning on these rigs.
|
||||||
|
func TestYaesuRigsAreRecognisedForTheSimplexSkip(t *testing.T) {
|
||||||
|
for _, yes := range []string{"FT-2000", "FT-891", "FTDX10", "ftdx101", " FT-991A "} {
|
||||||
|
if !isYaesuRig(yes) {
|
||||||
|
t.Errorf("%q not recognised as a Yaesu — SetSimplexMode would still be called on it", yes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Everything else keeps the call: on Icom it is the authoritative one.
|
||||||
|
for _, no := range []string{"IC-7610", "TS-590", "K3", "", "Flex-6600"} {
|
||||||
|
if isYaesuRig(no) {
|
||||||
|
t.Errorf("%q wrongly treated as a Yaesu", no)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||||
appVersion = "0.26.6"
|
appVersion = "0.26.7"
|
||||||
|
|
||||||
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
||||||
// to https://us.i.posthog.com for a US project.
|
// to https://us.i.posthog.com for a US project.
|
||||||
|
|||||||
Reference in New Issue
Block a user