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
+4 -2
View File
@@ -5,12 +5,14 @@
"en": [ "en": [
"SteppIR: a Calibrate button, beside Retract. It drives every element to its end stop so the controller re-learns where zero is — the cure for an antenna that tunes to the wrong length after a power cut mid-move, after the elements were pushed by hand, or after a motor slipped. It takes minutes and the antenna is unusable until it finishes, so it asks first. Ultrabeam controllers have no such command and say so rather than pretending. Retracting the elements was already there and now explains itself: it is the storage position, and the next tune brings them back out on its own.", "SteppIR: a Calibrate button, beside Retract. It drives every element to its end stop so the controller re-learns where zero is — the cure for an antenna that tunes to the wrong length after a power cut mid-move, after the elements were pushed by hand, or after a motor slipped. It takes minutes and the antenna is unusable until it finishes, so it asks first. Ultrabeam controllers have no such command and say so rather than pretending. Retracting the elements was already there and now explains itself: it is the storage position, and the next tune brings them back out on its own.",
"On the rotor dial, the beam no longer whips a full turn round the compass when the antenna crosses north. A rotator at 020° turned anticlockwise reports 020, 010, 000, 359, 358 … and the animation travelled from 20° to 359° the long way — a complete revolution on screen while the mast moved forty degrees the other way. The beam now follows an accumulated angle, so what is drawn is the way the antenna is actually turning. Both lobes of a bidirectional antenna are handled separately, since they cross north at different moments.", "On the rotor dial, the beam no longer whips a full turn round the compass when the antenna crosses north. A rotator at 020° turned anticlockwise reports 020, 010, 000, 359, 358 … and the animation travelled from 20° to 359° the long way — a complete revolution on screen while the mast moved forty degrees the other way. The beam now follows an accumulated angle, so what is drawn is the way the antenna is actually turning. Both lobes of a bidirectional antenna are handled separately, since they cross north at different moments.",
"The rotor widget stops claiming the antenna is turning every time the wind moves it. A threshold alone could not tell the difference — a gust pushes a beam well past four degrees and back, and each excursion counted, so Stop lit and went out all evening on an antenna that had not turned. What separates a rotation from the weather is not how far the reading moves but which way: a rotor under power advances, gust after gust reverses. A step now counts only when the previous one went the same way, so movement is announced one poll later on a mast that takes half a minute to cross a pass, and never at all on a windy afternoon." "The rotor widget stops claiming the antenna is turning every time the wind moves it. A threshold alone could not tell the difference — a gust pushes a beam well past four degrees and back, and each excursion counted, so Stop lit and went out all evening on an antenna that had not turned. What separates a rotation from the weather is not how far the reading moves but which way: a rotor under power advances, gust after gust reverses. A step now counts only when the previous one went the same way, so movement is announced one poll later on a mast that takes half a minute to cross a pass, and never at all on a windy afternoon.",
"LilacSat-2 gets its FM transponder — 144.350 up, 437.200 down — and its proper name, LO-90. It was shipped with nothing but an APRS digipeater, because the transponder database marks that transponder inactive: it runs to an announced schedule rather than continuously, which from a database looks exactly like a dead one. The label says \"(scheduled)\" so nobody wonders why it is quiet. The generator now also lists the satellites it refused on that ground and kept nothing else for, so the next regeneration is not silent about them."
], ],
"fr": [ "fr": [
"SteppIR : un bouton Calibrer, à côté de Rétracter. Il amène chaque élément en butée pour que le contrôleur retrouve son zéro — le remède à une antenne qui saccorde à la mauvaise longueur après une coupure en pleine course, après avoir poussé les éléments à la main, ou après un moteur qui a glissé. Cela prend plusieurs minutes et lantenne est inutilisable jusqu’à la fin : une confirmation est donc demandée. Les contrôleurs Ultrabeam nont pas cette commande et le disent, au lieu de faire semblant. Rétracter les éléments existait déjà et sexplique maintenant : cest la position de rangement, et le prochain accord les fait ressortir tout seuls.", "SteppIR : un bouton Calibrer, à côté de Rétracter. Il amène chaque élément en butée pour que le contrôleur retrouve son zéro — le remède à une antenne qui saccorde à la mauvaise longueur après une coupure en pleine course, après avoir poussé les éléments à la main, ou après un moteur qui a glissé. Cela prend plusieurs minutes et lantenne est inutilisable jusqu’à la fin : une confirmation est donc demandée. Les contrôleurs Ultrabeam nont pas cette commande et le disent, au lieu de faire semblant. Rétracter les éléments existait déjà et sexplique maintenant : cest la position de rangement, et le prochain accord les fait ressortir tout seuls.",
"Sur le cadran du rotor, le faisceau ne fait plus un tour complet de la boussole quand lantenne franchit le nord. Un rotor à 020° tourné dans le sens antihoraire annonce 020, 010, 000, 359, 358… et lanimation allait de 20° à 359° par le chemin long — une révolution complète à l’écran pendant que le pylône bougeait de quarante degrés dans lautre sens. Le faisceau suit désormais un angle cumulé : ce qui est dessiné est le mouvement réel de lantenne. Les deux lobes dune antenne bidirectionnelle sont traités séparément, puisquils ne franchissent pas le nord au même moment.", "Sur le cadran du rotor, le faisceau ne fait plus un tour complet de la boussole quand lantenne franchit le nord. Un rotor à 020° tourné dans le sens antihoraire annonce 020, 010, 000, 359, 358… et lanimation allait de 20° à 359° par le chemin long — une révolution complète à l’écran pendant que le pylône bougeait de quarante degrés dans lautre sens. Le faisceau suit désormais un angle cumulé : ce qui est dessiné est le mouvement réel de lantenne. Les deux lobes dune antenne bidirectionnelle sont traités séparément, puisquils ne franchissent pas le nord au même moment.",
"Le widget rotor nannonce plus une antenne en rotation chaque fois que le vent la bouge. Un simple seuil ne pouvait pas faire la différence — une rafale pousse une beam bien au-delà de quatre degrés puis la ramène, et chaque écart comptait : Stop sallumait et s’éteignait toute la soirée sur une antenne qui navait pas tourné. Ce qui distingue une rotation de la météo nest pas lamplitude mais le sens : un rotor sous tension avance, une rafale revient. Un écart ne compte donc que si le précédent allait dans le même sens — la rotation est annoncée un relevé plus tard sur un pylône qui met trente secondes à traverser, et plus du tout par vent fort." "Le widget rotor nannonce plus une antenne en rotation chaque fois que le vent la bouge. Un simple seuil ne pouvait pas faire la différence — une rafale pousse une beam bien au-delà de quatre degrés puis la ramène, et chaque écart comptait : Stop sallumait et s’éteignait toute la soirée sur une antenne qui navait pas tourné. Ce qui distingue une rotation de la météo nest pas lamplitude mais le sens : un rotor sous tension avance, une rafale revient. Un écart ne compte donc que si le précédent allait dans le même sens — la rotation est annoncée un relevé plus tard sur un pylône qui met trente secondes à traverser, et plus du tout par vent fort.",
"LilacSat-2 récupère son transpondeur FM — 144,350 en montée, 437,200 en descente — et son vrai nom, LO-90. Il était livré avec un simple digipeater APRS, parce que la base de transpondeurs marque ce transpondeur comme inactif : il fonctionne selon un calendrier annoncé plutôt quen continu, ce qui, vu dune base de données, ressemble exactement à un transpondeur mort. Le libellé indique « (scheduled) » pour que personne ne se demande pourquoi il est muet. Le générateur liste désormais aussi les satellites quil a écartés pour cette raison sans rien garder dautre, afin que la prochaine régénération ne les passe plus sous silence."
] ]
}, },
{ {
+48
View File
@@ -140,6 +140,23 @@ func main() {
} }
byNORAD[t.NORAD] = append(byNORAD[t.NORAD], t) 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 added := 0
for n, list := range byNORAD { for n, list := range byNORAD {
b := sat.Bird{Name: displayName(feed[n]), NORAD: n} b := sat.Bird{Name: displayName(feed[n]), NORAD: n}
@@ -442,3 +459,34 @@ func die(err error) {
fmt.Fprintln(os.Stderr, "satgen:", err) fmt.Fprintln(os.Stderr, "satgen:", err)
os.Exit(1) 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()
}
+11 -1
View File
@@ -390,15 +390,25 @@
] ]
}, },
{ {
"name": "LILACSAT-2", "name": "LO-90",
"norad": 40908, "norad": 40908,
"transponders": [ "transponders": [
{
"label": "FM voice transponder (scheduled)",
"mode": "FM",
"down_lo": 437200000,
"up_lo": 144350000
},
{ {
"label": "APRS Digipeater", "label": "APRS Digipeater",
"mode": "DATA", "mode": "DATA",
"down_lo": 144390000, "down_lo": 144390000,
"up_lo": 144390000 "up_lo": 144390000
} }
],
"aliases": [
"LILACSAT-2",
"CAS-3H"
] ]
}, },
{ {