Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
025472820b | ||
|
|
8b66030c89 | ||
|
|
d4f23a52af | ||
|
|
b4b9674d8c |
@@ -2721,6 +2721,15 @@ func (a *App) applySolar(q *qso.QSO) {
|
|||||||
if a.solar == nil {
|
if a.solar == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Today's space weather belongs on today's QSO. The ADIF monitor and the UDP
|
||||||
|
// path both feed contacts that are normally seconds old, but neither promises
|
||||||
|
// it: a logger re-broadcasting its backlog, or an operator typing in last
|
||||||
|
// month's contact by hand, would otherwise be given this morning's SFI as if
|
||||||
|
// it had been measured at the time. A wrong number is worse than none — it
|
||||||
|
// cannot be told from a real reading afterwards.
|
||||||
|
if !q.QSODate.IsZero() && time.Since(q.QSODate) > 24*time.Hour {
|
||||||
|
return
|
||||||
|
}
|
||||||
d := a.solar.Get()
|
d := a.solar.Get()
|
||||||
if !d.OK {
|
if !d.OK {
|
||||||
return
|
return
|
||||||
@@ -11823,6 +11832,16 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
|
|||||||
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
|
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
|
||||||
a.applyQSLDefaults(&q)
|
a.applyQSLDefaults(&q)
|
||||||
|
|
||||||
|
// ── Space weather and path length ──
|
||||||
|
// Also "same as the manual path", and they were missed when that comment was
|
||||||
|
// written. A QSO auto-logged from WSJT-X went in with no SFI, no A, no K and
|
||||||
|
// no distance, so an operator running digital — which is most of the traffic
|
||||||
|
// on most stations — had those fields empty across the whole log while a
|
||||||
|
// hand-logged contact carried them. Both are stamped only where the record
|
||||||
|
// left them empty, so an ADIF that supplied its own still wins.
|
||||||
|
a.applySolar(&q)
|
||||||
|
fillDistance(&q)
|
||||||
|
|
||||||
// ── Dedup (serialised) ──
|
// ── Dedup (serialised) ──
|
||||||
// Match by call + band + mode within a ±2-minute window: a QSO logged
|
// Match by call + band + mode within a ±2-minute window: a QSO logged
|
||||||
// manually in OpsLog and re-broadcast by Log4OM over UDP often differs by
|
// manually in OpsLog and re-broadcast by Log4OM over UDP often differs by
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.24.7",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"QSOs logged from WSJT-X now carry the space weather and the distance, like hand-logged ones. The UDP path stamped the station profile, the DXCC and the QSL defaults but not SFI, A, K or distance — so an operator running digital had those fields empty across the whole log. Space weather is only stamped on a contact less than a day old: a logger re-broadcasting its backlog would otherwise be handed this morning readings for last month contacts.",
|
||||||
|
"Band openings: EME contacts are no longer mistaken for an opening. A 2 m opening was announced at 9650 km towards Japan on stations working moonbounce — real contacts, but the moon says nothing about the band, and an antenna pointed that way finds nothing. Each band now has the longest path the atmosphere can actually carry: 3500 km on 2 m, 4000 on 4 m, and no limit at all on 6 and 10 m where multi-hop really does go round the world."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Les QSO enregistrés depuis WSJT-X portent désormais la météo spatiale et la distance, comme ceux saisis à la main. Le chemin UDP posait le profil station, le DXCC et les défauts QSL mais ni SFI, ni A, ni K, ni distance — un opérateur en numérique avait donc ces champs vides sur tout son log. La météo spatiale n est posée que sur un contact de moins d un jour : sinon un logiciel qui rediffuse son historique se verrait attribuer les relevés de ce matin sur des contacts du mois dernier.",
|
||||||
|
"Ouvertures de bande : les contacts EME ne sont plus pris pour une ouverture. Une ouverture 2 m était annoncée à 9650 km vers le Japon sur des stations en rebond lunaire — de vrais contacts, mais la Lune ne dit rien de la bande, et une antenne pointée par là ne trouve rien. Chaque bande a désormais la distance maximale que l atmosphère peut réellement porter : 3500 km en 2 m, 4000 en 4 m, et aucune limite en 6 et 10 m où les sauts multiples font vraiment le tour du monde."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.24.6",
|
"version": "0.24.6",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
@@ -373,12 +373,16 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth,
|
|||||||
<Input value={details.address} onChange={(e) => onChange({ address: e.target.value })} />
|
<Input value={details.address} onChange={(e) => onChange({ address: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<Field label={t('detp.qslMessage')} span={7}>
|
{/* QSL via gets the room, not the message. Width should follow use, and
|
||||||
<Input value={details.qsl_msg} onChange={(e) => onChange({ qsl_msg: e.target.value })} />
|
these two are nowhere near equal: a manager's callsign is filled in
|
||||||
</Field>
|
constantly and a QSL message almost never. The message had 7 columns
|
||||||
<Field label={t('detp.qslVia')} span={5}>
|
of 12 for text most operators never type. */}
|
||||||
|
<Field label={t('detp.qslVia')} span={7}>
|
||||||
<Input value={details.qsl_via} onChange={(e) => onChange({ qsl_via: e.target.value })} />
|
<Input value={details.qsl_via} onChange={(e) => onChange({ qsl_via: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label={t('detp.qslMessage')} span={5}>
|
||||||
|
<Input value={details.qsl_msg} onChange={(e) => onChange({ qsl_msg: e.target.value })} />
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,28 @@ var watched = map[string]bool{"10m": true, "6m": true, "4m": true, "2m": true}
|
|||||||
// Watched reports whether a band is one the detector looks at.
|
// Watched reports whether a band is one the detector looks at.
|
||||||
func Watched(band string) bool { return watched[strings.ToLower(strings.TrimSpace(band))] }
|
func Watched(band string) bool { return watched[strings.ToLower(strings.TrimSpace(band))] }
|
||||||
|
|
||||||
|
// maxTerrestrialKm is the longest path a band can carry through the atmosphere.
|
||||||
|
// Zero means no limit.
|
||||||
|
//
|
||||||
|
// The flat 2400 km ceiling was removed because it threw away real multi-hop Es
|
||||||
|
// on 6 m, and that was right — but "no limit anywhere" then let something else
|
||||||
|
// through. A 2 m opening was announced at 9650 km towards Japan, on stations
|
||||||
|
// that were unmistakably working EME: the moon is not an opening, and pointing
|
||||||
|
// an antenna at that bearing would find nothing.
|
||||||
|
//
|
||||||
|
// So the limit is per band, and it is physics rather than a threshold. Two
|
||||||
|
// metres reaches a few thousand kilometres by tropospheric duct or a chain of Es
|
||||||
|
// clouds and no further; beyond that the path went via the moon or a satellite,
|
||||||
|
// neither of which says anything about the band. Six and ten metres have no
|
||||||
|
// ceiling at all — multi-hop Es and F2 genuinely go round the world.
|
||||||
|
var maxTerrestrialKm = map[string]int{
|
||||||
|
"2m": 3500,
|
||||||
|
"4m": 4000,
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxKmFor returns the plausibility ceiling for a band, 0 for none.
|
||||||
|
func MaxKmFor(band string) int { return maxTerrestrialKm[strings.ToLower(strings.TrimSpace(band))] }
|
||||||
|
|
||||||
// Opening is a detected opening, ready to be announced.
|
// Opening is a detected opening, ready to be announced.
|
||||||
type Opening struct {
|
type Opening struct {
|
||||||
Band string `json:"band"`
|
Band string `json:"band"`
|
||||||
@@ -130,6 +152,12 @@ func (d *Detector) Add(s Spot, lat float64) *Opening {
|
|||||||
if s.DistKm < d.cfg.MinKm || (d.cfg.MaxKm > 0 && s.DistKm > d.cfg.MaxKm) {
|
if s.DistKm < d.cfg.MinKm || (d.cfg.MaxKm > 0 && s.DistKm > d.cfg.MaxKm) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Past what the atmosphere can carry on this band, the path went via the moon
|
||||||
|
// or a satellite. Those are real contacts and real reports; they are simply
|
||||||
|
// not evidence about the band.
|
||||||
|
if m := MaxKmFor(band); m > 0 && s.DistKm > m {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
d.recent = append(d.recent, s)
|
d.recent = append(d.recent, s)
|
||||||
d.prune(s.At)
|
d.prune(s.At)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package bandopen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A 2 m "opening" was announced at 9650 km towards Japan on stations that were
|
||||||
|
// plainly working EME. The moon is not an opening: an operator pointing an
|
||||||
|
// antenna at that bearing finds nothing.
|
||||||
|
func TestEMEIsNotAnOpeningOnTwoMetres(t *testing.T) {
|
||||||
|
d := New(DefaultConfig())
|
||||||
|
base := time.Date(2026, 8, 12, 6, 0, 0, 0, time.UTC)
|
||||||
|
for i, call := range []string{"7M4RRM", "JA7RPC", "JF1AWC", "JK1TPA", "JH1JCQ"} {
|
||||||
|
if op := d.Add(Spot{
|
||||||
|
Call: call, Band: "2m", DistKm: 9650, Bearing: 40 + i*3,
|
||||||
|
At: base.Add(time.Duration(i) * time.Minute),
|
||||||
|
}, 47.0); op != nil {
|
||||||
|
t.Fatalf("a 9650 km 2 m path was announced as an opening: %+v", op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// But a real 2 m opening — an Es chain at a plausible distance — must survive.
|
||||||
|
func TestLongButPlausibleTwoMetresStillCounts(t *testing.T) {
|
||||||
|
d := New(DefaultConfig())
|
||||||
|
base := time.Date(2026, 6, 20, 18, 0, 0, 0, time.UTC)
|
||||||
|
var got *Opening
|
||||||
|
for i, call := range []string{"EA1AA", "CT1BB", "EA7CC", "CT7DD"} {
|
||||||
|
if op := d.Add(Spot{
|
||||||
|
Call: call, Band: "2m", DistKm: 2000 + i*30, Bearing: 200 + i*4,
|
||||||
|
At: base.Add(time.Duration(i) * time.Minute),
|
||||||
|
}, 47.0); op != nil {
|
||||||
|
got = op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("a 2000 km 2 m burst in one sector was not reported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Six metres keeps no ceiling: multi-hop Es genuinely goes that far, which is
|
||||||
|
// why the flat limit was removed in the first place.
|
||||||
|
func TestSixMetresHasNoCeiling(t *testing.T) {
|
||||||
|
if MaxKmFor("6m") != 0 || MaxKmFor("10m") != 0 {
|
||||||
|
t.Error("6 m and 10 m must have no distance ceiling")
|
||||||
|
}
|
||||||
|
if MaxKmFor("2m") == 0 {
|
||||||
|
t.Error("2 m must have one")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user