feat(sat): generate the frequency plan, and join on the catalog number
25 satellites, typed by hand and never revisited. Eight of them were the
first-generation Tevel constellation, which re-entered in 2024; four more had
come down too; and the nine Tevel-2 satellites that replaced them, the Chinese
space station, AO-27, AO-123 and twenty others were simply absent. So: 44
satellites now, and a generator instead of a memory.
cmd/satgen joins three public sources on the NORAD catalog number — Celestrak's
amateur group and PE0SAT's mirror for which birds OpsLog can actually get
elements for, and SatNOGS DB for the transmitters. It is a one-shot tool, run by
hand, the same arrangement as cmd/cntygen, and it is deliberately conservative:
- It never destroys a curated entry. The hand-written plans hold things
SatNOGS does not reliably carry — a CTCSS tone, the QO-100 passband as
operators describe it — so an existing bird keeps its data and only gains
its catalog number.
- It prunes on the re-entry date, which is a fact SatNOGS publishes rather
than a judgement about which of the missing satellites are missing for good.
- It refuses a digital uplink that does not say what it is. A GMSK uplink is a
command channel far more often than a digipeater, and shipping the wrong one
invites somebody to transmit on a control frequency. An analog uplink with
both ends is a contact by construction, which is what catches the repeaters
that describe themselves only as "Mode V/U FM".
- Its output is deterministic. One satellite can hold two catalog entries —
GreenCube is 53106 in one feed and 53109 in the other — and iterating a map
picked a different one each run.
A bird now carries its NORAD number, and that is how its elements are found.
Names were the only join before, and they are written differently by every party
involved: "TIANYAN 01" and "TO-108" are one satellite that had never once met,
so TO-108 tracked nothing at all.
And the plan now reaches a station that has already run OpsLog. The editable
copy was written on the first launch and was the operator's list for ever after,
so a release adding nine satellites reached nobody who had opened the tab. It is
merged on each load instead: a satellite they already have is untouched, edits
and corrections included, and only the ones they have never seen are added.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
+71
-1
@@ -114,7 +114,12 @@ func (t Transponder) Centre() int64 {
|
||||
|
||||
// Bird is one satellite's frequency plan.
|
||||
type Bird struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"`
|
||||
// NORAD is the catalog number, and the only exact way to find this
|
||||
// satellite's elements: the feed, AMSAT and the operator all spell the NAME
|
||||
// differently, while the number is carried inside the TLE itself. Aliases
|
||||
// remain for the entries that predate it and for a hand-written plan.
|
||||
NORAD int `json:"norad,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
// Geostationary: no pass, no Doppler worth correcting, a fixed look angle.
|
||||
// QO-100 is the reason the flag exists, and it changes what the whole
|
||||
@@ -190,6 +195,22 @@ func LoadBirds(dir string) (*Birds, error) {
|
||||
_ = b.parse(shippedBirds)
|
||||
return b, fmt.Errorf("sat: %s could not be read (%w) — the shipped list is in use for this session, and your file has been left alone", BirdsName, perr)
|
||||
}
|
||||
// New satellites reach an EXISTING station too.
|
||||
//
|
||||
// The operator's copy is written once, on the first run, and was then
|
||||
// theirs for ever — which meant a release that added nine Tevel-2
|
||||
// satellites reached nobody who had already opened the tab. Merging on
|
||||
// each load fixes that without taking anything back: a satellite the
|
||||
// operator already has is left exactly as it is, edits included, and
|
||||
// only the ones they have never seen are added. Deleting a bird from the
|
||||
// file therefore brings it back, which is the price of the trade — and
|
||||
// the cheaper half of it, since an unwanted satellite is one row and a
|
||||
// missing one is a pass nobody can work.
|
||||
if n := b.addMissing(shippedBirds); n > 0 {
|
||||
if out, merr := json.MarshalIndent(b.list, "", " "); merr == nil {
|
||||
_ = os.WriteFile(path, append(out, '\n'), 0o644)
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
case os.IsNotExist(err):
|
||||
if perr := b.parse(shippedBirds); perr != nil {
|
||||
@@ -283,3 +304,52 @@ func (b *Birds) Len() int {
|
||||
defer b.mu.RUnlock()
|
||||
return len(b.list)
|
||||
}
|
||||
|
||||
// addMissing appends the satellites in `shipped` that this list does not already
|
||||
// hold, and reports how many were added.
|
||||
//
|
||||
// "Already hold" is by catalog number first and by the loose name second, so an
|
||||
// operator who renamed a bird, or who has it under the feed's spelling, does not
|
||||
// get a second copy of it. Nothing existing is touched: their frequencies, their
|
||||
// labels and their corrections all stand.
|
||||
func (b *Birds) addMissing(shipped []byte) int {
|
||||
var list []Bird
|
||||
if err := json.Unmarshal(shipped, &list); err != nil {
|
||||
return 0
|
||||
}
|
||||
b.mu.Lock()
|
||||
have := make(map[int]bool, len(b.list))
|
||||
for _, x := range b.list {
|
||||
if x.NORAD != 0 {
|
||||
have[x.NORAD] = true
|
||||
}
|
||||
}
|
||||
added := 0
|
||||
for _, cand := range list {
|
||||
if cand.NORAD != 0 && have[cand.NORAD] {
|
||||
continue
|
||||
}
|
||||
known := false
|
||||
for _, name := range append([]string{cand.Name}, cand.Aliases...) {
|
||||
if k := loose(name); k != "" {
|
||||
if _, ok := b.byKey[k]; ok {
|
||||
known = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if known {
|
||||
continue
|
||||
}
|
||||
b.list = append(b.list, cand)
|
||||
if cand.NORAD != 0 {
|
||||
have[cand.NORAD] = true
|
||||
}
|
||||
if k := loose(cand.Name); k != "" {
|
||||
b.byKey[k] = len(b.list) - 1
|
||||
}
|
||||
added++
|
||||
}
|
||||
b.mu.Unlock()
|
||||
return added
|
||||
}
|
||||
|
||||
+566
-304
@@ -1,6 +1,312 @@
|
||||
[
|
||||
{
|
||||
"name": "AO-123",
|
||||
"norad": 61781,
|
||||
"aliases": [
|
||||
"ASRTU-1 (AO-123)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transceiver",
|
||||
"mode": "FM",
|
||||
"down_lo": 435400000,
|
||||
"up_lo": 145850000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-27",
|
||||
"norad": 22825,
|
||||
"aliases": [
|
||||
"EYESAT A (AO-27)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U FM",
|
||||
"mode": "FM",
|
||||
"down_lo": 436795000,
|
||||
"up_lo": 145850000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-7",
|
||||
"norad": 7530,
|
||||
"aliases": [
|
||||
"AMSAT-OSCAR 7",
|
||||
"OSCAR 7"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode B linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145925000,
|
||||
"down_hi": 145975000,
|
||||
"up_lo": 432125000,
|
||||
"up_hi": 432175000,
|
||||
"inverting": true
|
||||
},
|
||||
{
|
||||
"label": "Mode A linear",
|
||||
"mode": "SSB",
|
||||
"down_lo": 29400000,
|
||||
"down_hi": 29500000,
|
||||
"up_lo": 145850000,
|
||||
"up_hi": 145950000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-73",
|
||||
"norad": 39444,
|
||||
"aliases": [
|
||||
"FUNCUBE-1",
|
||||
"FUNCUBE 1"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145950000,
|
||||
"down_hi": 145970000,
|
||||
"up_lo": 435130000,
|
||||
"up_hi": 435150000,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-91",
|
||||
"norad": 43017,
|
||||
"aliases": [
|
||||
"RADFXSAT",
|
||||
"FOX-1B",
|
||||
"RADFXSAT (FOX-1B)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 145960000,
|
||||
"up_lo": 435250000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "BEESAT-1",
|
||||
"norad": 35933,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Digipeater (Mobitex, idle mode: 10 sec interval)",
|
||||
"mode": "DATA",
|
||||
"down_lo": 435950000,
|
||||
"up_lo": 435950000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CO-65",
|
||||
"norad": 32785,
|
||||
"aliases": [
|
||||
"CUTE-1.7+APD II (CO-65)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode L/U Digipeater",
|
||||
"mode": "DATA",
|
||||
"down_lo": 437475000,
|
||||
"up_lo": 1267600000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CROCUBE",
|
||||
"norad": 62394,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode U/U - GFSK9k6 - Digipeater - AX.25",
|
||||
"mode": "DATA",
|
||||
"down_lo": 436775000,
|
||||
"up_lo": 436775000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CSS",
|
||||
"norad": 48274,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "3A V/V digipeater AFSK-FM 1200",
|
||||
"mode": "DATA",
|
||||
"down_lo": 145825000,
|
||||
"up_lo": 145825000
|
||||
},
|
||||
{
|
||||
"label": "1A V/V crew voice NFM",
|
||||
"mode": "FM",
|
||||
"down_lo": 145985000,
|
||||
"up_lo": 145850000
|
||||
},
|
||||
{
|
||||
"label": "2B U/V FM repeater NFM",
|
||||
"mode": "FM",
|
||||
"down_lo": 145985000,
|
||||
"up_lo": 435075000
|
||||
},
|
||||
{
|
||||
"label": "4A V/V imaging SSTV-FM",
|
||||
"mode": "FM",
|
||||
"down_lo": 145985000,
|
||||
"up_lo": 145850000
|
||||
},
|
||||
{
|
||||
"label": "1B U/U crew voice NFM",
|
||||
"mode": "FM",
|
||||
"down_lo": 436510000,
|
||||
"up_lo": 435050000
|
||||
},
|
||||
{
|
||||
"label": "2A V/U FM repeater NFM",
|
||||
"mode": "FM",
|
||||
"down_lo": 436510000,
|
||||
"up_lo": 145875000
|
||||
},
|
||||
{
|
||||
"label": "4B U/U imaging SSTV-FM",
|
||||
"mode": "FM",
|
||||
"down_lo": 436510000,
|
||||
"up_lo": 435050000
|
||||
},
|
||||
{
|
||||
"label": "3B U/U digipeater AFSK-FM 1200",
|
||||
"mode": "DATA",
|
||||
"down_lo": 437550000,
|
||||
"up_lo": 437550000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ESEO",
|
||||
"norad": 43792,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM",
|
||||
"mode": "FM",
|
||||
"down_lo": 145895000,
|
||||
"up_lo": 1263500000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FLORIPASAT-1",
|
||||
"norad": 44885,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode U/U GFSK2k4 Repeater",
|
||||
"mode": "DATA",
|
||||
"down_lo": 436100000,
|
||||
"up_lo": 436100000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FO-29",
|
||||
"norad": 24278,
|
||||
"aliases": [
|
||||
"JAS-2",
|
||||
"FUJI-OSCAR 29"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 435800000,
|
||||
"down_hi": 435900000,
|
||||
"up_lo": 145900000,
|
||||
"up_hi": 146000000,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FORESAIL-1P",
|
||||
"norad": 66778,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode U/U - GMSK9k6 - Digipeater - Skylink",
|
||||
"mode": "DATA",
|
||||
"down_lo": 437125000,
|
||||
"up_lo": 437125000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GRBBETA",
|
||||
"norad": 60237,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/V - GFSK9k6 - Digipeater - AX.25",
|
||||
"mode": "DATA",
|
||||
"down_lo": 145935000,
|
||||
"up_lo": 145935000
|
||||
},
|
||||
{
|
||||
"label": "Mode U/U - GFSK9k6 - Digipeater - AX.25",
|
||||
"mode": "DATA",
|
||||
"down_lo": 436785000,
|
||||
"up_lo": 436785000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IO-117",
|
||||
"norad": 53109,
|
||||
"aliases": [
|
||||
"GREENCUBE",
|
||||
"MEZTLI"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Digipeater (1200 bd GMSK)",
|
||||
"mode": "DATA",
|
||||
"down_lo": 435310000,
|
||||
"up_lo": 435310000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IO-86",
|
||||
"norad": 40931,
|
||||
"aliases": [
|
||||
"LAPAN-A2",
|
||||
"LAPAN-ORARI"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 435880000,
|
||||
"up_lo": 145880000,
|
||||
"ctcss": 88.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ISAT",
|
||||
"norad": 43879,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "MODE U/U DSTAR VOICE",
|
||||
"mode": "FM",
|
||||
"down_lo": 435525000,
|
||||
"up_lo": 437325000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ISS (ZARYA)",
|
||||
"norad": 25544,
|
||||
"aliases": [
|
||||
"ISS",
|
||||
"ZARYA",
|
||||
@@ -27,135 +333,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SO-50",
|
||||
"aliases": [
|
||||
"SAUDISAT 1C",
|
||||
"SAUDISAT 1C (SO-50)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436795000,
|
||||
"up_lo": 145850000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-91",
|
||||
"aliases": [
|
||||
"RADFXSAT",
|
||||
"FOX-1B",
|
||||
"RADFXSAT (FOX-1B)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 145960000,
|
||||
"up_lo": 435250000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IO-86",
|
||||
"aliases": [
|
||||
"LAPAN-A2",
|
||||
"LAPAN-ORARI"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 435880000,
|
||||
"up_lo": 145880000,
|
||||
"ctcss": 88.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PO-101",
|
||||
"aliases": [
|
||||
"DIWATA-2",
|
||||
"DIWATA-2B"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater (scheduled)",
|
||||
"mode": "FM",
|
||||
"down_lo": 145900000,
|
||||
"up_lo": 437500000,
|
||||
"ctcss": 141.3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-7",
|
||||
"aliases": [
|
||||
"AMSAT-OSCAR 7",
|
||||
"OSCAR 7"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode B linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145925000,
|
||||
"down_hi": 145975000,
|
||||
"up_lo": 432125000,
|
||||
"up_hi": 432175000,
|
||||
"inverting": true
|
||||
},
|
||||
{
|
||||
"label": "Mode A linear",
|
||||
"mode": "SSB",
|
||||
"down_lo": 29400000,
|
||||
"down_hi": 29500000,
|
||||
"up_lo": 145850000,
|
||||
"up_hi": 145950000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FO-29",
|
||||
"aliases": [
|
||||
"JAS-2",
|
||||
"FUJI-OSCAR 29"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 435800000,
|
||||
"down_hi": 435900000,
|
||||
"up_lo": 145900000,
|
||||
"up_hi": 146000000,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-73",
|
||||
"aliases": [
|
||||
"FUNCUBE-1",
|
||||
"FUNCUBE 1"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145950000,
|
||||
"down_hi": 145970000,
|
||||
"up_lo": 435130000,
|
||||
"up_hi": 435150000,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "JO-97",
|
||||
"norad": 43803,
|
||||
"aliases": [
|
||||
"JY1SAT",
|
||||
"JY1-SAT"
|
||||
@@ -173,24 +353,117 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RS-44",
|
||||
"name": "KNACKSAT-2",
|
||||
"norad": 67683,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/V - FSK9k6 - Digipeater - AX.25 G3RUH",
|
||||
"mode": "DATA",
|
||||
"down_lo": 145825000,
|
||||
"up_lo": 145825000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "KOSEN-1",
|
||||
"norad": 49402,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode HF/U - Onboard SDR",
|
||||
"mode": "DATA",
|
||||
"down_lo": 435525000,
|
||||
"up_lo": 21125000,
|
||||
"up_hi": 21150000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LASARSAT",
|
||||
"norad": 62391,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode U/U - GFSK9k6 - Digipeater - AX.25",
|
||||
"mode": "DATA",
|
||||
"down_lo": 436925000,
|
||||
"up_lo": 436925000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LILACSAT-2",
|
||||
"norad": 40908,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "APRS Digipeater",
|
||||
"mode": "DATA",
|
||||
"down_lo": 144390000,
|
||||
"up_lo": 144390000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NO-44",
|
||||
"norad": 26931,
|
||||
"aliases": [
|
||||
"DOSAAF-85"
|
||||
"PCSAT (NO-44)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"label": "Mode V/V APRS AFSK",
|
||||
"mode": "DATA",
|
||||
"down_lo": 145825000,
|
||||
"up_lo": 145825000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PO-101",
|
||||
"norad": 43678,
|
||||
"aliases": [
|
||||
"DIWATA-2",
|
||||
"DIWATA-2B"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater (scheduled)",
|
||||
"mode": "FM",
|
||||
"down_lo": 145900000,
|
||||
"up_lo": 437500000,
|
||||
"ctcss": 141.3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QB50P1",
|
||||
"norad": 40025,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear Transponder",
|
||||
"mode": "SSB",
|
||||
"down_lo": 435640000,
|
||||
"down_hi": 435680000,
|
||||
"up_lo": 145965000,
|
||||
"up_hi": 146005000,
|
||||
"down_lo": 145935000,
|
||||
"down_hi": 145965000,
|
||||
"up_lo": 435047000,
|
||||
"up_hi": 435077000,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QMR-KWT-2",
|
||||
"norad": 67291,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "V/U FM Transponder CTCSS 67.0 Hz",
|
||||
"mode": "FM",
|
||||
"down_lo": 436950000,
|
||||
"up_lo": 145920000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QO-100",
|
||||
"norad": 43700,
|
||||
"aliases": [
|
||||
"ES'HAIL 2",
|
||||
"ESHAIL 2",
|
||||
@@ -217,199 +490,176 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-1",
|
||||
"name": "RS-44",
|
||||
"norad": 44909,
|
||||
"aliases": [
|
||||
"TEVEL 1"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-2",
|
||||
"aliases": [
|
||||
"TEVEL 2"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-3",
|
||||
"aliases": [
|
||||
"TEVEL 3"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-4",
|
||||
"aliases": [
|
||||
"TEVEL 4"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-5",
|
||||
"aliases": [
|
||||
"TEVEL 5"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-6",
|
||||
"aliases": [
|
||||
"TEVEL 6"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-7",
|
||||
"aliases": [
|
||||
"TEVEL 7"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL-8",
|
||||
"aliases": [
|
||||
"TEVEL 8"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EO-88",
|
||||
"aliases": [
|
||||
"NAYIF-1",
|
||||
"FUNCUBE-5"
|
||||
"DOSAAF-85"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145960000,
|
||||
"down_hi": 145990000,
|
||||
"up_lo": 435015000,
|
||||
"up_hi": 435045000,
|
||||
"down_lo": 435640000,
|
||||
"down_hi": 435680000,
|
||||
"up_lo": 145965000,
|
||||
"up_hi": 146005000,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AO-109",
|
||||
"name": "SO-50",
|
||||
"norad": 27607,
|
||||
"aliases": [
|
||||
"RADFXSAT-2",
|
||||
"FOX-1E"
|
||||
"SAUDISAT 1C",
|
||||
"SAUDISAT 1C (SO-50)"
|
||||
],
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145860000,
|
||||
"down_hi": 145880000,
|
||||
"up_lo": 435750000,
|
||||
"up_hi": 435770000,
|
||||
"inverting": true
|
||||
"label": "FM voice repeater",
|
||||
"mode": "FM",
|
||||
"down_lo": 436795000,
|
||||
"up_lo": 145850000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CAS-4A",
|
||||
"aliases": [
|
||||
"ZHUHAI-1 OVS-1A",
|
||||
"OVS-1A"
|
||||
],
|
||||
"name": "SONATE-2",
|
||||
"norad": 59112,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145860000,
|
||||
"down_hi": 145880000,
|
||||
"up_lo": 435210000,
|
||||
"up_hi": 435230000,
|
||||
"inverting": true
|
||||
"label": "Mode V/V - APRS digipeater",
|
||||
"mode": "DATA",
|
||||
"down_lo": 145825000,
|
||||
"up_lo": 145825000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CAS-4B",
|
||||
"aliases": [
|
||||
"ZHUHAI-1 OVS-1B",
|
||||
"OVS-1B"
|
||||
],
|
||||
"name": "TAURUS-1",
|
||||
"norad": 44530,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Linear (inverting)",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145905000,
|
||||
"down_hi": 145925000,
|
||||
"up_lo": 435270000,
|
||||
"up_hi": 435290000,
|
||||
"inverting": true
|
||||
"label": "Mode V/U FM 67.0 PL",
|
||||
"mode": "FM",
|
||||
"down_lo": 436760000,
|
||||
"up_lo": 145820000,
|
||||
"ctcss": 67
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-1",
|
||||
"norad": 63217,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-2",
|
||||
"norad": 63219,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder - Beacon",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-3",
|
||||
"norad": 63218,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-4",
|
||||
"norad": 63213,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-5",
|
||||
"norad": 63214,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-6",
|
||||
"norad": 63215,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-7",
|
||||
"norad": 63238,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-8",
|
||||
"norad": 63239,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder - Beacon",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TEVEL2-9",
|
||||
"norad": 63237,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U - FM Transponder",
|
||||
"mode": "FM",
|
||||
"down_lo": 436400000,
|
||||
"up_lo": 145970000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TO-108",
|
||||
"norad": 44881,
|
||||
"aliases": [
|
||||
"CAS-6",
|
||||
"TIANQIN-1"
|
||||
@@ -427,17 +677,29 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IO-117",
|
||||
"aliases": [
|
||||
"GREENCUBE",
|
||||
"MEZTLI"
|
||||
],
|
||||
"name": "UKUBE-1",
|
||||
"norad": 40074,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Digipeater (1200 bd GMSK)",
|
||||
"mode": "DATA",
|
||||
"down_lo": 435310000,
|
||||
"up_lo": 435310000
|
||||
"label": "Inverting linear transponder",
|
||||
"mode": "SSB",
|
||||
"down_lo": 145930000,
|
||||
"down_hi": 145950000,
|
||||
"up_lo": 435074300,
|
||||
"up_hi": 435094300,
|
||||
"inverting": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "XIWANG-1 (HOPE-1)",
|
||||
"norad": 36122,
|
||||
"transponders": [
|
||||
{
|
||||
"label": "Mode V/U FM",
|
||||
"mode": "FM",
|
||||
"down_lo": 435675000,
|
||||
"up_lo": 145825000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -158,8 +159,19 @@ func TestLoadBirdsWritesTheEditableCopy(t *testing.T) {
|
||||
t.Fatalf("the editable copy was not written: %v", err)
|
||||
}
|
||||
|
||||
// An operator's own list is what gets used from then on.
|
||||
mine := `[{"name":"MY-SAT","transponders":[{"label":"FM","mode":"FM","down_lo":1,"up_lo":2}]}]`
|
||||
// An operator's own list is kept, AND the shipped satellites they have never
|
||||
// seen are added to it.
|
||||
//
|
||||
// The merge is what carries a new satellite to a station that has already
|
||||
// run OpsLog once: without it, the copy written on the very first launch was
|
||||
// the operator's list for ever, and a release adding nine Tevel-2 birds
|
||||
// reached nobody. What it must never do is take something back — so the
|
||||
// operator's own satellite, and their correction to a shipped one, both have
|
||||
// to survive it.
|
||||
mine := `[
|
||||
{"name":"MY-SAT","transponders":[{"label":"FM","mode":"FM","down_lo":1,"up_lo":2}]},
|
||||
{"name":"SO-50","norad":27607,"transponders":[{"label":"corrected","mode":"FM","down_lo":436796000,"up_lo":145850000,"ctcss":74.4}]}
|
||||
]`
|
||||
if err := os.WriteFile(path, []byte(mine), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -167,8 +179,23 @@ func TestLoadBirdsWritesTheEditableCopy(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.Len() != 1 {
|
||||
t.Fatalf("the operator's list was not used: %d satellites", b.Len())
|
||||
if b.Len() < shipped {
|
||||
t.Fatalf("the shipped satellites were not merged in: %d, expected at least %d", b.Len(), shipped)
|
||||
}
|
||||
if _, ok := b.Find("MY-SAT"); !ok {
|
||||
t.Error("the operator's own satellite was dropped by the merge")
|
||||
}
|
||||
// Their correction stands: same tone, same frequency, not the shipped one.
|
||||
got, ok := b.Find("SO-50")
|
||||
if !ok {
|
||||
t.Fatal("SO-50 vanished")
|
||||
}
|
||||
if len(got.Transponders) != 1 || got.Transponders[0].CTCSS != 74.4 || got.Transponders[0].DownLo != 436796000 {
|
||||
t.Errorf("the operator's correction to SO-50 was overwritten: %+v", got.Transponders)
|
||||
}
|
||||
// And a shipped satellite they had never seen is now there.
|
||||
if _, ok := b.Find("AO-7"); !ok {
|
||||
t.Error("a shipped satellite was not added to the operator's list")
|
||||
}
|
||||
|
||||
// And a broken one falls back without destroying what they wrote.
|
||||
@@ -186,3 +213,58 @@ func TestLoadBirdsWritesTheEditableCopy(t *testing.T) {
|
||||
t.Error("the operator's broken file was overwritten")
|
||||
}
|
||||
}
|
||||
|
||||
// The shipped plan is generated (cmd/satgen) from public databases, so it is
|
||||
// worth its own guard, on top of TestShippedBirds above: a bad regeneration
|
||||
// should fail here rather than mistune an antenna on somebody first pass.
|
||||
func TestGeneratedBirdsAreSane(t *testing.T) {
|
||||
var list []Bird
|
||||
if err := json.Unmarshal(shippedBirds, &list); err != nil {
|
||||
t.Fatalf("birds.json does not parse: %v", err)
|
||||
}
|
||||
// The generator joins three feeds. If one of them answered with nothing, the
|
||||
// output silently shrinks, and this is where that shows up.
|
||||
if len(list) < 30 {
|
||||
t.Errorf("only %d satellites shipped — the generator probably ran against an empty feed", len(list))
|
||||
}
|
||||
seenNORAD := map[int]string{}
|
||||
seenName := map[string]bool{}
|
||||
for _, b := range list {
|
||||
if b.Name == "" {
|
||||
t.Error("a satellite with no name")
|
||||
}
|
||||
if seenName[loose(b.Name)] {
|
||||
t.Errorf("%s appears twice", b.Name)
|
||||
}
|
||||
seenName[loose(b.Name)] = true
|
||||
// Two entries for one catalog number is one satellite the operator can
|
||||
// pick twice, with two different sets of frequencies.
|
||||
if b.NORAD != 0 {
|
||||
if other, dup := seenNORAD[b.NORAD]; dup {
|
||||
t.Errorf("NORAD %d is both %s and %s", b.NORAD, other, b.Name)
|
||||
}
|
||||
seenNORAD[b.NORAD] = b.Name
|
||||
}
|
||||
for _, tp := range b.Transponders {
|
||||
// An amateur satellite works between 15 m and 24 GHz. Anything outside
|
||||
// that is a units mistake, and a units mistake is how a rig ends up
|
||||
// commanded somewhere it cannot go.
|
||||
for _, hz := range []int64{tp.DownLo, tp.DownHi, tp.UpLo, tp.UpHi} {
|
||||
if hz != 0 && (hz < 21_000_000 || hz > 24_000_000_000) {
|
||||
t.Errorf("%s: %d Hz is not an amateur satellite frequency", b.Name, hz)
|
||||
}
|
||||
}
|
||||
// Inversion only means something across a passband. The code ignores
|
||||
// the flag on a channel, but a file that claims an FM repeater inverts
|
||||
// will mislead whoever reads it next.
|
||||
if tp.Inverting && !tp.Linear() {
|
||||
t.Errorf("%s: %q is a channel and cannot invert", b.Name, tp.Label)
|
||||
}
|
||||
switch tp.Mode {
|
||||
case "FM", "SSB", "CW", "DATA":
|
||||
default:
|
||||
t.Errorf("%s: %q is not an ADIF mode the log can store", b.Name, tp.Mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,27 @@ func (s *Store) Get(name string) (Element, bool) {
|
||||
return e, ok
|
||||
}
|
||||
|
||||
// GetNORAD returns one satellite's elements by catalog number.
|
||||
//
|
||||
// The exact join, and the only one that stays exact. A name is written
|
||||
// differently by every party involved — the feed says "RADFXSAT (FOX-1B)", the
|
||||
// operator says "AO-91", AMSAT's chart says both — and matching on letters and
|
||||
// digits gets most of them and quietly misses the rest. The catalog number is
|
||||
// in the TLE itself and is what a frequency plan should carry.
|
||||
func (s *Store) GetNORAD(n int) (Element, bool) {
|
||||
if n <= 0 {
|
||||
return Element{}, false
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, k := range s.order {
|
||||
if e := s.byKey[k]; e.NORAD == n {
|
||||
return e, true
|
||||
}
|
||||
}
|
||||
return Element{}, false
|
||||
}
|
||||
|
||||
// Names lists what the store holds, in the order it arrived.
|
||||
func (s *Store) Names() []string {
|
||||
s.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user