GNSS seed data

Ready-made orbit data for GPS, Galileo, BeiDou, GLONASS and QZSS. Load it into a GNSS receiver and it finds its first fix from cold in seconds rather than minutes. Free to use, with no account and no key.

Built 1 minute ago from the BKG live stream. The next build is due in about 9 minutes.

  • 32 GPS
  • 30 Galileo
  • 36 BeiDou
  • 24 GLONASS
  • 6 QZSS
The 128 satellites in this build around the Earth, to scale, seen from above 30 degrees north, 10 degrees westBeiDou C01BeiDou C03BeiDou C04BeiDou C07BeiDou C08BeiDou C10BeiDou C11BeiDou C12BeiDou C19BeiDou C20BeiDou C26BeiDou C29BeiDou C30BeiDou C31BeiDou C32BeiDou C35BeiDou C36BeiDou C39Galileo E02Galileo E04Galileo E05Galileo E06Galileo E09Galileo E10Galileo E11Galileo E15Galileo E16Galileo E18Galileo E19Galileo E25Galileo E28Galileo E30Galileo E34GPS G04GPS G05GPS G06GPS G09GPS G11GPS G13GPS G15GPS G17GPS G18GPS G19GPS G20GPS G21GPS G22GPS G24GPS G25GPS G28GPS G29QZSS J02QZSS J03QZSS J04QZSS J07QZSS J08QZSS J09GLONASS R01GLONASS R02GLONASS R07GLONASS R08GLONASS R12GLONASS R13GLONASS R14GLONASS R15GLONASS R17GLONASS R22GLONASS R23GLONASS R24BeiDou C02BeiDou C06BeiDou C09BeiDou C13BeiDou C14BeiDou C21BeiDou C22BeiDou C23BeiDou C24BeiDou C25BeiDou C27BeiDou C28BeiDou C33BeiDou C34BeiDou C37BeiDou C38BeiDou C41BeiDou C42Galileo E03Galileo E07Galileo E08Galileo E12Galileo E13Galileo E14Galileo E21Galileo E23Galileo E26Galileo E27Galileo E29Galileo E31Galileo E32Galileo E33GPS G01GPS G02GPS G03GPS G07GPS G08GPS G10GPS G14GPS G16GPS G23GPS G26GPS G27GPS G30GPS G31GPS G32GLONASS R03GLONASS R04GLONASS R05GLONASS R06GLONASS R09GLONASS R10GLONASS R11GLONASS R16GLONASS R18GLONASS R19GLONASS R20GLONASS R21
Every satellite in this build, to scale, where its own orbit data puts it now, trailing the last 40 minutes of its path.

Using one of our altimeters? There is nothing to set up. Our altimeters download this data automatically over the air; you do not need to configure anything on this page. It is published here purely so anyone can use the same data in their own projects and devices.

Choose a file

Every file is one frame per satellite, in three encodings. Pick the systems your receiver tracks and the encoding your device can unpack. Both http:// and https:// work on every link, for devices without TLS.

FileSatellitesRawzlibLZMA
Bundles, whole or in 1000-byte chunks
gnss_allGPS, Galileo, BeiDou, GLONASS, QZSS128.bin10.0 KB.z7.3 KB
8 chunks.0.1.2.3.4.5.6.7
.lzma6.5 KB
7 chunks.0.1.2.3.4.5.6
gnss_gps_gal_bds_gloGPS, Galileo, BeiDou, GLONASS122.bin9.5 KB.z7.0 KB
8 chunks.0.1.2.3.4.5.6.7
.lzma6.2 KB
7 chunks.0.1.2.3.4.5.6
gnssglobalGPS, Galileo, BeiDou98.bin8.2 KB.z6.0 KB
7 chunks.0.1.2.3.4.5.6
.lzma5.3 KB
6 chunks.0.1.2.3.4.5
gnss_gps_gloGPS, GLONASS56.bin3.7 KB.z2.8 KB
3 chunks.0.1.2
.lzma2.6 KB
3 chunks.0.1.2
gnss_gps_galGPS, Galileo62.bin4.8 KB.z3.7 KB
4 chunks.0.1.2.3
.lzma3.4 KB
4 chunks.0.1.2.3
One system each, to make your own mix
gnss_gpsGPS32.bin2.4 KB.z1.9 KB.lzma1.8 KB
gnss_galGalileo30.bin2.5 KB.z1.8 KB.lzma1.7 KB
gnss_bdsBeiDou36.bin3.4 KB.z2.4 KB.lzma2.0 KB
gnss_gloGLONASS24.bin1.3 KB.z1009 B.lzma949 B
gnss_qzssQZSS6.bin456 B.z401 B.lzma378 B

Getting it into a receiver

  1. Pick a file from the table, or read index.json to find one in code.
  2. Download it whole, or fetch its chunks .0, .1, .2 in order and join them. The last chunk is under 1000 bytes; a 404 also means you have them all.
  3. Unpack it. zlib with any inflate, LZMA with an LZMA-alone decoder. Raw needs nothing.
  4. Send the receiver the time (MGA-INI-TIME-UTC) and a rough position, then every frame just as it is. Each frame starts B5 62 and carries one satellite.
  5. Watch the MGA-ACK replies if you have turned them on. An ACK says the frame was stored, not that the orbit inside was checked.

Cache busting. These folders are served with caching turned off, so a download should always be the current build. To be certain nothing along the way, such as a proxy or a mobile network’s cache, hands you an old copy, add a random number or the Unix time to the URL, as in gnss_all.lzma?1791849600, and the same on each chunk. The server ignores it.

Working code for a u-blox SAM-M10Q, for an ESP32 or a PC, is on the Example code tab.

Formats

Encodings

.binThe frames, uncompressed.
.zzlib (RFC 1950): 2-byte header, DEFLATE, Adler-32 check.
.lzmaLZMA-alone, as xz --format=lzma writes: 5 bytes of properties (64 KB dictionary), the 8-byte length, then the data. The smallest.

Frames

GPSMGA-GPS-EPH, 76 bytes
GalileoMGA-GAL-EPH, 84 bytes
BeiDouMGA-BDS-EPH, 96 bytes
GLONASSMGA-GLO-EPH, 56 bytes
QZSSMGA-QZSS-EPH, 76 bytes

GLONASS orbit data is good for about 15 minutes either side of its time, so it helps most from a fresh download. It comes from the live streams only: a build made from a fallback file has none.

For programs

Each build writes an index of every file, so code never has to guess a name or a chunk count. The GNSS Seed Data API page sets it all out, with an example.

Data credit. Broadcast ephemeris from the IGS Real-Time Service, by the International GNSS Service (IGS) and its contributing organizations: the merged broadcast ephemeris streams of BKG (Federal Agency for Cartography and Geodesy, Germany), CAS (Chinese Academy of Sciences) and JPL (NASA Jet Propulsion Laboratory), served on BKG’s Ntrip caster, with BKG’s broadcast navigation files as the fallback. If you use these files, please acknowledge the IGS and its contributing organizations too. Land outlines in the picture from Natural Earth.

Terms of use. The underlying ephemeris is public broadcast navigation data. We rebuild and host it as a convenience and place our packaging in the public domain, so you may use these files freely in your own projects, commercial or otherwise. It is provided as-is with no guarantee of availability or accuracy; do not rely on it as a sole safety-critical input. Please fetch on a sensible schedule: the data only changes every 10 minutes, so there is no reason to poll faster than that.

Example: loading it into a u-blox SAM-M10Q

Two complete programs that load the latest data into a SAM-M10Q over its serial port: one for an ESP32 under Arduino, one for a PC or Raspberry Pi in Python. Each quietens the receiver’s NMEA, gives it the time, then sends every frame and waits for the receiver to acknowledge it before sending the next. They follow the same steps as the code in our own Jupiter flight computer, which uses a SAM-M10Q.

Other receivers will differ. These are written for the SAM-M10Q at its factory settings, 9600 baud. Other u-blox receivers take the same messages, but their default baud rate, the systems they track and how quickly they take messages vary, so check your receiver’s manual. Receivers from other makers use their own assistance formats and can’t use these files.

ESP32, Arduinoseed_m10.ino
// Load AltimeterCloud GNSS seed data into a u-blox SAM-M10Q from an ESP32 (Arduino).
// The receiver is on Serial2 at 9600 baud, the SAM-M10Q's factory default. WiFi is needed
// for the download, and NTP for the time the receiver must be given before the orbits.
#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>

const char* SSID = "your-wifi";
const char* PASS = "your-password";
const char* URL  = "http://altimetercloud.com/ubx/gnss_all.bin";   // all five systems, about 10 KB
HardwareSerial& gnss = Serial2;                                   // receiver TX to the ESP32's RX2, and back

// Send one UBX message: sync, class, id, length, payload, then the Fletcher checksum
void ubxSend(uint8_t cls, uint8_t id, const uint8_t* pl, uint16_t len) {
  uint8_t hdr[6] = {0xB5, 0x62, cls, id, (uint8_t)(len & 0xFF), (uint8_t)(len >> 8)};
  uint8_t a = 0, b = 0;
  for (int i = 2; i < 6; i++) { a += hdr[i]; b += a; }
  for (int i = 0; i < len; i++) { a += pl[i]; b += a; }
  gnss.write(hdr, 6); gnss.write(pl, len); gnss.write(a); gnss.write(b);
  gnss.flush();                                         // wait until it has all gone out
}

// Wait for UBX-MGA-ACK-DATA0 (0x13 0x60). Returns 1 accepted, 0 refused, -1 nothing heard
int waitAck(uint32_t ms) {
  uint8_t m[16]; int n = 0; uint32_t t0 = millis();
  while (millis() - t0 < ms) {
    if (!gnss.available()) { delay(1); continue; }
    uint8_t c = gnss.read();
    if (n == 0 && c != 0xB5) continue;                  // skips the NMEA in between
    if (n == 1 && c != 0x62) { n = (c == 0xB5); continue; }
    m[n++] = c;
    if (n == 4 && !(m[2] == 0x13 && m[3] == 0x60)) { n = 0; continue; }
    if (n == 16) return m[6];                           // m[8] holds the reason for a refusal
  }
  return -1;
}

void setup() {
  Serial.begin(115200);
  gnss.begin(9600);
  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) delay(200);
  configTime(0, 0, "pool.ntp.org");
  struct tm u;
  while (!getLocalTime(&u)) delay(200);                 // UTC, to the second

  // 1. Download the bundle into RAM
  HTTPClient http;
  http.begin(URL);
  if (http.GET() != 200) { Serial.println("Download failed"); return; }
  int len = http.getSize();
  if (len <= 0) { Serial.println("No length from the server"); return; }
  uint8_t* buf = (uint8_t*)malloc(len);
  if (!buf) { Serial.println("Not enough memory"); return; }
  WiFiClient* s = http.getStreamPtr();
  int got = 0; uint32_t t0 = millis();
  while (got < len && millis() - t0 < 10000) {
    if (s->available()) got += s->readBytes(buf + got, len - got); else delay(1);
  }
  http.end();

  // 2. Quieten the NMEA while we talk (CFG-UART1OUTPROT-NMEA off), and ask for an
  //    acknowledgement of each assistance message (CFG-NAVSPG-ACKAIDING on), both in RAM only
  const uint8_t nmeaOff[] = {0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x74, 0x10, 0x00};
  const uint8_t ackOn[]   = {0x00, 0x01, 0x00, 0x00, 0x25, 0x00, 0x11, 0x10, 0x01};
  ubxSend(0x06, 0x8A, nmeaOff, sizeof nmeaOff); delay(100);
  ubxSend(0x06, 0x8A, ackOn, sizeof ackOn); delay(100);
  while (gnss.available()) gnss.read();

  // 3. The time first (UBX-MGA-INI-TIME_UTC): without it the orbits are refused
  uint8_t t[24] = {0x10, 0x00, 0x00, 0x80};            // no time pulse reference, leap seconds unknown
  uint16_t y = u.tm_year + 1900;
  t[4] = y & 0xFF; t[5] = y >> 8; t[6] = u.tm_mon + 1; t[7] = u.tm_mday;
  t[8] = u.tm_hour; t[9] = u.tm_min; t[10] = u.tm_sec;
  t[16] = 2;                                            // good to about 2 seconds
  ubxSend(0x13, 0x40, t, sizeof t);
  waitAck(500);

  // 4. Every frame just as it is, one at a time, each answered before the next
  int ok = 0, refused = 0, silent = 0;
  for (int i = 0; i + 8 <= got; ) {
    if (buf[i] != 0xB5 || buf[i + 1] != 0x62) break;
    uint16_t n = buf[i + 4] | (buf[i + 5] << 8);
    if (i + 8 + n > got) break;
    gnss.write(buf + i, n + 8);
    gnss.flush();
    int r = waitAck(300);
    if (r == 1) ok++; else if (r == 0) refused++; else silent++;
    i += n + 8;
  }
  free(buf);
  const uint8_t nmeaOn[] = {0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x74, 0x10, 0x01};
  ubxSend(0x06, 0x8A, nmeaOn, sizeof nmeaOn);           // NMEA back on
  Serial.printf("%d accepted, %d refused, %d not answered\n", ok, refused, silent);
}

void loop() {}
Python, PC or Raspberry Piseed_m10.py
# Load AltimeterCloud GNSS seed data into a u-blox SAM-M10Q from a PC or Raspberry Pi.
# pip install pyserial requests
import datetime, lzma, struct, time
import requests, serial

PORT, BAUD = "/dev/ttyUSB0", 9600         # the SAM-M10Q's factory default

index = requests.get("https://altimetercloud.com/ubx/index.json", timeout=10).json()
bundle = next(x for x in index["bundles"] if x["name"] == "gnss_all")
data = lzma.decompress(requests.get(bundle["lzma"]["url"], timeout=10).content, format=lzma.FORMAT_ALONE)

def ubx(cls, mid, payload):
    """One UBX message: sync, class, id, length, payload, Fletcher checksum."""
    body = bytes([cls, mid]) + struct.pack("<H", len(payload)) + payload
    a = b = 0
    for c in body:
        a = (a + c) & 0xFF
        b = (b + a) & 0xFF
    return b"\xb5\x62" + body + bytes([a, b])

def wait_ack(port, secs=0.3):
    """UBX-MGA-ACK-DATA0: 1 accepted, 0 refused, None nothing heard."""
    end, buf = time.time() + secs, b""
    while time.time() < end:
        buf += port.read(port.in_waiting or 1)
        k = buf.find(b"\xb5\x62\x13\x60")
        if k >= 0 and len(buf) >= k + 16:
            return buf[k + 6]             # buf[k + 8] holds the reason for a refusal
    return None

with serial.Serial(PORT, BAUD, timeout=0.05) as gps:
    # quieten the NMEA while we talk (CFG-UART1OUTPROT-NMEA off), and acknowledge
    # each assistance message (CFG-NAVSPG-ACKAIDING on), both in RAM only
    def valset(key, value):
        gps.write(ubx(0x06, 0x8A, bytes([0, 1, 0, 0]) + struct.pack("<IB", key, value)))
        gps.flush()
        time.sleep(0.1)
    valset(0x10740002, 0)
    valset(0x10110025, 1)
    gps.reset_input_buffer()

    # the time first (UBX-MGA-INI-TIME_UTC): without it the orbits are refused
    now = datetime.datetime.now(datetime.timezone.utc)
    t = struct.pack("<BBBbHBBBBBBIHHI", 0x10, 0, 0, -128, now.year, now.month, now.day,
                    now.hour, now.minute, now.second, 0, now.microsecond * 1000, 1, 0, 0)
    gps.write(ubx(0x13, 0x40, t))
    gps.flush()
    wait_ack(gps, 0.5)

    # every frame just as it is, one at a time, each answered before the next
    counts = {1: 0, 0: 0, None: 0}
    i = 0
    while i + 8 <= len(data):
        n = struct.unpack_from("<H", data, i + 4)[0]
        gps.write(data[i:i + 8 + n])
        gps.flush()
        counts[wait_ack(gps)] += 1
        i += 8 + n
    valset(0x10740002, 1)                 # NMEA back on
    print(counts[1], "accepted,", counts[0], "refused,", counts[None], "not answered")

Which file

Out of the box the SAM-M10Q tracks GPS, Galileo, GLONASS and BeiDou, with QZSS and SBAS, so gnss_all matches it. If you have turned systems off, pick the bundle that matches what is left; frames for a system it isn’t tracking are wasted.

The time comes first

Until it knows the time to within a few seconds, the receiver refuses every orbit. NTP, a phone or a real-time clock is plenty. A rough position as well (UBX-MGA-INI-POS_LLH) saves a little more, but only send one whose stated accuracy truly covers where you are: a position that is wrong for the accuracy it claims stops the receiver finding satellites, which is worse than sending none.

Reading the replies

Each UBX-MGA-ACK-DATA0 carries 1 for accepted or 0 for refused, and a reason: 1 no time yet, 2 version not supported, 3 wrong length, 4 couldn’t store it, 5 not ready, 6 unknown type. Accepted means stored, not that the orbit was checked.

Download and speed

The raw file is about 10 KB, small enough for an ESP32’s RAM. For less time on the radio, fetch the .lzma (about two thirds the size) and unpack it with an LZMA-alone decoder; with a small HTTP buffer, fetch it in its 1000-byte chunks. At 9600 baud a full load takes about 15 seconds; a faster port rate (CFG-UART1-BAUDRATE) cuts that.

u-blox and SAM-M10Q are trademarks of u-blox AG. AltimeterCloud is not connected with u-blox; the examples are offered as they are, for you to adapt.