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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user