Raspberry Pi based PPK and RTK solutions with RTKLIB

It’s been over six years now since I published my last post on how to run RTKLIB on a Raspberry PI, so it’s more than time for an update. In my previous post, I described using a Pi Zero as a data logger for a u-blox M8N for PPK solutions. In this post I will work with a Pi Model 4 and a u-blox M8T to demonstrate both logging for PPK solutions and a real-time RTK solution. The good news is that this time no soldering is required since we are going to use the USB port on the Pi to connect the receiver. These instructions will work with any u-blox receiver that supports raw observations and any model Pi that has USB ports for peripherals. With minor modifications, they can be used with any receiver that has a USB or UART port and supports raw observations.

Here’s an image of the assembled setup. The Pi in the center, and the u-blox M8T receiver is on top. We will use a wireless connection to talk to the Pi from an external computer so there is no need for a keyboard or display.

Raspberry Pi with u-blox M8T receiver

Step 1: Configure the Pi

The first step is to configure the Pi in “headless” mode so that we can talk to it from an external computer. This is quite straightforward and well-explained in this post, so I will not describe how to do it here. Only steps 1 and 2 in the post are required for this exercise. If you plan to use this for RTK solutions, be aware that the Pi will rely on the wireless connection to the internet for base station observations. This means that if you don’t want to be limited to using it within range of your home wireless router, then you will probably want to connect to a hot spot from a cell phone. If you are just interested in collecting data for PPK solutions, then it doesn’t matter.

After you have completed steps 1 and 2 above, you should have a Putty window open and have logged into your Pi. The next step is to build and install the RTKLIB code. The commands below will clone the RTKLIB code from the Github repository, compile the stream server app (str2str) and the RTK solution app (rtkrcv), and copy the executables to a location where they can be accessed from any directory.

> sudo apt update
> sudo apt install git
> mkdir rtklib
> cd rtklib
> git clone https://github.com/rtklibexplorer/RTKLIB.git
> cd RTKLIB/app/consapp/str2str/gcc
> make
> sudo cp str2str /usr/local/bin/str2str
> cd ../../rtkrcv/gcc
> make
> sudo cp rtkrcv /usr/local/bin/rtkrcv
> cd ../../../../..

Step 2: Configure the receiver

Before we connect the u-blox receiver to the Pi, we will need to configure it to output raw observation and navigation messages. The easiest way to do this is from your computer with the u-center app which can be downloaded from the u-blox website. Connect the receiver to your computer with a USB cable, start u-center, and connect to the receiver using the “Connection” option in the “Receiver” tab as shown in the image below.

u-center: Connect to receiver

Next, use the “Messages View” window from the “View” menu to enable the RAWX and SFRBX messages as seen below. While you are in the messages view, you can also disable any unnecessary NMEA messages to save communication bandwidth.

u-center: Enable raw observation and navigation messages

Next, we will switch to the “Configuration View” window to configure any other desired settings and then save them to flash . I would recommend verifying that all constellations are enabled with the “GNSS” command and that the sample rate is set to the desired value with the “RATE” command. I usually set this to 5 Hz. I would also recommend disabling both UART ports with the “PORT” command if you are not using them. If the baud rates are set too low, they will limit bandwidth on all ports including the USB port, even if nothing is connected to those ports. Finally, use the “CFG” command to save the settings to flash as shown below.

u-center: Save settings to flash

Step 3: Verify the data stream(s)

Next, we will confirm that we are receiving data from the rover receiver and if running a real-time solution, also from the base receiver. This step is not absolutely essential, but it does verify that we have the individual pieces working before we put it all together, and also gives some practice using the RTKLIB str2str command.

Disconnect the rover receiver from the computer and connect it to the Pi using a USB cable as shown in the image at the top of this post. Enter the following commands into the Putty console to create a new folder and run the stream server. This will connect to the USB port on the Pi. If you are using a UART port, you will need to use the appropriate port name.

> mkdir data
> cd data
> str2str -in serial://ttyACM0

The output of the receiver should now scroll across the Putty console screen. If you have any NMEA messages enabled, you should be able to see them mixed in with a bunch of random characters from the binary messages. Once you’ve confirmed the data stream, hit Control C to stop it.

If we want to log the receiver output for a PPK solution, we just need to add a file name to the previous command to redirect the data stream from the screen to a file. The command below will do this, using keywords in the file name to create a name that includes the current month, day, hour, and minute.

> str2str -in serial://ttyACM0 -out rover_%m%d_%h%M.ubx

The image below shows the expected output of both commands.

Verification of receiver data stream

If you are using the Pi just to log receiver data then you are done at this point unless you want to configure the Pi to make it automatically start collecting data whenever it is turned on. There are several ways to do this, all described in this post. Modifying the rc.local file is the simplest method.

For those who would prefer to run an RTK solution rather than just log data for a PPK solution, the next step is to confirm the base data stream. We will use the “str2str” command again, but this time we will specify the input to be an NTRIP stream using the format:


In my case, the command looks like this: (with the username and password removed)

> str2str -in ntrip://username:password@rtgpsout.unavco.org:2101/P041_RTCM3: -out temp.log

If everything is working properly, you should see non-zero transfer numbers and no errors, as in example above, in which case you can use Control C again to stop.

Note that if your NTRIP provider is using a VRS (Virtual Reference Station), then things are a little more complicated. We will need to send our local position inside of a GGA message. For this to work, you must have enabled the NEMA GGA message when configuring the receiver. To route these GGA messages back to the NTRIP server we will need to connect the stream server output to the receiver and enable the relay back feature with the “-b” option. Here’s an example I used to connect to test this with a VRS NTRIP server.

str2str -in ntrip://username:password@na.l1l2.skylark.swiftnav.com:2101/CRS -b 1 -out serial://ttyACM0

Step 4: Run the RTK solution

OK, now that we’ve confirmed that we are getting data from base and rover, it’s time to generate an RTK solution. We will use the “rtkrcv” console app in RTKLIB to do this, which we installed in Step 1.

We will need a configuration file for rtkrcv. You can use the “rtknavi_example.conf” file included with the demo5 release as a starting point but you will need to edit the stream configuration settings. Below are the settings I changed as well as a few important ones worth verifying are correct for your configuration. I have it configured to write the output to a file in LLH format. If you want the output in NMEA messages you can either change output stream 1 to “nmea” format or enable output stream 2 to get both a file and a stream of NMEA messages.

pos1-posmode =kinematic  # (0:single,1:dgps,2:kin,3:static)
pos1-frequency =l1  # (1:l1,2:l1+l2,3:l1+l2+l5)
pos1-navsys =13  # (1:gps+2:sbas+4:glo+8:gal+16:qzs+32:comp)
pos2-armode =fix-and-hold # (0:off,1:cont,2:inst,3:fix-and-hold)
pos2-gloarmode =fix-and-hold # (0:off,1:on,2:autocal,3:fix-and-hold)
out-solformat =llh #    (0:llh,1:xyz,2:enu,3:nmea)
ant2-postype =rtcm # (0:llh,1:xyz,2:sing,3:file,4:rinex,5:rtcm)
inpstr1-type =serial (0:off,1:ser,2:file,3:,...,7:ntrip)
inpstr2-type =ntripcli # (0:off,1:ser,2:file,3:,...,7:ntrip)
inpstr1-path =ttyACM0
inpstr2-path =usrname:pwd@rtgpsout.unavco.org:2101/P041_RTCM3
inpstr1-format =ubx # (0:rtcm2,1:rtcm3, ...)
inpstr2-format =rtcm3 # (0:rtcm2,1:rtcm3,...)
inpstr2-nmeareq =single # (0:off,1:latlon,2:single)
outstr1-type =file # (0:off,1:serial,2:file, ...)
outstr2-type =off # (0:off,1:serial,2:file, ...)
outstr1-path =rtkrcv_%m%d_%h%M.pos
outstr2-path =
outstr1-format =llh # (0:llh,1:xyz,2:enu,3:nmea)
outstr2-format =nmea # (0:llh,1:xyz,2:enu,3:nmea)
logstr1-type =file # (0:off,1:serial,2:file, ...)
logstr2-type =file # (0:off,1:serial,2:file, ...)
logstr1-path =rover_%m%d_%h%M.ubx
logstr2-path =base_%m%d_%h%M.rtcm3

I like to use WinSCP for editing and transferring files between the Pi and external computer but there are many other ways to do this. When you are done, the edited configuration file needs to be in the current folder you will run rtkrcv from. For my example, I renamed it “rtkrcv_pi.conf”

To run rtkrcv with a configuration file named “rtkrcv_pi.conf”, use the following commands:

> rtkrcv -s -o rtkrcv_pi.conf
  >> status  1

If all is well, you should see a status screen updated every second that looks something like this:

I changed the Putty display defaults to make this a little easier to read. I’ve also highlighted in yellow some of the numbers to check to make sure they look OK. Make sure you are seeing base RTCM location messages (usually 1005). If you want to check the input streams in more detail, you can use control c to exit the status menu, then enter “?” to see some of the other rtkrcv commands. To exit rtkrcv, use the “shutdown” command.

If all of your inputs look good, your solution is not working, and it is not obvious why, you can rerun rtkrcv with a “-t 3” in the command line. This will enable trace mode which will create a trace debug file which may offer clues as to what is wrong.

This should be enough to get you started. To explore more configuration options, see the str2str and rtkrcv sections in Appendix A of the RTKLIB users manual.

Google Smartphone Decimeter Challenge 2022

In a previous post, I described my experience using RTKLIB to analyze smartphone GNSS data from last year’s Google Smartphone Decimeter Challenge. In that case, I did not get involved until after the competition was complete. After making a few modifications to RTKLIB to handle the relatively low quality smartphone data, I was able to generate a set of solutions that would have placed 5th out of 811 teams in the final standings. I shared the code to duplicate my results in this code release on Github. It includes a custom version of RTKLIB with changes specifically made for the smartphone data, as well as a set of python scripts to automatically run solutions on all of the 2021 Google test rides.

Google is is hosting a second competition this year. It started in the beginning of May and will finish at the end of July. This year I decided to join the fun and submit some results while the competition was still ongoing.

Since last year, I had already incorporated all of the changes that were previously in only the GSDC version of RTKLIB, into the main branch of the demo5 fork of RTKLIB. These are in the latest b34f release, so the special release is no longer required.

Google changed the format of some of the files for this year’s competition and so I did have to rewrite the python scripts. One of the more significant changes they made was to include only one set of phone data for each ride in the test data set. Last year it was possible to combine results from multiple phones on a single ride to improve the results but that is not an option this time.

In order to encourage participation in this year’s competition, I have shared the code and instructions to duplicate my initial attempt on this year’s data in a Kaggle notebook . If followed correctly it will generate a score of 3.135 meters when submitted to Kaggle, the competition host. At the time I first published it, it was good enough for first place. However, the competition has picked up since then, and some teams have taken advantage of this code. It will no longer get you into first place, but it will still put you into a tie for 21st place out of 234 teams. This means that anyone interested in jumping in now can still start near the front of the pack.

Since sharing the notebook, I have made a few local tweaks to the code, config files, and python scripts which improve my score to 2.152 meters. This is currently good enough for first place, but given that there are nearly two months left in the competition, I don’t think this will be good enough to win without further improvements.

To keep things interesting, I don’t plan to share my most recent changes until the competition is complete but anyone who follows some of the suggested hints at the end of my Kaggle notebook should be able to get a good part of the way there. To get all the way there will require a little more ingenuity but I also believe there is still plenty of room for further improvement on my results.

However, I suspect that winning the competition using RTKLIB will require more than just configuration changes and python script changes. I believe it will also require making changes to the RTKLIB code itself.

As anyone who has worked with the RTKLIB code is probably aware, it can be quite a challenging environment to work in. To make things easier and to encourage innovation to the code and algorithms I have recently ported a subset of RTKLIB sufficient to generate PPK solutions into Python which I described in this post. The actual code is available on Github here. I have also generated a second Kaggle notebook with instructions on duplicating the C/C++ version results on the Google data with the Python code. I have not actually submitted the results of this code to Kaggle, but based on results from this year’s training data set, and last year’s test data set, I believe this code should give slightly better results than the C/C++ code.

The python code is primarily intended for those planning to develop or modify algorithms internal to the PPK solutions and not just running the code as-is or with just configuration changes. For those users, the C code will run much faster. However, the python version provides a friendlier development platform. When development is complete, the modified python code can either be run on the complete data set on a faster PC with a little patience, or the completed changes can be fairly easily ported back into the C code since two code sets are very closely aligned. This alignment includes file names, function names, variable names, and comments. The code does not align on a line by line basis because of extensive use of Numpy in the python code, but structurally it is very similar.

Based on the discussion threads on the Kaggle forum for this competition, it appears that most competitors are more familiar with machine learning and post-solution filtering techniques than they are with GNSS theory. I suspect anyone who already has a reasonably solid background in GNSS can do quite well in the competition without an enormous amount of effort. Using some of the tools I describe here should help to get there even more quickly.

My hope is that providing these tools will encourage at least a few more people from the GNSS community to participate and help them to do well. For any of you who decide to take the challenge, I wish you good luck and hope to see you near the top of the leaderboard!

A Python version of RTKLIB

In this post I would like to introduce a new project that I have recently been working on.

RTKLIB can be a great tool for exploring the world of precision GNSS solutions. There are many configuration options (some say too many!) which allow the user to investigate the effects of different algorithm settings. It is also open source, so the user can see exactly what the code is doing behind the scene and even modify the code to explore options outside the available configuration set.

However, the code is quite complex and written in a dense style that can be quite intimidating to the casual explorer who wants to dig deeper. The compiled nature of C/C++ also makes the development platform more difficult to work in than a more interactive environment would be. These barriers mean that, despite its open source, only a small number of RTKLIB users actually ever dig into the core code. For those who want to take things a step further and experiment with or develop their own GNSS algorithms, RTKLIB may not be an optimal choice.

I have often thought that a Python version of RTKLIB would help minimize some of these barriers and make RTKLIB more useful as a learning and development tool. Recently I was made aware of a newly developed Python version of the code through a reference in the diary of Tomoji Takasu, the creator of RTKLIB. The code is called CSSRLib, and was written by Rui Hirokawa from Mitsubishi Electric. It is primarily intended to demonstrate the use of SSR correction data but includes a port into python of the pieces of RTKLIB needed to run PPK and PPP solutions. It also includes example data and wrapper scripts to run these solutions on the sample data. It was obviously a big effort and a significant accomplishment.

Although the CSSRLib package could be quite useful as-is for exploring precision GNSS solutions, the translation from C to Python is not strict enough to allow a user to jump directly back and forth between this code and RTKLIB.

I thought it would be an interesting exercise to rewrite the python code to be more closely aligned to RTKLIB, add all of the changes and enhancements in the demo5 version, and try to match the solutions of the demo5 solutions as closely as possible. This turned out to be a significantly more time-consuming proposition than I realized, but after several months of on-and-off work on the project, I finally was able to complete a code that, for the most part, meets these criteria.

It is not intended to be a substitute for the C version of RTKLIB since it only performs a small subset of the full library capability and runs noticeably slower. It currently runs only PPK solutions, although I would like to add PPP solutions later. The CSSRLib package supported the GPS and Galileo constellations. I have added support for GLONASS, but Beidou is still not supported. Since the resulting code is quite different in implementation and purpose from the original CSSRLib, I have created a new repository on Github for it, and named it rtklib-py. The code still shares many of its CSSRLib roots and so I have left the original copyright notices in the code files and have added acknowledgements to the original code.

The CSSRLib package includes an example dataset from a geodetic quality rover but since the demo5 code focuses on low-cost receivers, I have replaced the sample data with two other datasets, one from a u-blox F9P rover mounted on a vehicle roof, and a second dataset from the Google Smartphone Decimeter Challenge containing data from a smartphone mounted inside a vehicle.

A truly literal translation of the C code would run very slowly, primarily because Python is an interpreted language and C is a compiled language. To make the python code run at a reasonable speed, many of the for loops in RTKLIB have been replaced with Numpy array operations. This need to optimize, along with inherent differences between C and Python, means the codes are not identical but I have attempted to make the two codes as similar as possible in file names, function names, variable names, logic, and even comments. I also added a similar trace debug feature to the code that, when enabled, produces trace files that are very similar to the RTKLIB trace files. These will align quite closely when compared against each other with a file compare app and provides a way to confirm that the intermediate results match between the two code sets.

Here is an example of the trace file from RTKLIB on the left and rtklib-py on the right which demonstrates how similar they are. There are still small differences in the codes, which along with the iterative nature of the solution does cause the final positions to diverge by small amounts, but the intermediate results are for the most part very close.

The inputs (rinex files) and outputs are the same between the two codes, so RTKCONV and RTKPLOT in RTKLIB can be used to generate the rinex input files and plot the solution output files.

The config parameters in the new code have names and functionality very similar to the RTKLIB code although not all options are supported, particularly the ones used more frequently in PPP solutions than in PPK solutions. For the two sample data sets, the config parameters are defined in f9p_config.py and phone_config.py respectively. The top level script to run a solution is run_ppk.py. By default, it is setup to run the F9P sample data set but includes a commented out section to run a solution for the smartphone data. You can run a solution on your own data by modifying the files specified in run_ppk.py. You may also want to create a copy of one of the existing config files and adjust it for your data.

Below is a comparison of an RTKLIB forward solution to the F9P example data set on the left and an rtklib-py solution on the right, using the same configuration parameters for both.

Here is the difference between the two solutions. As you can see, they are quite close but not exactly the same.

Rtklib-py is not meant to be a replacement for RTKLIB and would not be a good choice for someone who is interested only in the final results. However, I am hoping it can be useful in several other possible ways:

  1. As a “map” to explore and learn how the inner details of RTKLIB work
  2. As a development environment to experiment with enhancements or adjustments to the RTKLIB algorithms. Due to the close alignment between the two packages, these can then be fairly easily ported back into the C/C++ version of RTKLIB when they are complete
  3. To debug issues found in RTKLIB within a more interactive environment
  4. To cut and paste pieces of the code into more custom solutions

One last goal is for this code is for it to be available as a tool for teams competing in this year’s Google Smartphone Decimeter Challenge competition.

The purpose of this post was just to introduce the new code so I won’t go into any more detail here. There are some brief instructions in the readme file in the Github repository for running the code but I do assume users are already reasonably comfortable with running Python applications.

I personally like to run Python in the Spyder IDE which provides an easy-to-work-in interactive environment and includes Numpy and other popular libraries in the installation. I’m sure, however, that there are other good development environments as well, if you prefer another option.

I’m always interested in other people’s thoughts on these topics so please leave any comments or suggestions you have in the comment section below.

Google Smartphone Decimeter Challenge

Last year, Google hosted a Kaggle competition to see who could generate the most accurate solutions for a large number of raw observation data sets collected with Android phones on vehicles driven around the Bay Area. The phones were located inside the vehicle on the front dash and they did not use ground planes under the phones, making the data far more challenging than the previous static cell phone data sets I have explored.

I took a quick look at the data when they first posted it, but felt that the quality was outside the scope of what RTKLIB could reasonably handle, given the low quality of the collection environment, and so I did not pursue it. Now that I have gained some experience working with a few other cell phone data sets, I thought it might be time to take another look at this data. In the interest of full disclosure, this second look was also motivated by a very generous contribution by Google to support and maintain the demo5 RTKLIB code.

This turned out to be a very useful exercise. Most of my efforts with RTKLIB have been focused on improving the ambiguity resolution but in this case the errors were too large for ambiguity resolution to be reliable so I had to focus entirely on improving the float solution. The large and frequent errors severely stressed the RTKLIB solution code and exposed several weaknesses that do not normally show up with the cleaner raw data used for more typical precision solutions. So I am thankful to Google not just for their contribution but also for encouraging me to perform this exercise.

The input data for the competition is available here. The raw data files are in the Android GNSSLogger raw format and include IMU and other sensor data from the phones in addition to the GNSS data. As a starting point, Google has also included standard RINEX files converted from the GNSSLogger files as well as what is a very good set of standard precision GNSS solutions considering the difficulty of the data. The goal of the competition is to use any combination of the provided input data to generate a set of solutions with the minimum error when compared to the ground truths. The data is divided into two sets, the training data for which the ground truths are included and the test data for which the ground truths are not included. The intent is for competitors to develop and evaluate their algorithms using the training data, then submitting their solutions for the test data. Submissions are scored using the mean of the 50th and 95th percentile distance errors for the test data set.

For my exercise I will focus only on providing a set of PPK baseline solutions similar to the baseline solutions that Google provided and for the most part leave the post-processing filtering and other opportunities for others to explore. The one exception I will make is that I did find a few of the data sets were too poor to resolve with RTKLIB so I will merge the solutions from all phones for each ride into a single combined solution, ignoring the small distance between phones. More details on this later.

The baseline solutions have the advantage of being able to use satellites from all the constellations in the raw data (GPS, GLONASS, Galileo, Bediou, and QZSS) while the PPK solutions can only use satellites with matching observations in the local base data (GPS, GLONASS, and Galileo in this case). However, the PPK solutions have the advantage that by differencing the observations between the smartphones and a nearby base station, they can cancel out most of the atmospheric, orbital, and clock errors. In addition, the baseline solutions use only the pseudorange measurements while the PPK solutions can take advantage of the carrier phase measurements as well. In general, the advantages from differencing with the base data and using the carrier phases should outweigh the disadvantage of having fewer satellites, and I would expect the PPK solutions should be more accurate than the baseline solutions, but we’ll see.

So let’s get started.

Since I am going to generate PPK solutions, the first thing I need is some nearby base observations. For these, I downloaded raw observation data for the appropriate dates and times for a nearby CORS station from the NOAA National Geodetic Survey website. There were several different CORS stations I could have used but I chose the SLAC station because it was reasonably close to all of the data collection rides and contained Galileo observations in addition to GPS and GLONASS.

I also needed satellite navigation data for each data set so I downloaded the BRDM files from the International GNSS Service website. This is the easiest place I have found to get navigation files that contain data for all of the GNSS constellations. The CORS data I downloaded above included navigation data for GPS and GLONASS, but not for Galileo.

Next, let’s look at the raw observation data from the smartphones. One challenge when working with any raw receiver data with RTKLIB is that there is always more information in the raw data than can be translated directly into either the RINEX format or the RTKLIB internal variables. This means that to take advantage of this additional data, the conversion process needs to be more than just a simple translation, it also needs to include some interpretation and possible consolidation of the data. For this reason, I chose not to start with the provided RINEX files which can be read directly by RTKLIB, but instead to translate the raw GNSSlogger files to RINEX using a python script. This gives us the opportunity to take some advantage of the additional information in the raw file before it is discarded.

As a starting point, I used a python script from Robukun that I found on Github to do the GNSSLogger to RINEX conversion. I modified it to use the same set of rules that Google described for the pre-processing of the raw data for their baseline solutions. Interestingly, the RINEX files they provided did not appear to follow this set of rules. In addition to a few small tweaks to these rules, I made two more significant changes. First, I ignored all of the cycle slip or half cycle ambiguity flags in the raw data. These appear to be too conservative and caused RTKLIB to throw out too much useful data.

I also embedded the reciever’s pseudorange and carrier phase uncertainty estimates into the older, mostly unused legacy SNR field using the same format that the demo5 code uses to do the same thing for the u-blox raw data. I ended up not using the uncertainty estimates in my solution but they are available for future exploration. In the latest demo5 code, observation weighting can be based on any arbitrary combination of elevation, SNR, and, when available, receiver uncertainty, but for this solution I used the standard elevation-only weighting.

I have included the modified python script along with all the other files required to duplicate my results in a “smartphone” release package of the demo5 code available here.

This python script will convert each GNSSLogger raw data file into a RINEX file that can then be processed with RTKPOST or RNX2RTKP. I started with RTKPOST to develop a solution configuration file on a couple of datasets, then used RNX2RTKP to batch process all of the data sets. I used a modified version of the batch-processing python script that I described in my last post to do both the GNSSLogger->Rinex conversions and to run the RTKLIB PPK solutions.

I started with the b34d version of the demo5 RTKLIB code and the config file I used for the previous cell phone data analysis but ended up needing to make some changes to both the code and the config file. Some of the changes are in the b34e RTKLIB code but others are only in the “smartphone” release of the code since I haven’t yet confirmed that they don’t cause any issues with more typical PPK/RTK solutions.

The most significant change to the RTKLIB code was to enable cycle slip detection using the doppler raw measurements. This feature has been in the code for many years but has been commented out because it is unable to distinguish between clock jumps and cycle slips. By rewriting the function to process all satellites in a single call instead of just a single satellite I was able to remove the common-mode effect of the clock jumps. This feature compares the doppler measurement to the change in carrier phase measurement since the doppler shift of a given signal is the time derivative of the carrier phase. It flags a cycle slip if the difference between these two values exceeds a user-configurable threshold.

Because I removed the cycle slip flags from the RINEX files, it was very important to have a reasonably reliable alternative method to detect the cycle slips. The existing geometry-free detection method works quite well for satellites with dual-frequency measurements but many of the satellites in these data sets have only single frequency measurements and so this test can not be applied. By only responding to verified cycle slips instead of every flagged potential slip, the code is much better able to preserve the phase bias estimates of each satellite.

In addition to this change to the RTKLIB code, I made a few other minor changes which allowed the code to degrade a little more gracefully in the presence of very low quality raw observations.

I started with the configuration file I used in my recent post for the Xiamoi Mi8 static cell phone data sets but made the following changes:

Positioning mode: static -> kinematic
GNSS constellations: disable Beidou
Integer Ambiguity Res: on -> off
Slip Thresh: Geom-Free: 0.05 -> 0.10
Slip Thresh: Doppler: N/A -> 5.0
Time Format: hms -> tow
Phase Error Ratio L1/L5: 1500/300 -> 300/100
Phase Error a+b: 0.006/0.006 -> 0.003/0.003
Carrier Phase Bias: 0.001 -> 0.01

Switching the position mode from static to kinematic is self-explanatory. I disabled Beidou because the base observations did not include it. I disabled ambiguity resolution because the errors in this data are too large to reliably resolve the ambiguities and we would just end up with many false fixes. I also increased the threshold for geometry-free slips because of the larger errors and added a new parameter for the new doppler slip detection. I switched the time format in the solution file just because it was more compatible with the format used in the Google baseline files. I have less solid justification for the last three changes and am also less certain that these are optimal but was trying to increase the weighting of the pseudorange measurements while also accounting for the lower confidence in the carrier phase biases remaining constant due to undetected slips. There was also a bit of trial and error on a couple of the data sets with these parameters before applying them to the full data set. If you make a close comparison between these config files you will notice that a few other parameters also changed but these are all related to the ambiguity resolution which is turned off so they have no effect on the results. The config file with all of these changes is included in the release package.

With these changes I was able to generate PPK solutions for all of the data in the training set. Comparing these to the provided ground truths showed that the majority of the solutions were reasonably good relative to the provided baseline solutions, but a few were very, very poor.

Plotting the raw observations for the poor quality solutions showed that the raw observations for these datasets were distinctly worse than the others. The plot below shows excerpts of the raw observations for two phones from the same ride. I used the RINEX files provided by Google for these plots since they include the receiver flagged cycle slips (red ticks). The two phones were placed next to each other on the front dash but provided very different results. The bad data did not follow any particular model of phone, and every data set had at least one good data set, so I am not sure what was the cause of these bad data sets. The baseline solutions provided by Google did not seem to be nearly as significantly affected by this, presumably because they relied primarily on the pseudorange measurements and not the carrier phase.

Pixel4 and Pixel4Modded raw observations for 1/4/21 RWC-1 drive

Since every ride had at least one usable set of raw observations I decided to replace the individual phone solutions for each ride with a single solution created with a weighted average of the individual solutions for that ride. This does introduce some error because I ignored the small distance between phones (typically ~ 0.2 meters) but these errors are still relatively small compared to the total errors. Each solution point was weighted by the inverse of the RTKLIB estimated variance for that point.

Using the rules of the competition to calculate phone averaged errors relative to the ground truths, the resulting errors for the training set were 1.37 meters for the 5oth percentile, 5.35 meters for the 95th percentile and 3.36 meters for the average of the two.

This compares to 50th percentile =2.43 meters, 95th percentile =9.62 meters and average =6.02 meters for the Google baseline solutions. This comparison isn’t quite fair since the baseline solutions did not include the phone merge. However, the baseline solutions don’t include accuracy estimates for each point, so there is no easy way to merge these solutions.

I then ran the same process to generate merged PPK solutions for the test data set provided by Google. There are no ground truths included for this data so I can not directly calculate the errors. However I did submit the results to Kaggle to get a combined 50th/95th percentile score for both the Google baseline solutions and the RTKLIB PPK solutions. The contest was over five months ago now, so new entries are not recorded, but we can compare how our solutions would have done relative to the final results.

The Google baseline solutions returned a score of 5.42 meters which would have put us in a 35 way tie for 692th place out of 810 competitors. Apparently many competitors did not get past simply submitting the given input as their output.

The RTKLIB PPK phone-merged solutions returned a score of 2.15 meters which would have given us 5th place. Not too bad for a baseline with very little post-processing! Presumably this could be improved a fair bit with some of the many other techniques competitors used to improve the Google baselines.

I’ve given the “private leaderboard” scores from Kaggle here since that is what was used to determine the winners of the contest. The “public leaderboard” score is determined from a different slice of the test data set and was not ranked as high, probably because it included more urban data which benefits more from the post-processing techniques.

In most cases I would be very disappointed with PPK solution errors measured in meters, not centimeters, but in this case, given the extremely challenging data, I was just happy that RTKLIB was able to converge to any kind of reasonable answer.

Kaggle results for RTKLIB PPK baseline solutions

I’ve included the merged and unmerged baseline PPK solution files as “baseline_locations_merged_test_1230.csv” and “baseline_locations_test_1230.csv” in the release package. These files are in the same format as the Google provided baseline file “baseline_locations_test.csv”, so for anyone who competed in the competition it should be straightforward to substitue this baseline file in place of the Google baseline file. If you do run this exercise, I would be interested in hearing your results, so please leave a comment.

For those of you who would like to duplicate my results, this is a brief summary of the steps required to do this. All of the necessary files are included in the above-mentioned release package. Note that you should review and adjust the input parameters at the top of each python file to make sure it matches your file structure. The parameters are set up to calculate the solutions for the test data but can be modified to specify the training data. The python scripts are located in the python folder and the base station observation files, navigation files, configuration file, and solution files are in the Google folder

  1. If not already done, download and unzip the Google datasets
  2. Download the demo5 RTKLIB smartphone release package.
  3. Copy the base observation, navigation and configuration files from the RTKLIB package into the raw data file folders.
  4. Run “batch_rnx2rtkp_google.py” to generate the RINEX files and solution files
  5. Run “createBaselineCsvFromPos.py” to create the unmerged baseline solutions file.
  6. Run “mergePhones.py” to merge the individual phone solutions into a baseline file with combined solutions.
  7. Run “createSubmission.py” to generate a file for submission to Kaggle

I have heard that Google will be running the competition again this year, so for those of you who missed it last year, you will have another chance to compete. I hope to be more involved this time. Although I don’t think I will compete myself, I would like to use this post as a starting point to put together some tools and information to make it easier for others to use RTKLIB in the competition.

Batch processing RTKLIB solutions with RNX2RTKP and Python

The RTKPOST GUI app in RTKLIB is a great tool for interactively exploring post-processed PPK or PPP solutions. However, once you have settled on a solution configuration and simply want to run that solution on many data sets, RTKPOST is not the right tool. The RNX2RTKP command line app is a better choice but will still only run a single solution at a time. If your goal is to run the same solution configuration on many data sets, then you will need to add some kind of wrapper to call the RNX2RTKP app multiple times.

I usually use a python script to do this. Below I have included a simple script that I used to process the large number of cellphone data sets in my previous post. It is configured to run in Windows but with possible minor modifications, it should run in linux as well. It is not meant to be used as is, but is a template you can use to write your own script to match the structure of your data.

 batch_rnx2rtkp - template to run multiple simultaneous solutions of rnx2rtkp in Windows.
    This example is configured to run the cellphone data sets available at 

import os
import subprocess
import psutil
import time

# set location of data and rnx2rtkp executable
datapath = r'C:\gps\data\cellphone\Mi8_Julien\0521_dataset'
binpath = r'C:\gps\rtklib\bins\demo5_b34d'

# Choose datasets to process
DATA_SET = 0  # 0=open-sky, 1=partial forest, 2=forest
USE_GROUND_PLANE = True  # True or False

# set input files
cfgs = ['ppk_ar_1027_snr24']  # list of config files to run (files should have .conf ext)
rovFile = 'GEOP*.19o'  # rover files with wild cards
baseFile = 'BBYS*.19o' # base files with wild cards
navFile = r'"..\brdm*.19p"' # navigation files with wild cards
outFile = 'ppk_ar_1027'

# set maximum number of simultaneous occurences of rnx2rtkp to run
max_windows = 10 

# get list of current threads running in Windows
current_process = psutil.Process()
num_start = len(current_process.children())

# get list of date folders in data path
dates = os.listdir(datapath)

# loop through date folders
for date in dates:
    datepath = datapath + '/' + date
    # Filter which folders to process
    if not os.path.isdir(datepath):  # skip if not folder
        if date[-2:] != 'gp':  #skip folders without ground plane tag in name
    else: # no ground plane
        if date[-2:] == 'gp':  #skip folders with ground plane tag in name

    # Get list of datasets in this date folder
    datasets = os.listdir(datapath + '/' + date)
    # Select desired folder in data set 
    dataset = datepath + '/' + datasets[DATA_SET] # 0=open-sky, 1=partial forest, 2=forest 
    # Run a solution for each config file in list       
    for cfg in cfgs:
        # create command to run solution
        rtkcmd=r'%s\rnx2rtkp -x 0 -y 2 -k ..\..\%s.conf -o %s.pos %s %s %s' % \
            (binpath, cfg, outFile + '_' + cfg, rovFile, baseFile, navFile)    
        # run command

    # if max windows open, wait for one to close
    while len(current_process.children())-num_start >= max_windows:
        time.sleep(1) #wait here for existing window to close

# Wait for all solutions to finish
print('Waiting for solutions to complete ...')  
while len(current_process.children())-num_start > 0:
    pass #wait here if max windows open        

RTKLIB is a single threaded app so will not take advantage of multiple processors on a single computer. To get around this limitation and maximize the use of all processors, the script will launch separate processes for each RTKLIB solution until a maximum number of simulatenous processes has been reached and then will wait for a prior process to complete before launching a new solution. For my typical laptop, I find that setting the maximum number of simultaneous windows to 10 works fairly well but this number can be adjusted up or down based on the processing power of your computer.

In this case I have run only one solution for each dataset but the script is set up to run as many different solutions as desired. Just add an additional config file name to the list of config files for each desired solution. Be aware that the list of config files leaves off the file extensions. The actual config files should all have a “.conf” extension.

Note that the list of input files uses wildcards in the file names in many cases, since the file names for each data set will likely vary slightly from data set to data set.

In my example, the datasets contain three different types of environment (open sky, partial forest, and forest) and two different antenna configurations (ground plane or no ground plane). I have set the script up to only run the data for one type of environment and one antenna type as specified in the input parameters at the top of the script. However, this can be modified to filter the datasets in any way desired or to just run all of them.

There are probably faster and more elegant ways to do this, but if you are just looking for something simple to allow you to quickly run a given solution or set of solutions on many data sets, then you may find this useful.

Cell phone RTK/PPK -How important is a ground plane?

I often stress in my posts the importance of using a ground plane under the antenna when collecting observations for precision GNSS solutions to reduce the effect of multipath. This is true for traditional ceramic patch antennas but is even more important when collecting data with a cell phone.

A ceramic patch antenna typically used with a low cost GNSS receiver has some built in protection against multipath. It is directional with maximum gain in the vertical direction and is also circularly polarized which helps because the reflected signals have the opposite polarization of the direct signals. In this case, adding a ground plane under the antenna reduces multipath by attenuating signals from low elevation angles and eliminating signals from below the horizon, but it is only part of the multipath mitigation.

A cell phone antenna on the other hand is omni-directional and most likely linearly polarized. Hence it has little or no built-in protection against the reflected signals. The only defense in this case against multipath is a ground plane, and so it becomes that much more critical.

I recently stumbled across a very nice cell phone data set shared online by the same authors of the data I used in my very first post on cell phone solutions from a couple years ago. It is taken with an Xiaomi Mi8 phone and includes 40 days of static measurements, each measurement between 10 and 15 minutes long. There are measurements each day from three locations with varying sky visibility; open sky, partially forested, and forested. What makes this data particularly interesting is that for half of the 40 days, the measurements were taken with a ground plane underneath the phone and the other half were taken without a ground plane. The authors then went on to show that the solutions derived from the ground plane data were significantly more accurate than those derived from the data without ground planes. Unfortunately their paper is not available without logging into a service, but a short summary is available here.

In this post I will try to repeat their analysis using their data to confirm I get the same result. They actually used an earlier version of the demo5 RTKLIB for their analysis but I will use the latest version of the code (b34d) and a more recent configuration file. For this exercise I will analyze only the open sky measurements and will leave the more challenging measurements for another post.

The data set is very complete and includes observations from a nearby base station as well as downloaded broadcast navigation data. This last item is important, because as I’ve noted before, the Mi8 Galileo navigation data either has a bug in it or is just incompatible with RTKLIB. For this reason, I ran all solutions with the downloaded broadcast navigation data rather than the navigation data collected by the phone.

I started with the config file I described in my last cell phone post but made a few changes. I have appended the contents of the final config file I ended up with to the end of this post. I’ll describe the changes I made below.

First of all, this data is static, unlike the previous data, so I changed the solution mode from “kinematic” to “static”.

Next, looking at the residuals from an initial solution plotted with RTKPLOT, I noticed a significant difference in the magnitude of the pseudorange residuals between L1 and L5. In the plots below, the left plot shows the L1 residuals and the right plot shows the L5 residuals. The carrier phase residuals are similar but the pseudorange residuals are much smaller for L5. This is not unexpected because the newer L5 signal has improved structure and higher power. The details are explained in this article.

Solution residuals for L1(left) and L5(right) observations

To take advantage of this difference, I adjusted the code/carrier phase error ratio for L1 (stats-eratio1) from 300 to 1500 and left the L5 error ratio (stats-eratio5) at 300. This will cause the kalman filter to weight the L5 pseudorange observations more heavily than the L1 observations. In RTKPOST, this parameter is set in the Statistics tab of the Options menu. Note that prior to the b34d code, the L5 error ratio parameter existed but was not configurable through the GUI interface in RTKPOST, but that is fixed now.

Next, based on the SNR of the observations, plotted below, I chose to reduce the minimum SNR threshold from 34 dbHz to 24 dbHz. This is a somewhat arbitrary setting and may not be optimal, but leaving it at 34 dbHz for this data set would have thrown out too many usable satellites. It could also have been adjusted separately for L1 and L5 but I did not choose to do this. It is interesting that the L5 observations do not appear to have higher SNR than L1 despite the increased signal power mentioned above. This may be due to limitations of the antenna.

SNR of observations, L1 (left) and L5(right)

The last change I chose to make was to change the solution type (Filter mode in the RTKPOST options menu) from “Combined to “Combined-no phase reset”. This is a relatively new option in the demo5 code. The official 2.4.3 code always resets the phase bias estimates between the forward and backward solutions and this is a more conventional approach. Earlier versions of the demo5 code reset the phase estimates if ambiguity resolution was set to fix-and-hold but did not reset them otherwise. The reason to reset them is to insure the forward and backward solutions independent, but especially for shorter data sets, it can be advantageous to use the converged estimates as a starting point for the backwards solution. Since, in this experiment, the measurements are fairly short (10-15 minutes), I chose to not reset the biases.

The next step was to run the solution on all 40 days of data. RTKPOST is nice for working with a single data set and experimenting with different parameter settings, but to run many solutions quickly, it is easier to batch process them with the RNX2RTKP command line version of RTKPOST. I used a simple python script to run RNX2RTKP ( the CLI version of RTKPOST) in multiple simultaneous threads to make this process faster since a single instance of an RTKLIB app will only use a single processor . I’ll discuss this script in more detail in my next post.

I then used python to plot the error in the solutions, 40 points in all, 2o with ground planes, 20 without. To calculate the errors, I subtracted the ground truth included with the data set from each solution position.

Static PPK solutions for open sky data, with and without ground plane

Note that the ground truth was generated from a survey grade receiver but is only approximate because the exact location of the antenna within the phone was unknown. Cell phone antennas can also have large offsets between the mechanical center of the antenna (ARP) and the electrical center of the antenna (APC). No attempt was made to compensate for these.

As expected, the ground plane solutions were significantly more accurate than the no-ground plane solutions. In fact, 19 out of the 20 measurements solutions for data using a ground plane were within a few centimeters of each other. Only 7 of the 20 measurements without ground planes met the same criteria. Overall, I would say this data makes quite a compelling argument for using a ground plane. I understand it’s not always possible in a cell phone application to do this, but it’s important to at least understand the significance of this choice.

The results were also consistent enough to suggest that cell phones, despite their low performance antennas, are becoming a viable option for practical measurements, at least in open sky environments. It’s worth noting as well, that, although the errors were much larger without the ground plane, even the errors in these measurements were consistently well below a meter.

It is important to remember that this data was all taken with an open sky view and that you should not expect such good results in more challenging conditions. It’s also still true that low cost receivers with higher quality antennas will still give much more robust and reliable solutions.

There’s plenty more data in this data set that I didn’t look at in this post but hope to take a closer look at in the future.

Config file used for this experiment:

# rtkpost options for b34d code, static Mi8 cell phone
pos1-posmode       =static     # (0:single,1:dgps,2:kinematic,3:static,4:static-start,5:movingbase,6:fixed,7:ppp-kine,8:ppp-static,9:ppp-fixed)
pos1-frequency     =l1+l2+l5   # (1:l1,2:l1+l2,3:l1+l2+l5,4:l1+l2+l5+l6)
pos1-soltype       =combined-nophasereset # (0:forward,1:backward,2:combined,3:combined-nophasereset)
pos1-elmask        =15         # (deg)
pos1-snrmask_r     =on         # (0:off,1:on)
pos1-snrmask_b     =on         # (0:off,1:on)
pos1-snrmask_L1    =24,24,24,24,24,24,24,24,24
pos1-snrmask_L2    =34,34,34,34,34,34,34,34,34
pos1-snrmask_L5    =24,24,24,24,24,24,24,24,24
pos1-dynamics      =on         # (0:off,1:on)
pos1-tidecorr      =off        # (0:off,1:on,2:otl)
pos1-ionoopt       =brdc       # (0:off,1:brdc,2:sbas,3:dual-freq,4:est-stec,5:ionex-tec,6:qzs-brdc)
pos1-tropopt       =saas       # (0:off,1:saas,2:sbas,3:est-ztd,4:est-ztdgrad)
pos1-sateph        =brdc       # (0:brdc,1:precise,2:brdc+sbas,3:brdc+ssrapc,4:brdc+ssrcom)
pos1-posopt1       =off        # (0:off,1:on)
pos1-posopt2       =off        # (0:off,1:on)
pos1-posopt3       =off        # (0:off,1:on,2:precise)
pos1-posopt4       =off        # (0:off,1:on)
pos1-posopt5       =off        # (0:off,1:on)
pos1-posopt6       =off        # (0:off,1:on)
pos1-exclsats      =           # (prn ...)
pos1-navsys        =45         # (1:gps+2:sbas+4:glo+8:gal+16:qzs+32:bds+64:navic)
pos2-armode        =fix-and-hold # (0:off,1:continuous,2:instantaneous,3:fix-and-hold)
pos2-gloarmode     =fix-and-hold # (0:off,1:on,2:autocal,3:fix-and-hold)
pos2-bdsarmode     =on         # (0:off,1:on)
pos2-arfilter      =on         # (0:off,1:on)
pos2-arthres       =3
pos2-arthresmin    =3
pos2-arthresmax    =3
pos2-arthres1      =0.1
pos2-arthres2      =0
pos2-arthres3      =1e-09
pos2-arthres4      =1e-05
pos2-varholdamb    =0.1        # (cyc^2)
pos2-gainholdamb   =0.01
pos2-arlockcnt     =5
pos2-minfixsats    =4
pos2-minholdsats   =5
pos2-mindropsats   =10
pos2-rcvstds       =off        # (0:off,1:on)
pos2-arelmask      =15         # (deg)
pos2-arminfix      =10
pos2-armaxiter     =1
pos2-elmaskhold    =15         # (deg)
pos2-aroutcnt      =20
pos2-maxage        =30         # (s)
pos2-syncsol       =off        # (0:off,1:on)
pos2-slipthres     =0.05       # (m)
pos2-rejionno      =1          # (m)
pos2-rejgdop       =30
pos2-niter         =1
pos2-baselen       =0          # (m)
pos2-basesig       =0          # (m)
out-solformat      =llh        # (0:llh,1:xyz,2:enu,3:nmea)
out-outhead        =on         # (0:off,1:on)
out-outopt         =on         # (0:off,1:on)
out-outvel         =off        # (0:off,1:on)
out-timesys        =gpst       # (0:gpst,1:utc,2:jst)
out-timeform       =hms        # (0:tow,1:hms)
out-timendec       =3
out-degform        =deg        # (0:deg,1:dms)
out-fieldsep       =
out-outsingle      =off        # (0:off,1:on)
out-maxsolstd      =0          # (m)
out-height         =ellipsoidal # (0:ellipsoidal,1:geodetic)
out-geoid          =internal   # (0:internal,1:egm96,2:egm08_2.5,3:egm08_1,4:gsi2000)
out-solstatic      =all        # (0:all,1:single)
out-nmeaintv1      =0          # (s)
out-nmeaintv2      =0          # (s)
out-outstat        =residual   # (0:off,1:state,2:residual)
stats-weightmode   =elevation  # (0:elevation,1:snr)
stats-eratio1      =1500
stats-eratio2      =300
stats-eratio5      =300
stats-errphase     =0.006      # (m)
stats-errphaseel   =0.006      # (m)
stats-errphasebl   =0          # (m/10km)
stats-errdoppler   =1          # (Hz)
stats-snrmax       =52         # (dB.Hz)
stats-stdbias      =30         # (m)
stats-stdiono      =0.03       # (m)
stats-stdtrop      =0.3        # (m)
stats-prnaccelh    =3          # (m/s^2)
stats-prnaccelv    =1          # (m/s^2)
stats-prnbias      =0.001      # (m)
stats-prniono      =0.001      # (m)
stats-prntrop      =0.0001     # (m)
stats-prnpos       =0          # (m)
stats-clkstab      =5e-12      # (s/s)
ant1-postype       =llh        # (0:llh,1:xyz,2:single,3:posfile,4:rinexhead,5:rtcm,6:raw)
ant1-pos1          =0          # (deg|m)
ant1-pos2          =0          # (deg|m)
ant1-pos3          =0          # (m|m)
ant1-anttype       =
ant1-antdele       =0          # (m)
ant1-antdeln       =0          # (m)
ant1-antdelu       =0          # (m)
ant2-postype       =rinexhead  # (0:llh,1:xyz,2:single,3:posfile,4:rinexhead,5:rtcm,6:raw)
ant2-pos1          =0          # (deg|m)
ant2-pos2          =0          # (deg|m)
ant2-pos3          =0          # (m|m)
ant2-anttype       =
ant2-antdele       =0          # (m)
ant2-antdeln       =0          # (m)
ant2-antdelu       =0          # (m)
ant2-maxaveep      =1
ant2-initrst       =on         # (0:off,1:on)
misc-timeinterp    =on         # (0:off,1:on)
misc-sbasatsel     =0          # (0:all)

RTKLIB user survey results

All right, I think it’s a good time to summarize the survey results. At this point I’ve had just over 100 completed survey responses and new responses have slowed to just a trickle. As with all my posts, I make no attempt to follow rigorous scientific methods, this is just a summary of the data and some informal observations.

First of all it’s interesting to see where people are using RTKLIB.

For what country or countries are you processing RTKLIB solutions?

Europe: 57% of total users (Oct blog views: 44%)
6 users: Germany, France
5 users : UK, Finland, Italy, Sweden
4 users: Switzerland
3 users: Russia
2 users: Poland, Slovakia, Austria, Ukraine, Spain
1 user: Czech Rep, Croatia, Slovenia, Ireland, Belarus, Turkey, Hungary

North America: 17% (Oct blog views: 21%)
15 users: USA
2 users: Canada

Asia: 8% (Oct blog views: 23%)
4 users: Japan
3 users: China
1 user: Taiwan

Oceania: 8% (Oct blog views: 4%)
6 users: Australia
2 users: New Zealand

South America: 6% (Oct blog views: 5%)
3 users: Brazil
1 user: Chile, Argentina, Columbia

Other (Africa/ Mideast): 4% (Oct blog views: 2%)
1 user: Uganda, Iran, GCC, Africa

It’s great to see such an international response! I’ve also included blog view statistics for comparison. I suspect less familiarity with English at least partially explains the comparatively low response rate from Asian countries relative to their blog views.

Here’s a summary of the remaining questions with a few comments and observations from me. Many of the questions allowed for multiple answers and some respondents didn’t answer all the questions, so both the total answers and total percentages vary from question to question.

It looks like users are fairly evenly split between business (48%), academic (38%), and personal applications (48%) but lean more towards post-processing solutions (67%) over real-time solutions (47%) and RTK/PPK solutions (67%) over PPP solutions (21%). Dual frequency solutions (71%) are most popular but many people are still using single frequency solutions (44%). I think the most surprising thing I saw here was that the percent of real-time solutions was as high as 47%, I would have expected lower.

Pretty evenly split here between beginner/intermediate and advanced/expert.

It’s great to see responses that span the range from new users to long time experts, although it can make it challenging some times to find the right balance for everyone when writing the posts.

It looks like just over 2/3 of respondents are using the Demo5 version of RTKLIB, at least some of the time. For those not using the Demo5 code, that’s fine, there should still be lots of useful information in the blog, but be aware that a good number of the features and results described in my posts apply only to the Demo5 version of the code.

I was surprised how few respondents are using RNX2RTKP, the CUI alternative to RTKPOST. RTKPOST is better for interactive experimentation, but RNX2RTKP is much better for processing large amounts of data. Maybe it’s time for another post about RNX2RTKP?

Again, it looks like there is a full range of answers here, all the way from daily use to never tried it.

A majority of Window users, which isn’t surprising given that the GUI apps are only fully supported in Windows. I’m still hoping someone from the linux world will step up to better support the linux GUIs.

It’s great to see that quite a few respondents are customizing the code themselves, that is one of the big advantages of open source code. One thing I’d like to explore more in my blogs is using RTKLIB as a library instead of a set of apps, as well as making some changes to the code to make this easier to do.

I’m always amazed at how many different applications that people have found for RTKLIB! Still, I was surprised that ground based surveying (64%) dominated the results as much as it did.

It looks like most people are working with a single receiver or pair of receivers but there are still a significant percent of respondents (31.4%) who are working on a bigger project involving at least five receivers.

These numbers suggest to me that more than half of the respondents are using local receivers or VRS as base rather than a CORS station.

I was surprised here how many respondents are looking for horizontal accuracies better than 5 cm given that there seems to be a shift in the industry to easier-to-use, more scalable solutions (PPP, PPP-RTK) that often offer decimeter accuracies rather than centimeter accuracies.

What brand /model of receivers do you typically use for the rover?

I’ve grouped the answers as best as I can to make them easier to summarize.

U-blox: (78 users total)
—F9P: 35 users
—M8T: 17 users
—M8N/M8P/M9: 4 users
—model not specified: 22 users
Emlid: 13 users
Novatel: 9 users
Trimble: 7 users
Leica: 4 users
3 users: Septentrio, Topcon
2 users: Hemisphere, Android, Sokkia, NVS
1 user: Swiftnav, Stonex, Allystar, DJI, South, Spectra, Ashtech, SIRF, Unicore, Sinan, NTLab, CHC, Span, Skytraq, Topodrone, SingoGNSS, Huawey, Hiro

Clearly dominated by u-blox and u-blox based receivers (mostly Emlid). I guess I was a little surprised that Swiftnav didn’t appear more frequently in the responses as either rover or base.

What brand/model or type (CORS, VRS, etc) of receiver do you typically use for the base in RTK/PPK solutions?

Again, I’ve grouped the actual answers to make them easier to summarize.

Reference Stations: (36% of total users)
—CORS: 37 users
—VRS: 7 users
U-blox: (28% of total users)
—F9P: 15 users
—M8T: 8 users
—M8N: 1 user
—model not specified: 11 users
Trimble: 11 users
Emlid: 7 users
Lecia: 6 users
Novatel: 4 users
Septentrio: 3 users
2 users: Topcon, Centipede
1 user: Drotek, Unicore, Sinan, NTlab, Skytraq, SinoGNSS, Hiro, NVS, Stonex

A more evenly distributed set of answers for the base than for the rover, but still dominated by u-blox receivers for those using dedicated receivers.

What is your largest frustration when using RTKLIB?

I tried to combine these and I’ve listed all the resulting categories that had at least two occurrences.

None: 31
Cryptic options/ Config file too complex / Too difficult to configure: 17
Solution not good enough/ not as good as internal F9P: 9
Lack of support for linux GUIs/MacOS: 6
Frustrations unrelated to demo5 version of RTKLIB: 5
Cryptic or missing error messages: 4
Lack of complete documentation in one place: 4
Non-intuitive/old-style GUIs: 4
Poor coding style/organization: 3
Too slow/ too much memory use: 3
False fixes: 2
Not compatible with other survey tools: 2
Limitations in RINEX/RTCM conversions: 2
Limited import/export options: 2
Differences between different versions of RTKLIB: 2

I think it’s a good list. I wouldn’t disagree with very much of it. Definitely some good suggestions for things that can be improved. Not surprisingly, the largest frustration was with the difficulty of configuring the solution. There were lots of other more specific frustrations that only got mentioned once that I didn’t have space to include here, but also made good feedback.

What features or fixes (up to 3) would you most like to see added to RTKLIB?

At first, I tried to consolidate this list as well but gave up because there were so many specific requests. I’ve just listed them below with some slight editing. Lots of good ideas. The suggestions that were the most common were IMU integration, better support for Linux GUIs, improved GUI interfaces, and automatic download of ephemeris/clock files required for a solution.

Brief description of your RTKLIB application:

Lots of interesting uses! Again I’ve just listed them all below with some slight editing.

Thank you everyone who responded to the survey! It’s great to hear what everyone is doing with RTKLIB. It’s also all great feedback and will be very helpful for choosing future blog post subjects and code features. The survey is still open so if you haven’t responded yet, you still can.

Listed responses:

New Feature Requests:

-Better multipath rejection
-Nrtk, nppk
-Support NTRIP caster
-linux gui
-port of demo5 to android
-Así está bien
-Better fix 😉
-Import Leica raw format
-multipath mitigation
-advanced algorithms for ambiguity resolution
-The possibility of using SP3 files containing GNSS precise ephemerides for more than 99 satellites
-included explanation for parameters (show them on hovering for example)
-Iono-free for dual frequeny receivers
-A more clear interface
-When used in post-processing, the Analyze the obs file to exclude satellites with low signal quality.
-Return error status appropriately
-Drawing kinematic profile. Non much report choise
-IMU integration (loosely and tightly)
-working gui for Linux
-multi station processing
-“I don’t understand most of the views in rtknavi, and they’re all unlabeled which is frustrating
-Documentation on file formats
-The buttons that are just an unlabeled box with no tooltip are very frustrating, especially when they don’t seem to do anything”
-Funcionalidad Stop and Go con rtk mas simple.
-automatic PPP all necessary files are downloaded from the Internet themselves.
-Simultaneous calculation forward – backward.
-full ntrip caster feature in str2str
-Better Linux support (for GUI)
-Simplified GUI like Emlid Studio,
-more useful diagnostic reporting
-tightly coupled processing with IMU data.
-CASTER option would be a great addition
-Plot in google earth.
-Install files with native apps rather than a folder with ini files – ie ini files stored in app data etc.
-A easy way to combine rinex data together, Often have to merge 1hz data with 15 min files.
-better work with multifrequency measurements, both in PLOT and POST
-Ability to copy/paste the ORI results in RTKPLOT. Fix tabbing behavior in TIME Select in RTKPLOT.
-option for str2str to compress base data or extract absolutely necessary data for long range low bitrate (14kbps) radio links between base and rover.
-I know rtklib benefit from static start but it would be absolutely fantastic if rtklib could support hybrid navigation with additional gyro,accelerometer, odometry data to help rtk processing and provide position in case gps signal is degraded or temporarily unavailable
-Better RAIM-like detection and rejection of interference and multipath. Support for using all available signal types (eg L1C, L1P, L1CA, L1M, L1Y) and frequencies (L1, L2, L5) in the nav solution simultaneously.
-help; tutorials;
-Fixed solutions criteria;
-CLI versions better usability (parameter selection)
-More tutorials, better interface
-Specific support for the L1+L5 GNSS chips that are coming out in the newest android phones
-Q= should report real accuracy
-A solid version for network and PPP static/kinematic solution,
-Multipath analysis
-Bigger UI window.
-More up to date/user friendly gui
-Tightly coupled gnss/ins soluions
-multi rover observations
-better and more robust kinematic solutions
-an automatically takeover of the internal q5 sol
-Linux GUI support
-rtkpost auto populate rinex nav/clock files if available based on base station file root name to make less steps when adding the additional files. feature
– add teqc like interface/functionality into rtkconv for help with rinex 3.02 conversions submitted to OPUS that return with errors.
-automatic download of sp3 and clk &etc. files
-taking advantage of wide lane combination
-more tightly integrated multi frequency solution
-faster processing
-Better import (offline) or interfaces (online) for correction data
-UI via a web application (using WebSocket and modern HTML5 etc). Should be platform neutral.
-for stop and go i would like to be able to tell rtkpost the time frames when i am stopped
-Clear reasons why RTKPost fails
-selection or ability to change the reference satellite (potentially multiple times in a session)
-any improvements to the moving-baseline tool for compassing
-Moving baseline support
-I think it’s quite feature complete and does everything I need.
-macOS support.
-Filenames not being reset in RTKCONV when changes are made.
-An active forum. Your blog is fantastic, I imagine you’ll be getting an email from me eventually, but have a StackExchange RTKLIB would be amazing.
-Sensor fusion module taking advantage of IMU/INU including in the ublox receiver
-Better integration between the apps
-reduce complexity in settings
-F9P msm translation to legacy messages to be received in my olds hemisphere receivers
-static baseline support.
-an “if this then that” guide (for example: if you want to include SP3 for ephemeris ans SBAS for correction then you must use navsys …. and ephopt … )
-the GUI for Linux
-fully nmea 0183 output sentences
-extended options referring to atmospheric models
-the possibility of placing own models inside
-I wish RTKlib indicated reasons why “fix” was unstable.
-Possibility to access/output filter internals (covariances, etc.)
-I want the vectors AND weighting in the form of covariances

RTKLIB User Applications:

-using it internaly in RTKBase (https://github.com/Stefal/rtkbase) and for PPK.
-Robotic Lawn Mower, it navigates without boundaries. But has “issues” when near walls or other objects that interfere with reception of GPS signals.
-PPK system for drones
-Ground survey for gcp, mapping in marine application
-Improving the quality of GPS position data recorded during marine surveys.
-Quality monitoring of network solution RTK service
-Research and development within the scope of the work done in Rokubun
-precise location of caves entrances with post process
-Post-processing of land survey
-Teaching and education
-vehicle navigation, AHRS, autonomous agriculture machine
-Teaching undergrad students to process real time/post-mission GNSS data in static, kinematic, PPP, and DGNSS modes
-Main use is for PPK solution for UAV applications but moving towards local NTRIP based RTK android applications as receivers become more available and price drops.
-Motorcycle sports riding trajectory visualization.
-Static and kinematic for surveying by land surveyor and drones
-Near real time land monitoring
-amateur survey and mapping
-diverse – from RTK to PPP
-Tracking how far off the ublox M8U sensor fusion solutions are to determine if that is acceptable for our V2X application
-Proyecto propio de construccion GNSS, homologacion de equipos GNSS en el trabajo
-drone flight trajectory calculation
-monitoring landslide with rtk and expirement of real time ppp
-accurately record archaeological features and to establish ground control points for aerial survey.
-Topographic and bathymetric survey
-Unmanned fixed-wings for aerial survey
-Post processing for Drone PPk photogrametry
-I run country wide CORS network but would love to aggregate my streams into a caster using RTKLIB and as well provide VRS solutions to rover users.
-Real time positioning of various sports
-Processing data in aid of finding errors.
-Low-cost equipment (cell phone, F9P etc.), short-observation-time static and RTK measurements in forests. Surveying, navigation (staking out), and combination with other methods (photogrammetry, laser scanning)
-GCPs for Aerial Surveys
-automatic mower that uses rtk gps position to mow according to map
-trying to pp poor data (float to fix hopefully)
-Forestry, surveying, boating
-Survey Ground Control Points for Photogrammetry with RTK, UAS picture geotagging with PPK.
-Processing for PPP, PPK
-Frustrating, confusing, and fun. It would help if I knew what I was doing.
-open pit mine and stock volume calculation
-Tested for PPP, some student projects (Smart phones, multipath, static baseline processing, etc)
-we develop gnss devices for agriculture
-Calcualtiom of drone positions
-single Freq dynamic RTK
-use it in a university context for education and projects. i also work privately with kinematic applications in the sports sector.
-Navigation validation
-Converting UBX to RINEX for OPUS etc, RTKPost to process static and kinematic solutions.
-UAS photogrammetry survey
-Precise measurements of reference points
-Agriculture: hedgerow scanning to measure sequestered carbon.
-we use it to for rinex convertion and str2str in our embedded app for streams
-woodland fish passage obstruction issues and habitat qualification
-collect observable satellite data on the ground using ublox F9P as a rover, then post-process against a base station
-Realtime drone positioning and simple ground based mapping
-high-resolution measurements of glacier motion in the Canadian Arctic.
-Post processing aerial photogrammetry
-My base is connected to RTK2GO by STSVR. RTKGPS+ used as rover for collect data (Android app). RTCONV and RTKPOST used for post prosessing.
-PPK for aerial drone surveying. We’re just starting out. Mostly surveying and construction applications so far. Hopefully move into digital agriculture soon.
-academic, leveling for tidal inundation gauges
-Double checking official survey point
-a survey robot which takes georeferenced picture inside crops and establish a map of diseases, weeds over the crops.
-Pre-processing for loosely coupled multi-sensor integration.
-control, boundary, topography

RTKLIB user survey

Usually I share my experiences with RTKLIB in this blog, but this time I’d like to find out more about everyone else’s experiences. I’ve put together a short survey here. I’m hoping as many readers as possible will respond, even if you have little or no direct experience with RTKLIB. The world of GNSS has evolved tremendously since I started this blog almost six years ago and I know that the users and uses of RTKLIB have evolved along with it but I’m curious to know more about the details. I’d like to keep the blog as relevant as possible to as many readers as possible, and so it will be very helpful to have this feedback.

I’m sure others are interested as well, so I will publish the results once everyone has has time to respond. In the meantime you can see a summary of the results up to date when you complete the survey.

Let me know in the comments if I’ve left out any questions I should have included. I may update the survey or publish a set of follow-on questions.

Comparing the u-blox F9P to the Comnav K803 receiver

In my last post I compared the internal u-blox F9P RTK solution to an RTKLIB demo5 RTK solution for a couple of cases, one with easy and one with moderately challenging conditions. In this post I will extend that comparison to some more difficult conditions, specifically under more dense tree foliage. To make things more interesting I will add a pair of ComNav K803 eval boards to the experiment.

The ComNav K803 is a relatively new module from Comnav that supports triple frequencies, L1, L2 , and L5. It is more expensive than the u-blox F9P at about $1000, but should have higher performance. ComNav generously gave me two of these boards for evaluation purposes.

As far as user interface goes, they are very similar to the older K501G or K708 ComNav boards and I was able to configure the K803 boards using RTKLIB command files very similar to what I previously described for the K708 in this post.

Here is a photo of the ComNav board on the left and the Ardusimple F9P board on the right. The ComNav board is inside a plastic case which is a nice feature. It is supposed to work with just the USB port for power and communication but I did not find this to be reliable and had to fall back to using the RS232 port with USB adapter for communication which is why the photo shows two cables for the K803. ComNav is aware of the issue and hopefully they will sort this out relatively soon.

In my previous experiment, I used a nearby CORS station for the base which limited the solution to GPS L1/L2, GLONASS L1/L2, and Galileo L1 and also extended the baseline to several kilometers . To take full advantage of both receivers available constellations and bands in this experiment, and to minimize the baseline, I used local base receivers for this experiment.

I used two Harxon geodetic GPS-500 antennas, one on the roof as base and one for the rover which was located on a tripod in an area underneath some moderate tree foliage. Both antennas were fed to an antenna splitter and then to an F9P receiver and a ComNav receiver. I tested three locations, each one further under the trees and so more challenging than the previous one. To avoid too much complexity in the experiment, I did not run a real-time RTKLIB solution but logged the F9P and ComNav raw data for post-processing.

Like in the previous experiment, I disconnected the antenna and then reconnected it multiple times to measure the time to first fix (TTFF) for both receivers. The plot below shows only the fixed epoch results for the first two locations. The F9P results are in green, and the ComNav in blue. The constant offset between the two is not real, I accidently configured the ComNav receiver to use a self-surveyed approximate position for the base location instead of an independently determined precise position.

Fix epochs for u-blox F9P (green) and ComNav K308 (blue) for location 1 and 2

In the first and least challenging rover location, both receivers performed well. The average ComNav TTFF was slightly faster, but both were below 20 seconds.

In the second location, both receivers were were still providing usable results but the ComNav receiver started to become noticeably better than the F9P. Average TTFF was 54 seconds for the F9P and 12 seconds for the ComNav receiver. In addition, the F9P had a couple of short false fixes and intermittent float after first fix that were not present in the ComNav data.

Here is the data for the third and most challenging location. Again, the F9P results are green and the ComNav results are blue.

Fix epochs for u-blox F9P (green) and ComNav K308 (blue) for location 3

In this case the differences were quite significant. The ComNav reliably found the same location on each re-acquire but more than 50% of the F9P fix epochs were false fixes, sometimes in error by several meters.

I also ran the RTKLIB demo5 solution with the same config settings as in the previous post. The results are below for the first two locations. The RTKLIB fixed epochs are in green and the ComNav fixed epochs are in blue. The performance at the first (least challenging) location was similar between RTKLIB and the F9P internal solution but the RTKLIB solution struggled with the second location and for the third location did not find any fixes except for a single short false fix. At least in these conditions it looks like there is still some room for improvement in the RTKLIB solution. However, it is also true that it did not get as much false fixes as the F9P solution which is important as well.

Fix epochs for u-blox F9P RTKLIB solution(green) and ComNav K308 (blue) for location 1 and 2

As in my other experiments, this is just a quick comparison of relative performance and is not intended to be any type of in-depth analysis.

To summarize the results, The u-blox F9P is a very capable dual-frequency receiver for easy to moderately challenging conditions, but for the most challenging conditions you will still get better performance from a more expensive receiver.

Comparing real-time precise (RTK) solutions for the u-blox F9P

The u-blox F9P internal RTK solution engine is very good and I have been recommending using it rather than RTKLIB for real-time RTK solutions, saving RTKLIB for F9P post-processing solutions. However, in this post, I will take another look at that recommendation.

Tomoji Takasu, the creator of RTKLIB, recently published some data also confirming the high quality of the F9P RTK solutions. He ran comparisons of the F9P against several other receivers of various price and quality. You can see his results in the 2021/07/01 entry of his online diary. His conclusion (at least the Google translation of the original Japanese) is that “Considering the price, there is no reason to choose other than F9P at this time.”

He did his comparisons by splitting an antenna output, feeding it to the two receivers being compared, then cycled the antenna output on and off. He fed the output of the receivers to RTKPLOT to produce plots like the example shown below from his diary which show acquire times and consistency of the fix location. In this case the upper plot is a Bynav C1-8S receiver and the lower plot is a u-blox F9P. The yellow intervals in the plot indicate float status and show the acquire times for each signal cycle. The green intervals indicate fix status and show the consistency of the fix location. Clearly the F9P outperformed the C1-8S in this example.

Receiver comparison data from Tomoji Takasu online diary

Inspired by his comparisons and the recent changes I incorporated into the demo5 b34c code, particularly the variable AR ratio threshold option described in my previous post, I decided it was time to do a similar head to head comparison between the internal F9P RTK solution and the latest demo5 RTKNAVI RTK solution.

To setup this experiment I used u-center to configure an Ardusimple F9P receiver to output both NMEA position messages (GGA) and raw observation messages (RAWX, SFRBX) to the USB port. I then setup STRSVR and RTKNAVI on my laptop as shown in the image below.

RTKLIB configuration for the comparison

The first instance of STRSVR is configured to stream NTRIP data from a nearby UNAVCO reference base (P041) to both the u-blox receiver and a local TCP/IP port. The output of the u-blox receiver is streamed to a second local TCP/IP port by selecting the “Output Received Stream to TCP Port” in the “Serial Options” window. The second instance of STRSVR then streams the second TCP/IP port containing the output of the u-blox receiver to a file for logging the internal F9P solution. This file will contain the raw observations in binary format as well as the receiver solution but RTKPLOT will ignore the binary data and plot just the NMEA message data.

The demo5_b34c version of RTKNAVI is configured to use the two TCP/IP ports configured above, one from the receiver, and one from the UNAVCO base, as inputs for rover and base. I also configured two instances of RTKPLOT so that I could see the real-time status of both solutions in addition to saving the results to files.

To setup the RTKNAVI config file for this experiment, I started with the PPK config file from the U-blox F9P kinematic PPP data set 12/24/20 data set and made a few changes to make it more suitable for a real-time solution. Time to first fix is not as important in post-processed solutions since they are usually run in both directions, so the config parameters in that file are set to minimize the chance of a false fix at the expense of relatively long acquire times. To shift this balance towards faster acquire times, I increased the maximum filter variance allowed for fix (Max Pos Var for AR / pos2-arthres1) from 0.1 to 1.0 and decreased the number of fix samples required for hold (Min Fix / pos2-arminfix) from 20 to 10. I also changed the solution type from combined to forward since combined mode is not possible for real-time solutions.

Most importantly, I enabled the new variable AR ratio threshold described in my previous post by setting the AR ratio threshold min and max (pos2-arthresmin, pos2-arthresmax) to 1.5 and 10. I left the nominal AR ratio threshold at 3.0. This means that instead of using a fixed AR ratio threshold of 3.0 for all epochs, the AR ratio threshold used in each epoch is selected from the blue curve below, with the limitation that it can not drop below the specified minimum of 1.5

AR ratio threshold curves

This will allow the AR ratio to drop well below 3.0 when there are many available satellites and the model strength is very high, while still protecting against false fixes by increasing the AR ratio when the number of satellites and the model strength are low.

To run the experiment, I connected the antenna to the receiver, waited until both solutions had acquired a fix, then disconnected the antenna for 10-20 seconds to force the receiver to lose lock to all the satellites, then reconnected the antenna again. I repeated this sequence for about 15 cycles.

I ran this experiment twice. The first time, I connected the receiver to a Harxon geodetic GPS-500 antenna mounted on my roof with a reasonably good view of the sky. The second time, I connected the receiver to a smaller, less expensive u-blox ANN-MB antenna with ground plane on a tripod in my backyard in a moderately challenging environment with the sky view partially blocked by the house and nearby trees. In both cases, the base station (P041) was 17 kms away and included Galileo E1 signals in addition to GPS and GLONASS.

Below is the result from the first experiment. As previously, yellow is float, and green is fix. The internal F9Psolution is on the top and the RTKNAVI solution is below. The blue in the F9P plot indicates a dead reckoning solution which only occurs when the antenna is disconnected, so can be ignored.. To reduce clutter in the plots I am showing only the U/D axis. In this case I know the precise location of my roof antenna so the y-axis values are absolute errors.

F9P vs RTKNAVI, GPS-500 antenna on roof

Here is a zoomed in version of the same plot. Both solutions acquire first fix fairly quickly in most cases. The absolute errors during fix are small for both solutions but do appear to be a little larger in the internal F9P solution. None of the errors are large enough to be considered false fixes.

F9P vs RTKNAVI, GPS-500 antenna on roof (zoomed in)

Below is the same plot for the second experiment where the smaller ANN-MB antenna is located in a more challenging environment. Again, both solutions tend to acquire first fix quite quickly, and again the internal F9P errors appear to be larger than the RTKNAVI errors. In this case I don’t know the precise location of the antenna so the errors are relative.

F9P vs RTKNAVI, ANN-MB antenna in backyard (zoomed in)

Here is a summary of the acquire times for both solutions measured from the above plots. The plots don’t show precisely when the antenna was reconnected so I measured both acquire times starting from the first solution output sample after the disconnect gap, regardless of which solution it came from. The first column in the table is the number of acquisitions measured, the other columns show the minimum, maximum, mean, and standard deviations of the time to first fix in seconds.

GPS-500 antenna on roof
ANN-MB antenna in backyard
Time to first fix comparison between F9P and RTKNAVI

Both solutions performed quite well in both experiments. The RTKNAVI solution outperformed the F9P internal solution in both cases, but given the very small amount of data and testing conditions, I would be reluctant to declare RTKNAVI the winner. I does suggest though, that the RTKLIB solution has closed the gap and should be considered at least roughly equal in performance to the internal RTK engine for real-time solutions.

In many cases it will still be simpler for real-time solutions to use the internal solution than RTKNAVI since it requires less configuration. There are cases, however, where it makes more sense to use an external solution even in real-time. For example, one user recently shared data with me where the rover is measuring real-time buoy activity. In order to avoid needing a bi-directional radio link, he sends the rover raw observations back to shore and calculates the solution there. Otherwise he would need to send the raw base observations to the buoy, and then send the resulting solution back to shore.

If anyone else runs a similar experiment comparing RTKNAVI to the internal F9P solution and wants to share their results, please leave a comment below.