fix(sat): LilacSat-2 keeps its FM transponder

Shipped with nothing but an APRS digipeater on 144.390, which is not what
anybody works that satellite on. The FM transponder — 144.350 up, 437.200 down —
was dropped by the generator, correctly by its own rule: SatNOGS marks that
transmitter inactive.

And SatNOGS is not wrong. LilacSat-2's transponder is switched on to an
announced schedule rather than left running, and from a database a scheduled
transponder looks exactly like a dead one. The judgement belongs to whoever
reads the diff, not to the filter.

So the transponder goes back in, labelled "(scheduled)" the way PO-101 already
is, and the satellite takes its OSCAR name, LO-90, with LILACSAT-2 and CAS-3H as
aliases so the element feeds still meet it.

The generator now reports what it refused on that ground and kept nothing else
for — one line per satellite, phrased as a question. Reviving every inactive
transponder would fill the list with birds that answer nothing; saying nothing
is how this one shipped wrong. There is no third option that a filter can decide
on its own.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-10 10:27:15 +02:00
co-authored by Claude Opus 5
parent 3af6299f32
commit 3ce74ef8d7
3 changed files with 63 additions and 3 deletions
+48
View File
@@ -140,6 +140,23 @@ func main() {
}
byNORAD[t.NORAD] = append(byNORAD[t.NORAD], t)
}
// A satellite whose only WORKABLE path is one SatNOGS calls inactive.
//
// LilacSat-2 is the case that taught this: its FM transponder (144.350 up,
// 437.200 down) is marked inactive because it runs on an announced schedule
// rather than continuously, so the generator dropped it and shipped the
// satellite with nothing but an APRS digipeater. An operator comparing
// against any other tracker then finds a frequency plan missing the only
// thing anybody works that bird on.
//
// Not fixed automatically: "inactive" is right far more often than it is
// wrong, and reviving every dead transponder would fill the list with
// satellites that answer nothing. It is REPORTED, so the next regeneration
// is read with this in front of it and the handful worth curating are
// curated — a scheduled transponder belongs in the plan with "(scheduled)"
// in its label, the way PO-101 and now LO-90 carry it.
reportRefused(txs, feed, covered, byNORAD)
added := 0
for n, list := range byNORAD {
b := sat.Bird{Name: displayName(feed[n]), NORAD: n}
@@ -442,3 +459,34 @@ func die(err error) {
fmt.Fprintln(os.Stderr, "satgen:", err)
os.Exit(1)
}
// reportRefused names the satellites whose only two-way path was refused for
// being inactive, so the operator running this can decide about each one.
//
// The output is deliberately a question and not a change: SatNOGS calling a
// transponder inactive is usually correct, and the exceptions are the birds
// whose transponder is switched on to a schedule rather than left running.
func reportRefused(txs []transmitter, feed map[int]string, covered map[int]bool, kept map[int][]transmitter) {
var lines []string
for _, t := range txs {
if t.Status == "active" || t.UplinkLow <= 0 || t.DownlinkLow <= 0 {
continue
}
if feed[t.NORAD] == "" || covered[t.NORAD] || len(kept[t.NORAD]) > 0 {
continue
}
lines = append(lines, fmt.Sprintf(" ? %5d %-22s %-4s %.3f up / %.3f down %q",
t.NORAD, displayName(feed[t.NORAD]), adifMode(t.Mode),
float64(t.UplinkLow)/1e6, float64(t.DownlinkLow)/1e6, t.Description))
}
if len(lines) == 0 {
return
}
sort.Strings(lines)
fmt.Println("\nRefused as inactive, and nothing else was kept for these satellites.")
fmt.Println("A transponder that runs to a schedule looks exactly like a dead one here:")
for _, l := range lines {
fmt.Println(l)
}
fmt.Println()
}