Decoding GNSS navigation messages on Android

If you’ve worked with raw data from the Android’s Global Navigation Satellite System (GNSS) APIs, you’re probably aware that apps like GPSTest and GnssLogger are capable of logging three types of information:

  1. NMEA data — Contains information about the locations calculated by the GNSS hardware in the industry-standard NMEA 0183 format
  2. Raw GNSS measurements —Contains raw pseudorange and carrier phase information from satellites that you can use to compute your own location
  3. Navigation messages — Contains the information about the GNSS constellations (e.g., almanac, ephemeris, clock offsets) that are needed by the receiver acquire GNSS signals. See EU GSA’s documentation and GPS.gov’s documentation for more details.

So which devices support each of these Android APIs?

In the past that question has been hard to answer because manufacturers typically don’t publish this level of detail about GNSS capabilities. Fortunately, though, users of GPSTest have been contributing crowd-sourced GNSS feature information to the GPSTest Database so we can easily see which devices support logging each of the above information types. If you’d like to contribute information about your device, see this article:

The good news 👍 — nearly all devices support logging NMEA data. This is important for Android devices to be able to show users information like PDOP, VDOP, and HDOP, as well as height above the geoid (i.e., altitude above mean sea level), because this information isn’t yet supported directly by the Android Location API. Also, most devices with Android 7 and higher support logging raw measurement pseudorange information (although far fewer support carrier phase via “accumulated delta range”).

The bad news 👎— very few devices support logging navigation messages. As a result, it can be hard to interpret this information at a glance based on the Android documentation alone.

I spent some time looking at navigation messages on Android, and I wanted to share what I’ve found.

Looking at the Android documentation

The official Android documentation for navigation messages data is GnssNavigationMessage.

I was particularly interested in logging the type of navigation messages — in other words, the GNSS constellations (GPS, Galileo, etc.) and signals (L1, L5, etc.).

First, there is the GnssNavigationMessage.getType() method, which returns an integer value and is described as:

Gets the type of the navigation message contained in the object.

Upon further digging there are integer constants at the top of this class that seem to represent GNSS constellation and signal types: TYPE_BDS_CNAV1, TYPE_BDS_CNAV2, TYPE_BDS_D1, TYPE_BDS_D2, TYPE_GAL_F, TYPE_GAL_I, TYPE_GLO_L1CA, TYPE_GPS_CNAV2, TYPE_GPS_L1CA, TYPE_GPS_L2CNAV, TYPE_GPS_L5CNAV, TYPE_IRN_L5CA, TYPE_QZS_L1CA, TYPE_SBS, and TYPE_UNKNOWN.

My next question was whether these types could be combined in a bitmask, similar to the accumulated delta range states for carrier phase measurements.

To look at this further, we need to examine the binary values of each integer constant. Here’s a table of each constant integer value and it’s binary equivalent:

Taking a look at bits 8–15, you can see that there is a unique value for each GNSS constellation:

So if you wanted to get all messages for a given GNSS constellation from a list of GnssNavigationMessages, you could use a bitmask or bitwise shift operation on bits 8–15 to find these.

However, when looking at bits 0–15, as hinted by the names each integer does indeed seem to point to a unique constellation message and signal carrier combination. For example, a GnssNavigationMessage object with a type of TYPE_GPS_L1CA should contain navigation message data just for GPS L1 signals.

The next step was to get ahold of some raw data and confirm this theory.

Looking at the raw data

Thanks to a GPSTest user with access to a Xiaomi Mi 8, here’s a snippet of raw navigation messages data from a log file:

As suspected, each line represents a navigation message for a particular satellite in a particular constellation. For example, the first line shows svid 22 with type 769, which is information for L1 signals for satellite 22 in Russia’s GLONASS constellation.

Navigation messages for other constellations and signals are also shown, including GPS L1 CA (type 257), Beidou D1 message (type 1281), Beidou D2 message (type 1282), and Galileo I/NAV message (type 1537).

Decoding the data

Now that we know the constellation and signal type for each GnssNavigationMessage, we need to decode the raw bytes above to extract the important information. This is particularly tricky because each constellation has a slightly different format for the information. The different formats are described in the GnssNavigationMessage Android documentation.

For example, for GnssNavigationMessage.getData():

For GPS L1 C/A, IRNSS L5 C/A, Beidou D1 & Beidou D2, each subframe contains 10 30-bit words. Each word (30 bits) should be fit into the last 30 bits in a 4-byte word (skip B31 and B32), with MSB first, for a total of 40 bytes, covering a time period of 6, 6, and 0.6 seconds, respectively.

For Glonass L1 C/A, each string contains 85 data bits, including the checksum. These bits should be fit into 11 bytes, with MSB first (skip B86-B88), covering a time period of 2 seconds.…

For Galileo I/NAV, each page contains 2 page parts, even and odd, with a total of 2x114 = 228 bits, (sync & tail excluded) that should be fit into 29 bytes, with MSB first (skip B229-B232).

Here’s a visual representation of the GPS L1 C/A navigation message:

The structure of the GPS L1 C/A navigation message (Source: Navipedia)

If you want to decode this information in software, this obviously requires some work for each signal and constellation.

Fortunately, we have a good start with Google’s GnssLogger application, which provides an example for decoding the GPS L1 CA navigation message. You can see the initial step to process each GnssNavigationMessage here:

Note the shift of bits (>> 8) so that the constellation prefix remains in the messageType variable. In the case of GPS, this value is now the 001 prefix we saw earlier in the table. As I mentioned earlier, this allows the app to identify all GPS navigation messages with the check for messageType == 1.

These messages are then passed to GpsNavigationMessageStore.onNavMessageReported() this class is where the decoding of the raw bytes to the navigation message happens. Here’s what that method looks like:

You can see how bytes for each subframe are handled separately in subsequent methods — I won’t go into the details for each of those, but you can check out the source code if you’re interested.

Wait…do we even need navigation messages from the Android API?

You may have seen the term “assisted GNSS” — this means that the GNSS device is receiving information from sources other than the GNSS satellite radio signals.

Assistance information normally includes all the data in the navigation message so that the device can more quickly acquire a first fix — it takes longer to decode the navigation message from over-the-air GNSS signals than it does to download it over a cellular or Wi-Fi connection.

Additionally, predictive ephemeris information is available from servers which lasts longer than the typical “real-time” ephemeris info in navigation messages — you may have seen this listed as “Predicted Satellite Data Service” (PSDS), or the vendor-specific term of gpsOneXTRA, in some apps.

So if you want to compute your own position from raw measurements offline, you really don’t need to decode navigation messages from the Android API in real-time — there are other sources you can get the information from, given the same data is broadcast all over the world.

If you want real-time ephemeris information over the network in real-time and the device doesn’t support the navigation messages API, Google has an implementation of a Secure User Plane Location (SUPL) Client (supl-client) here on GitHub. This code shows how to access SUPL messages from Google’s supl.google.com server. The project description says:

The SuplTester class provides an example on how to use the SUPL Client Project. The SuplTester sets up the SUPL TCP connection specifications, then at a given latitude and longitude sends an LPP SUPL request and prints the SUPL server response.

Closing thoughts

I hope this demystifies navigation messages on Android. Want to log some raw navigation message data yourself? See the GPSTest logging documentation for more details.

Do you know of other open-source examples for decoding navigation messages for other constellations? Please let me know in the comments below.

Finally, a special thanks to András Lukoviczki who provided raw navigation message logs from the Android API.

Was this article helpful? Consider following me on Medium. If you’re a user of the open-source GPSTest app and you’d like to support it, you can check out the GPSTest “Buy me a coffee” page:

Improving the world, one byte at a time. @sjbarbeau, https://github.com/barbeau, https://www.linkedin.com/in/seanbarbeau/. I work @CUTRUSF. Posts are my own.