From 3ce74ef8d77e9608081ef0198f88b38114c008b7 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 10 Sep 2026 10:27:15 +0200 Subject: [PATCH] fix(sat): LilacSat-2 keeps its FM transponder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- changelog.json | 6 ++++-- cmd/satgen/main.go | 48 +++++++++++++++++++++++++++++++++++++++++ internal/sat/birds.json | 12 ++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/changelog.json b/changelog.json index d087654..64c3024 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,14 @@ "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.", "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": [ "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 s’accorde à 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 l’antenne est inutilisable jusqu’à la fin : une confirmation est donc demandée. Les contrôleurs Ultrabeam n’ont pas cette commande et le disent, au lieu de faire semblant. Rétracter les éléments existait déjà et s’explique maintenant : c’est 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 l’antenne franchit le nord. Un rotor à 020° tourné dans le sens antihoraire annonce 020, 010, 000, 359, 358… et l’animation 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 l’autre sens. Le faisceau suit désormais un angle cumulé : ce qui est dessiné est le mouvement réel de l’antenne. Les deux lobes d’une antenne bidirectionnelle sont traités séparément, puisqu’ils ne franchissent pas le nord au même moment.", - "Le widget rotor n’annonce 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 s’allumait et s’éteignait toute la soirée sur une antenne qui n’avait pas tourné. Ce qui distingue une rotation de la météo n’est pas l’amplitude 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 n’annonce 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 s’allumait et s’éteignait toute la soirée sur une antenne qui n’avait pas tourné. Ce qui distingue une rotation de la météo n’est pas l’amplitude 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 qu’en continu, ce qui, vu d’une 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 qu’il a écartés pour cette raison sans rien garder d’autre, afin que la prochaine régénération ne les passe plus sous silence." ] }, { diff --git a/cmd/satgen/main.go b/cmd/satgen/main.go index 25829ee..4426633 100644 --- a/cmd/satgen/main.go +++ b/cmd/satgen/main.go @@ -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() +} diff --git a/internal/sat/birds.json b/internal/sat/birds.json index 2dd60fc..8614679 100644 --- a/internal/sat/birds.json +++ b/internal/sat/birds.json @@ -390,15 +390,25 @@ ] }, { - "name": "LILACSAT-2", + "name": "LO-90", "norad": 40908, "transponders": [ + { + "label": "FM voice transponder (scheduled)", + "mode": "FM", + "down_lo": 437200000, + "up_lo": 144350000 + }, { "label": "APRS Digipeater", "mode": "DATA", "down_lo": 144390000, "up_lo": 144390000 } + ], + "aliases": [ + "LILACSAT-2", + "CAS-3H" ] }, {