← Alle Artikel

7. August 2026  ·  🔭 Astronomie

Wenn das N.I.N.A.-Orbitals-Plugin keine MPC-Kometendaten mehr lädt – bei Starfront und anderen Remote-Hosting-Standorten

Beim ersten Versuch mit einem Kometen habe ich 19 Grad danebengelegen. Die Hälfte davon ging darauf zurück, dass das Orbitals-Plugin in N.I.N.A. seine Kometendaten nicht vom Minor Planet Center laden konnte.

Der Fehler kommt seitdem zuverlässig:

Failed to update comet elements. A connection attempt failed because the
connected party did not properly respond after a period of time…
(www.minorplanetcenter.net:443)

Zwei Reflexe hatte ich sofort – und beide führen ins Leere. Der eine: einfach auf JPL umstellen. Der andere: die Datei von Hand herunterladen und ins Verzeichnis legen. Warum keiner von beiden funktioniert, und was stattdessen hilft, steht hier.

Reflex 1: „Dann nehme ich eben JPL“

Das Plugin kann Kometenelemente aus zwei Quellen holen. Bei JPL läuft der Download zuverlässig, die Datei ist kleiner, der Parser robuster. Verlockend.

Nur ist das genau die Quelle, die mich die erste Nacht gekostet hat. JPL veröffentlicht die Bahnelemente mit der Epoche der Bahnbestimmung – bei 10P/Tempel 2 ist das 2016. Für JPL ist das kein Problem, weil Horizons diese Werte numerisch integriert, mit Planetenstörungen und Ausgasungsmodell. Das Plugin rechnet aber zweikörperlich, und über zehn Jahre summiert sich das auf 9,3 Grad.

Das MPC führt die Epoche dagegen laufend nach. Für Kometen ist es die richtige Quelle – und deshalb lohnt sich der Umweg.

Bei Asteroiden stellt sich die Frage nicht: die kommen immer von JPL, da gibt es keine Wahl.

Erst die Diagnose: liegt es an mir oder am MPC?

Bevor man irgendetwas repariert, der Test, der mir die entscheidende Information gebracht hat – zwei Minuten, kein Werkzeug. Die URL

https://www.minorplanetcenter.net/iau/MPCORB/CometEls.txt

auf beiden Rechnern im Browser öffnen: einmal zu Hause, einmal auf dem Rechner bei der Rig in Starfront.

Befund Bedeutung
Zu Hause kommt nichts, auf dem Rig auch nichtDas MPC hat gerade selbst ein Problem. Später nochmal.
Zu Hause kommt es, auf dem Rig sofortEs lag an der Zwölf-Stunden-Sperre. Einmal Update klicken und laufen lassen.
Zu Hause kommt es, auf dem Rig nach über 100 SekundenDas Plugin wird es nie schaffen – es bricht hart bei 100 s ab.
Zu Hause kommt es, auf dem Rig gar nichtDer Standort ist blockiert. Das ist mein Fall.

Bei mir liefert das MacBook in Dänemark die Datei sofort, der Beelink in Texas läuft in die Zeitüberschreitung. Damit ist es kein Plugin-Fehler und kein Serverausfall. Es liegt an der Adresse, mit der der Rechner in Texas nach außen auftritt.

Warum sich ein ganzes Gelände eine Sperre teilt

Die Regel des MPC lautet seit dem 25. Februar 2015:

„…only one download of each file per 12 hours is permitted per IP address. Subsequent attempts to download the same file within 12 hours will return a blank file… Due to the way that the scheme works, an aborted download will trigger the 12-hour block.“

Pro IP-Adresse. Nicht pro Nutzer, nicht pro Rechner.

Starfront ist nach eigenen Angaben die größte Remote-Sternwarte der Welt: über 900 Teleskope in Betrieb, verteilt auf 14 Observatoriumsgebäude, über 900 Kunden. Angebunden ist das Ganze über einen gemeinsamen Glasfaseranschluss – Starfront nennt ihn selbst „Fiber Backbone“; jeder Mieter bekommt daran einen Ethernet-Anschluss am Pier. Und die Bandbreite bezeichnet die FAQ ausdrücklich als „communal resource“: ein gemeinsam genutztes Gut.

Und wer sich eine Leitung teilt, teilt sich in aller Regel auch die öffentliche IPv4-Adresse. Für das MPC sind wir dann nicht 900 Sternwarten, sondern ein einziger Nutzer.

Und CometEls.txt ist genau die Datei, die alle ziehen: Orbitals, Stellarium, Astrometrica, SkySafari, TheSkyX, jedes Kometenskript. Bei über 900 Rigs auf dem Gelände reicht ein einziger Nachbar, dessen Software morgens automatisch aktualisiert – dann ist das Kontingent des ganzen Standorts weg, bevor ich aufgestanden bin. Und weil bereits ein abgebrochener Versuch die Sperre auslöst, halten die Rigs sie sich gegenseitig am Leben.

Daraus folgt eine Regel, die auch für Einzelplätze gilt: Nach einem Timeout nicht nochmal klicken. Jeder Versuch startet die zwölf Stunden neu. Genau in diese Falle bin ich am ersten Abend getappt.

Ein Detail, das nicht ganz passt

Bei aller Plausibilität – eine Sache stimmt nicht. Das MPC beschreibt als Folge der Sperre eine leere Datei, keine Zeitüberschreitung. Eine leere Datei sähe im Plugin anders aus: Der Download liefe durch, und der Parser fände null Kometen.

Ein Verbindungs-Timeout deutet auf etwas Härteres hin. Das MPC sperrt auffällige Adressen nämlich auch komplett aus; im Statusverlauf steht das mehrfach, sinngemäß „the offending IP addresses have been blocked“. Über 900 Rechner hinter einer Adresse sehen von außen exakt wie ein einzelner Nutzer aus, der den Server hämmert – genau das, wogegen die Sperre gebaut wurde.

Das bleibt eine Vermutung. Ebenso möglich: eine Firewall am Standort, ein Routing- oder IPv6-Problem. Sicher ist nur, dass mein MacBook dieselbe Datei bekommt und der Rechner in Texas nicht.

Wer es genauer wissen will: auf dem Rig ifconfig.me aufrufen und die Adresse mit der eines Nachbarn vergleichen. Dieselbe Adresse bei allen bedeutet ein gemeinsames Kontingent. Melden kann man das beim MPC – aber es ist die IP des Hosters, also ein Fall für den Betreiber, nicht für einen einzelnen Mieter.

Reflex 2: „Dann kopiere ich die Datei eben von Hand rüber“

Naheliegend, funktioniert aber nicht. Das habe ich im Quellcode des Plugins nachgesehen – es steht unter MPL-2.0 offen, und ich habe es von Claude AI analysieren lassen.

Das Ergebnis in einem Satz: Die heruntergeladene Textdatei wird nie gespeichert. Sie wird direkt aus dem HTTP-Stream geparst, und nur das Ergebnis landet auf der Platte – als GZip-komprimierte Binärdatei im protobuf-net-Format. Der Textparser läuft ausschließlich im Download-Pfad; beim Start liest das Plugin nur diese Binärdatei.

Eine CometEls.txt ins Verzeichnis zu legen, bewirkt also exakt nichts. Wer von Hand einspielen will, muss die Binärdatei selbst erzeugen.

Sie liegt hier:

%LOCALAPPDATA%\NINA\OrbitalElements\
Datei wird gelesen, wenn
MPC_CometElements.bin.gzQuelle = MPC
CometElements.bin.gzQuelle = JPL
NumberedAsteroidsElements.bin.gzimmer (Asteroiden)
UnnumberedAsteroidsElements.bin.gzimmer (Asteroiden)

Zwei Nebenbefunde, die praktisch relevant sind:

Der Timeout liegt hart bei 100 Sekunden – das Plugin setzt keinen eigenen, es gilt der .NET-Standard, und es gibt keine Einstellung dafür. Wenn das MPC länger braucht, ist es aussichtslos.

„Last Updated“ kommt aus dem Dateidatum, nicht aus einer Datenbank. Vor jedem Download vergleicht das Plugin dieses Datum mit dem Last-Modified des Servers und meldet „already up to date“, wenn die lokale Datei neuer ist. Eine selbst erzeugte Datei bremst also künftige Update-Versuche aus – was hier sogar erwünscht ist.

Das Skript

mpc_to_orbitals.py lädt CometEls.txt (oder liest eine lokale Kopie), parst die Spalten und schreibt die Binärdatei im erwarteten Format. Es braucht keine Fremdbibliotheken, nur Python 3, und läuft unter Windows, macOS und Linux.

⬇️ mpc_to_orbitals.py herunterladen  ·  8,6 kB, Python 3, keine Abhängigkeiten – identisch mit dem Listing am Ende dieses Artikels.

Entscheidend ist die Frage, welchen Schritt man auf welchem Rechner macht:

Schritt wo
Herunterladenzu Hause – auf dem Rig geht es ja gerade nicht
Umwandelnegal, beides geht
Ablegen und N.I.N.A. neu startenauf dem Rig

Der heimische Rechner wird also ausschließlich zum Herunterladen gebraucht. Daraus ergeben sich zwei Varianten; der Unterschied ist nur, was man transferiert.

Weg A: die fertige Binärdatei transferieren

Empfohlen, weil auf dem Rig dann gar kein Python installiert sein muss. Die Binärdatei ist plattformneutral; es spielt keine Rolle, welches Betriebssystem sie erzeugt hat.

Zu Hause:

curl -O https://www.minorplanetcenter.net/iau/MPCORB/CometEls.txt
python3 mpc_to_orbitals.py --input CometEls.txt --out ~/Desktop/MPC_CometElements.bin.gz
python3 mpc_to_orbitals.py --verify --out ~/Desktop/MPC_CometElements.bin.gz

Auf dem Rig: die MPC_CometElements.bin.gz per Remote-Desktop-Zwischenablage oder Netzlaufwerk nach

C:\Users\<Benutzer>\AppData\Local\NINA\OrbitalElements\

kopieren, dann N.I.N.A. neu starten.

Weg B: die Textdatei transferieren

Braucht Python auf dem Rig, dafür muss man den Zielpfad nicht kennen – das Skript findet ihn selbst.

Zu Hause: nur herunterladen.

curl -O https://www.minorplanetcenter.net/iau/MPCORB/CometEls.txt

Auf dem Rig: die CometEls.txt hinkopieren und umwandeln lassen, dann N.I.N.A. neu starten.

python mpc_to_orbitals.py --input CometEls.txt
python mpc_to_orbitals.py --verify

Statt N.I.N.A. neu zu starten, kann man im Plugin auch die Quelle einmal MPC → JPL → MPC schalten; das löst ebenfalls ein Neuladen aus.

Ein Fallstrick beim Transfer: Die .bin.gz darf unterwegs nicht entpackt werden. Safari packt heruntergeladene .gz-Dateien gern automatisch aus. Beim Kopieren über RDP oder ein Netzlaufwerk passiert das nicht, aber wer die Datei per Mail oder Cloud schickt, sollte hinterher kontrollieren: Sie muss auf .bin.gz enden und ist nur ein paar hundert Kilobyte groß.

Ein Bug, der früher oder später jeden trifft

Beim Nachbauen des Parsers bin ich auf Issue #12 gestoßen, offen seit März 2026. Das MPC liefert gelegentlich einen Datensatz, in dem statt eines Zahlenwerts nur Sternchen stehen:

CJ54O010  2085 07 20.7056 *********  9.999999   76.9423 ...  C/1954 O1 (Vozarova)

Der Parser des Plugins wirft daraufhin eine Exception, und der gesamte Update schlägt fehl – eine einzige kaputte Zeile unter tausenden reißt alles mit. Der Melder schlug vor, solche Datensätze einfach zu überspringen. Passiert ist bisher nichts.

Mein Skript überspringt sie und sagt hinterher, wie viele es waren.

Das vollständige Skript

⬇️ mpc_to_orbitals.py herunterladen  ·  8,6 kB, Python 3, keine Abhängigkeiten – identisch mit dem Listing am Ende dieses Artikels.

#!/usr/bin/env python3
"""
mpc_to_orbitals.py — fetch comet orbital elements from the Minor Planet Center
and write them in the binary format used by the N.I.N.A. "Orbitals" plugin.

Background: the plugin downloads CometEls.txt from the MPC, parses it in memory
and stores the result as a GZip-compressed protobuf-net stream. The text file
itself is never saved and never read back from disk. So if the built-in download
does not work for you, you have to produce that binary file yourself.

Target:  %LOCALAPPDATA%\\NINA\\OrbitalElements\\MPC_CometElements.bin.gz
         (only read when the plugin's comet source is set to MPC)

Usage:
    python mpc_to_orbitals.py                 # download from MPC and write
    python mpc_to_orbitals.py --input C.txt   # use a local CometEls.txt
    python mpc_to_orbitals.py --verify        # read the written file back
    python mpc_to_orbitals.py --out X.bin.gz  # different target path

Runs on Windows, macOS and Linux. Standard library only, no dependencies.
"""

import argparse, gzip, math, os, struct, sys, urllib.request

MPC_URL = "https://www.minorplanetcenter.net/iau/MPCORB/CometEls.txt"
GM_SUN  = 1.32712440018e20          # m^3/s^2, same as GravitationalParameter.Sun
RAD     = math.pi / 180.0

# ---------------------------------------------------------------- protobuf ---

def _varint(n):
    out = bytearray()
    while True:
        b = n & 0x7F
        n >>= 7
        out.append(b | (0x80 if n else 0))
        if not n:
            return bytes(out)

def _f64(field, value):
    return bytes([(field << 3) | 1]) + struct.pack("<d", value)

def _bytes(field, payload):
    return bytes([(field << 3) | 2]) + _varint(len(payload)) + payload

def _gm(field, value):
    inner = _f64(1, value) if value != 0.0 else b""   # protobuf-net omits defaults
    return _bytes(field, inner)

def encode_element(name, epoch_jd, q_au, ecc, i_rad, w_rad, node_rad, tp_jd):
    """One OrbitalElements record as declared in Kepler.cs."""
    m  = _bytes(1, name.encode("utf-8"))
    m += _gm(2, GM_SUN)      # PrimaryGravitationalParameter
    m += _gm(3, 0.0)         # SecondaryGravitationalParameter
    m += _f64(4, epoch_jd)
    m += _f64(5, q_au)
    m += _f64(6, ecc)
    m += _f64(7, i_rad)
    m += _f64(8, w_rad)
    m += _f64(9, node_rad)
    m += _f64(10, tp_jd)
    # Fields 11 (M_MeanAnomalyAtEpoch) and 12 (a_SemiMajorAxis_au) are left out
    # (null), exactly like MPCCometElements.ToOrbitalElements() does.
    return _bytes(1, m)      # SerializeWithLengthPrefix(Base128, field number 1)

# -------------------------------------------------------------------- date ---

def julian_date(year, month, day, hour=0.0):
    """NOVAS julian_date(): integer day number plus fraction of day.

    Careful: C integer division truncates toward zero, Python's // floors.
    For (month-14)/12 that differs for months 1..13 and puts the result
    two days off.
    """
    a = math.trunc((month - 14) / 12)
    jd = (day - 32075
          + 1461 * (year + 4800 + a) // 4
          + 367 * (month - 2 - a * 12) // 12
          - 3 * ((year + 4900 + a) // 100) // 4)
    return jd - 0.5 + hour / 24.0

# ------------------------------------------------------------------ parser ---

COLS = dict(number=(0,4), otype=(4,5), prov=(5,14), tp_y=(14,19), tp_m=(19,22),
            tp_d=(22,30), q=(30,41), e=(41,51), w=(51,61), node=(61,71),
            i=(71,81), epoch=(81,89), name=(100,159))

def _num(s):
    s = s.strip()
    if not s or "*" in s:
        # The MPC occasionally ships asterisks instead of a value. The plugin
        # aborts the whole update on those (issue #12); we skip the record.
        raise ValueError(f"unusable value {s!r}")
    return float(s)

def parse_line(line):
    g = lambda k: line[COLS[k][0]:COLS[k][1]]
    name = g("name").strip()
    if not name:
        raise ValueError("no name")
    tp_y, tp_m = int(g("tp_y")), int(g("tp_m"))
    tp_d = _num(g("tp_d"))
    day  = int(tp_d)
    tp_jd = julian_date(tp_y, tp_m, day, (tp_d - day) * 24.0)

    ep = g("epoch").strip()
    epoch_jd = (julian_date(int(ep[0:4]), int(ep[4:6]), int(ep[6:8]))
                if len(ep) == 8 else float("nan"))

    # Angles go in as radians; a is derived from q and e by the plugin at runtime.
    return dict(name=name, epoch_jd=epoch_jd, q_au=_num(g("q")), ecc=_num(g("e")),
                i_rad=_num(g("i")) * RAD, w_rad=_num(g("w")) * RAD,
                node_rad=_num(g("node")) * RAD, tp_jd=tp_jd)

# ------------------------------------------------------------------ reader ---

def read_back(path, limit=5):
    """Minimal decoder, used by --verify to sanity-check what was written."""
    data = gzip.open(path, "rb").read()
    pos, n, shown = 0, 0, []
    while pos < len(data):
        assert data[pos] == 0x0A, f"unexpected tag at {pos}"
        pos += 1
        ln, shift = 0, 0
        while True:
            b = data[pos]; pos += 1
            ln |= (b & 0x7F) << shift; shift += 7
            if not b & 0x80: break
        msg, pos = data[pos:pos+ln], pos + ln
        n += 1
        if len(shown) < limit:
            p, rec = 0, {}
            while p < len(msg):
                tag = msg[p]; p += 1
                fld, wt = tag >> 3, tag & 7
                if wt == 1:
                    rec[fld] = struct.unpack("<d", msg[p:p+8])[0]; p += 8
                elif wt == 2:
                    l2, sh = 0, 0
                    while True:
                        b = msg[p]; p += 1
                        l2 |= (b & 0x7F) << sh; sh += 7
                        if not b & 0x80: break
                    val, p = msg[p:p+l2], p + l2
                    rec[fld] = val.decode("utf-8") if fld == 1 else val
            shown.append(rec)
    return n, shown

# -------------------------------------------------------------------- main ---

def default_out():
    """Default target on Windows only; elsewhere --out is required."""
    base = os.environ.get("LOCALAPPDATA")
    if not base:
        sys.exit("No %LOCALAPPDATA% found (not Windows?).\n"
                 "Please give a target, e.g.:\n"
                 "  --out ~/Desktop/MPC_CometElements.bin.gz\n"
                 "then copy the finished file to\n"
                 "  C:\\Users\\<user>\\AppData\\Local\\NINA\\OrbitalElements\\\n"
                 "without unpacking it.")
    return os.path.join(base, "NINA", "OrbitalElements", "MPC_CometElements.bin.gz")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--input", help="local CometEls.txt instead of downloading")
    ap.add_argument("--out", default=None, help="target path for the .bin.gz")
    ap.add_argument("--timeout", type=int, default=600,
                    help="download timeout in seconds (default 600; "
                         "the plugin itself is stuck at 100)")
    ap.add_argument("--verify", action="store_true",
                    help="read the written file back and show the first records")
    a = ap.parse_args()
    out = a.out or default_out()

    if a.verify:
        n, recs = read_back(out)
        print(f"{out}\n{n} records\n")
        for r in recs:
            print(f"  {r[1]:<34} q={r[5]:.6f} e={r[6]:.6f} "
                  f"i={math.degrees(r[7]):8.4f}deg tp={r[10]:.4f}")
        return

    if a.input:
        text = open(a.input, encoding="utf-8", errors="replace").read()
    else:
        print(f"Downloading {MPC_URL} (timeout {a.timeout}s) ...")
        req = urllib.request.Request(MPC_URL, headers={
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
            "Accept": "text/plain,*/*"})
        with urllib.request.urlopen(req, timeout=a.timeout) as r:
            text = r.read().decode("utf-8", errors="replace")
        print(f"  {len(text):,} characters received")

    ok, bad = [], 0
    for line in text.splitlines():
        if len(line) < 100:
            continue
        try:
            ok.append(parse_line(line))
        except Exception:
            bad += 1

    if not ok:
        sys.exit("No valid records found - is this really CometEls.txt?")

    # Write to a temp file first, then swap it in, so a crash cannot leave
    # the plugin with a half-written file.
    d = os.path.dirname(out)
    if d:
        os.makedirs(d, exist_ok=True)
    tmp = out + ".tmp"
    with gzip.open(tmp, "wb", compresslevel=9) as f:
        for e in ok:
            f.write(encode_element(e["name"], e["epoch_jd"], e["q_au"], e["ecc"],
                                   e["i_rad"], e["w_rad"], e["node_rad"], e["tp_jd"]))
    os.replace(tmp, out)

    print(f"\n{len(ok)} comets written, {bad} bad lines skipped")
    print(f"-> {out}")
    print("\nRestart N.I.N.A. (or toggle the source MPC->JPL->MPC), then check")
    print("that 'Comets (...)' shows the expected count.")

if __name__ == "__main__":
    main()

Quellen

ghilios/NINA.Joko.Plugin.Orbitals – Quellcode, MPL-2.0
Issue #12: „Orbitals is failing with a bad record from MPC on comet updates“
MPC Status Page – Downloadsperre pro IP-Adresse, seit 25.02.2015
MPC – Contacting the Minor Planet Center – Meldung blockierter IP-Adressen
Starfront Observatories – About Us und FAQ – Anzahl Teleskope, Gebäude, Netzanbindung (Stand August 2026)
JPL Horizons – Vergleichsephemeride

🔭 Wie es dazu kam: Mein erster Komet – und warum ich 19 Grad danebenlag →  ·  Das Rig dazu: Mein Rig @ Starfront in Texas