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.


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.

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

Using U-blox receiver options in RTKLIB

One of the less well documented features in RTKLIB is the capability to set receiver options. These are different from the solution options that are specified in the configuration file or through the GUI option menus. They are also different from the receiver commands that are used to send configuration messages directly to the receivers. The receiver options are specific to each receiver type. Actually, to be more precise, they are unique to each receiver output format, not each receiver. If a u-blox receiver is outputting RTCM3 messages, then only RTCM3 specific receiver options will be applied, not any u-blox options.

The receiver options are applied when translating the raw receiver output messages into internal RTKLIB observations or rinex files. In this post, I will describe the most commonly used options specific to u-blox receivers and the demo5 version of RTKLIB. Some of these options are also supported by other receivers and other versions of RTKLIB and I will try to make that clear in the discussion as well. I will also explain how to apply receiver options in the different RTKLIB apps.

First of all, here is a list of the receiver options available for the u-blox receivers in the demo5 RTKLIB code. They are listed along with options for all the other receivers in Appendix D.5 of the RTKLIB user manual. I have recently updated the demo5 version of the manual for the u-blox receivers but other listed receivers may not be fully up to date.

U-blox Receiver Options in Demo5 RTKLIB

– EPHALL: Input all of ephemerides
– INVCP: Invert polarity of carrier-phase
– TADJ=tint: Adjust time tags to multiples of tint (sec)
– MULTICODE: Preserve multiple signal codes for single freq
– MAX_STD_CP=n: Reject observations with StDev > n
-STD_SLIP=n: Set cycle slip for phase obs with StDev > n


The most commonly used option is “-TADJ”. Unlike most other receivers, the u-blox receivers include the estimated receiver clock error in the observation time stamps. This causes them to deviate from nice round numbers.   With most receivers, if you set the sample interval to one second, you will get observation timestamps at 1.0000, 2.0000, 3.0000… . With the u-blox receivers you will see something more like 0.9973, 1.9973, 2.9973… . This doesn’t cause any issues with downstream RTKLIB processing and so there is normally no need to adjust the timestamps. However, other applications sometimes expect the timestamps to occur on the rounded intervals and complain if they don’t. One of the most common questions I get is from readers asking how to deal with this problem.

Setting the receiver option to “TADJ=1.0” will adjust each observation so that it falls on the nearest rounded second. “TADJ=0.1” will do the same thing, except round to the nearest tenth of a second. Note that it is not enough for the code to just change the timestamp. To maintain consistency in the observations, the pseudorange and carrier phase measurements are also adjusted by a distance equivalent to the change in time of the timestamp. This option is available in all versions of RTKLIB.


The u-blox binary observation messages include a standard deviation estimate for each pseudorange and carrier phase measurement. The internal RTKLIB observation variables and the rinex file format only handle the observations, not the uncertainty estimates, so this additional information is lost. Instead of just discarding this information, RTKLIB uses it to filter the observations. In the 2.4.3 code and until the most recent version of demo5 code, the default behavior was to discard all carrier phase observations with standard deviation estimates greater than 5. Typically, discarding the lowest quality observations gives better solutions than including them. However, I found that the scale of the standard deviation estimates of the Gen9 u-blox modules (e.g. F9P) seem to be a little different than the Gen8 modules (e.g. M8T) Because of this I have increased the default threshold to discard the carrier phase observations for Gen9 modules from 5 to 8 in the most recent version of the demo5 code. I have not modified the default threshold for Gen8 modules.

The “MAX_STD_CP=n” option allows the user to choose this threshold themselves if they don’t feel the default is optimal. The optimal value can also vary depending on use case. For example, when assisting with some research on precise tracking of birds, I found that when using the M8T modules, the default value of “5” was fine for lower acceleration migratory birds, but increasing the threshold to “8” improved the solutions when tracking higher acceleration birds of prey.

Be aware this option is only available if you are logging the binary u-blox messages. If you are using the RTCM3 messages, then all observations are passed on, regardless of quality. In environments with good sky views where most of the observations are of high quality, this difference will be negligible, but in more challenging environments with obstructed sky views, this adjustment can make a bigger difference. This option is only available in the demo5 code, the 2.4.3 code uses a fixed threshold of five for all u-blox modules.


The “STD_SLIP=n” option is similar to the previous option. However, in this case, instead of discarding the observations with standard deviations greater than a threshold, it sets a cycle slip if the standard deviation is greater than or equal to the threshold. The two options can be used together to set a cycle slip on the lower quality observations but discard the lowest quality observations. However I prefer to just use the previous option and not this one. This option is also available in the 2.4.3 code but since observations with standard deviations greater than 5 are always discarded in that code, it is of only very limited use.


This option only applies to the F9P dual frequency module and is only in the demo5 code. The F9P outputs different GPS L2C observation codes depending on whether it decoded the CM or the CL codes in the raw signal. It usually reports the long code (CL) but will occasionally report some medium code (CM) messages as it is acquiring a new satellite. The 2.4.3 version of RTKLIB reports these observation codes as is. Unfortunately RTKLIB does not handle multiple code observation types for a single constellation frequency well and they tend to degrade the solution. To avoid this issue, I have followed the lead of the Emlid version of RTKLIB code and modified the default behavior of the demo5 version to combine both observation codes into a single combined observation code (L2X) which is reported regardless of whether the F9P reports the CM observation code (L2S) or the CL observation code (L2L). For processing RTKLIB solutions this is the preferred option, but for other uses, users might prefer to keep the original codes. Using the “-MULTICODE” option, reverts back to this behavior.

I have never found a need to use the remaining two options (-EPHALL, -INVCP) and am not familiar with any situation where they would be used.

Setting Receiver Options

How to set these options varies depending on which RTKLIB app you are using. It only applies to those apps which work directly with the raw receiver messages or files. These include RTKCONV and CONVBIN for post-processing, and RTKNAVI and RTKRCV for real-time processing.

In RTKCONV, use the “Receiver Options” box in the “Options” menu as shown on the left below. In RTKNAVI, use the “Receiver Option” box selected from the “Opt” option in the “Input Streams” menu as shown on the right below.

Setting receiver options in RTKCONV (left) and RTKNAVI (right)

You can include multiple options in the box as shown in the examples above.

In CONVBIN, use the “-ro” option on the command line. For example

convbin run1.ubx -ro “-MAX_STD_CP=3 -TADJ=1.0”

The quotes are only necessary if including more than one option.

At this point, RTKRCV does not appear to support receiver options but I hope to add this capability to the demo5 version in the near future.

I think that’s all there is to receiver options but if I’ve left anything out, let me know in the comments below and I will try to answer any questions.

Another look at L1/L5 cellphone PPK with RTKLIB and an Xiaomi Mi8 phone

I last looked at a L1/L5 dual frequency cellphone data set from the Xiaomi Mi8 cell phone about 18 months ago in this post. Recently I had some questions and discussion in the comment section regarding a data set from another Mi8 phone, in this case collected by a group at the Universität der Bundeswehr München and available for download here. I gave a few hints for getting a reasonable PPK solution with RTKLIB in my responses to the comments but thought it deserved a more complete explanation.

The data itself is quite unique. It was collected while the phone was sitting on a choke ring antenna base to reduce multipath as shown in this image from the above link. The choke ring base and phone were then rotated around a pivot to create a circular trajectory.

Universität der Bundeswehr München: Phone on choke ring base

For this analysis I used the latest b34_d3 version of the demo5 RTKLIB code for which the Window executables are available on the rtkexplorer website or the source code is on the b34_dev branch on Github. I did make some recent changes to this code to improve the L1/L5 solutions so you will need to use this code version or something more recent if you are attempting to duplicate my results.

First let’s take a look at the raw observations from the cell phone. The quality looks quite good, there are relatively few cycle slips (red ticks) or missing samples. The blue lines indicate L1 and L5 measurements and the red lines indicate L1-only measurements. The accompanying base data includes matching observations for all of these satellites and frequencies.

Raw observations from Xiaomi Mi8 cell phone

Before running a solution, I did a little pre-processing of the raw observations to reduce the number of codes since RTKLIB sometimes does not handle large numbers of different codes well. Using RTKCONV to convert from rinex format to rinex format, I reduced the number of codes for both base and rover from the original rinex headers below on the left, to the new rinex headers on the right. I used the frequency and code buttons in the Options menu to select the desired frequencies and codes. Not only does this tend to give better results when multiple codes are provided for single frequencies, but it also makes debugging easier if things don’t go well on the initial solution attempt.

[Note for b34_d3 version of RTKCONV: Use the “L3” button to select “L5/E5a” frequencies. I will be updating this in the next release to label it as “L5/E5a” to match the naming convention used in the other demo5 apps]

Base codes before and after RTKCONV rinex->rinex conversion

Rover codes before and after RTKCONV rinex->rinex conversion

Next, I downloaded a multi-GNSS navigation file for the date of this data from CDDIS. There was a navigation file included with the data but it did not seem to use the standard format for the Galileo ephemeris and RTKLIB was not able to extract valid ephemeris for the Galileo satellites from that file.

For a config file I started with the sample PPK config file for the u-blox F9P that is included with the demo5 b34 executables (f9p_ppk.conf). However I made a few changes to optimize for the dataset. Here is a list of the changes I made:

pos1-frequency: l1+l2 -> l1+l2+l5 # enable L5
pos1-snrmask_r: off -> on # enable SNR mask for rover
pos2-snrmask_b: off-> on # enable SNR mask for base
pos1-snrmask_L1: 35,35,… -> 34,34,… # reduce threshold for low SNR
pos2-arlockcnt: 0 -> 5 # improved cycle slip detection
pos2-arminfix: 20 -> 10 # adjust for 1 Hz sample rate
pos2-rejioinno: 2 -> 1 # tighten outlier threshold
stats-errphase: 0.003 -> 0.006 # adjust for larger residuals
stats-errphaseel: 0.003 -> 0.006 # adjust for larger residuals
stats-prnbias: 0.0001 -> 0.001 # adjust for larger residual biases

The above names are what are used in the config file. For those modifying the parameters directly in the RTKPOST GUI, the equivalent changes are:

Frequencies: L1+L2/E5b -> L1+L2/E5b+L5/E5a
SNR Mask:Rover: unchecked -> checked
SNR Mask:Base: unchecked -> checked
SNR Mask: SNR values: 35->34
Min Lock: 0 -> 5
Min Fix: 20 -> 10
Reject Threshold of Innov: 2 -> 1
Carrier-Phase Error a+b: 0.003 -> 0.006
Carrier-Phase Error sinEl: 0.003 -> 0.006
Carrier-Phase Bias: 0.0001 -> 0.001

Here’s a brief explanation of each of the above changes:

I prefer to run solutions with the SNR mask enabled but don’t leave it on in the default file since the optimal SNR threshold can vary for different receivers. In this case, the SNR values of the rover data are fairly low, presumably due to the relatively low quality antenna in the phone, so I somewhat arbitrarily decreased the minimum SNR threshold to 34 dbHz.

After running an initial solution I also noticed that the solution residuals plotted with RTKPLOT were a fair bit higher than I am used to seeing with u-blox solutions. I’m assuming this is related to the low SNR’s and due to the very small, relatively low quality antenna in the phone. To adjust for this I increased the two terms used to calculate the standard deviations of the carrier phase measurements (errphase, errphaseel) from 0.003 to 0.006. The biases in the carrier phase residuals also looked unusually large so I increased the standard deviation of the carrier phase biases (prnbias) from 0.0001 to 0.001.

I generally set arlockcnt=5 for all receiver types except u-blox, since I find most other receivers allow more unflagged cycle slips than the u-blox receivers do.

The default config file is optimized for a 5 Hz sample rate and this data is 1 Hz so I lowered arminfix from 20 to 10 since it is measured in samples not seconds.

I normally prefer to set the outlier threshold (rejionno) to 1 meter but have recently increased it to 2 meters in the default config file. This is because I have found that 1 meter is occasionally too low and a too low value causes more problems than a too high value. For this experiment, I put it back to 1 meter.

For the most part, these changes are relatively minor optimizations for the specifics of this dataset and are not necessarily related to differences between L1/L2 solutions and L1/L5 solutions. For more information on any of these config parameters see appendix F in the demo5 version of the RTKLIB manual. This is also included with the demo5 executables.

I then ran a combined (forward+backward) PPK solution using RTKPOST. The result was quite decent with a fix rate of 98.2% with relatively small deviations from the expected circular trajectory.

One thing however that was somewhat unusual about this solution is the relatively large differences in the vertical component between the forward and backward solutions. A useful tip with RTKPLOT is that if you plot the “.pos.stat” residual file instead of the “.pos” solution file, you will see both forward and backward solutions before they are combined as shown in the plot below.

Notice that the differences between forward and backward solutions are reasonable for the horizontal components, but quite large, as much as +/-10 cm for the vertical component. Usually large differences between forward and backward are caused by false fixes and appear in all components. I don’t fully understand this result but I don’t believe it is a false fix and suspect that it is due to the large amount of noise in the raw observations. These differences were actually large enough to cause RTKPOST to downgrade some of the fixed forward and backward solution points to float before I increased the “stats-xxx” config parameters and in fact were part of the reason I increased these.

It’s always a little hard to know how well a config file optimized for a single data set will generalize to other similar data sets. To get at least a sanity check for this config file, I ran it on the Mi8 data set from the earlier post. The data was actually static but I ran it as a kinematic solution. The result matched the previous solution position with 100.0% fix rate and reduced the time to first fix by more than a minute from the previous solution, so it passed the sanity check.

Previous Mi8 data set run with the new code and config file

If you do run this code and config file on your own cell phone data, I would be interested to know how it goes and I think other readers would be as well. You can leave a comment below.

One thing to consider when collecting data is the ground plane. I suspect that most people won’t have an extra choke ring antenna base lying around but I would suggest using at least a metal disk under the phone as a ground plane to reduce multipath.

A look at the recently released RTKLIB b34 code

In January 2021, after a fairly long gap of 17 months without code updates, Tomoji Takasu released a b34 update to the 2.4.3 RTKLIB code. He and his team apparently were very busy over that time as, according to Github, the new release has 1064 changed files, 279,767 additions and 312,550 deletions! While some of these numbers come from reorganizing the file structure, this still constitutes a major rewrite of the codebase and a significant challenge to merging the updates into the demo5 version of RTKLIB.

After a fair bit of effort I have completed a first pass at this merge. Given the magnitude of the changes, I have decided to keep this update on a separate branch in the demo5 repository until it is better tested and more stable. It is now on the demo5_b34_dev branch and the beta executables, along with the more stable b33f version, are available here. All of the Windows GUI and CUI apps appear to run as well as all the linux CUI apps. I have done a very limited amount of testing and am not aware of any major issues in any of the apps at the moment but expect that with more testing, issues will be found. Unfortunately the linux GUI Qt apps have not been updated and given the amount of work involved to do this they will most likely be dropped from both the 2.4.3 and the demo5 code.

At this point I am looking for feedback from regular (or new) users of the demo5 version of RTKLIB. In particular, I would like to focus on finding and fixing features or capabilities that are functional in either the demo5 b33 code or the 2.4.3 b34 code but that are not functional in the demo5 b34 code. Ideally, if you find an issue of this sort, if you can send me an email with your data set, config file, and results with both the good code and the bad code, and a detailed description of the problems, it will make it easier for me to track them down. I will also be monitoring issues reported to the demo5 Github repository, so you can use that mechanism as well, it may just be harder to share data that way.

One thing I should mention is that at this point, the Swiftnav, Comnav, and Tersus receivers are not supported by the demo5 b34 code since they are not supported in the 2.4.3 code but I hope to eventually bring these receivers back into the demo5 code. In the meantime you can still convert files from these receivers to RINEX using the b33 code and run post-processed solutions with the b34 code.

In my limited testing I did not find significant differences in the results between the b33 and b34 code but I believe the emphasis of the changes was on basic structural improvements as well as improvements for the newer constellations and signals which for the most part were not included in my testing. I ran a data set collected from a u-blox F9P moving rover with a PPK solution using a CORS reference as base as well as a kinematic PPP solution. The PPK solution was virtually identical between the two codes but the b34 code did run about 30% faster which is a nice improvement. The PPP results were similar but the b34 results were no better and maybe a little worse than the b33 results so there may still be some room for improvement there. I do think that many of the structural changes will be valuable in the long term even if they do not have an immediate payoff.

There are also some new features in the code that I am looking forward to exploring after I’m more comfortable that the basic functionality is there.

So give it a try and let me know what you find!

I will try to update the beta source code and executables fairly frequently as I make fixes and will eventually move them to the main branch of Github but will probably keep the b33 code around on a separate branch for the foreseeable future.

Exploring kinematic single-receiver solutions with RTKLIB and the u-blox F9P.

Most of my work with RTKLIB has been done with differential solutions (RTK or PPK) using two receivers, a base and a rover. I have briefly explored static PPP solutions but have not previously looked at kinematic PPP solutions or analyzed the internal standard precision solutions of the u-blox F9P receiver. In this post, I will take a closer look at these options.

For this experiment I started by collecting a data set using an F9P receiver connected to a u-blox ANN-MB-00 antenna mounted on the roof of my car. The first 40 minutes were static followed by another 30 minutes of driving around residential and light industrial neighborhoods, all sparsely treed. The goal here was to start with something not overly challenging so I intentionally avoided any significant tree canopy, underpasses, or tall buildings. I enabled and logged u-blox raw observation and navigation messages (RXM-RAWX and RXM-SFRBX) as well as NMEA solution messages for the internal F9P standard precision solution ($GNGGA, $GNGLL, $GNGST).

I’ve uploaded this logged raw data file as well as all other necessary files to generate the solutions described below to the download section of my website in case anyone wants to download the data and duplicate my results. I did make some changes to the RTKLIB code as I worked through the experiment, so you will also want to download the executables for the latest b33f version of the demo5 RTKLIB code.

To generate a ground truth for the subsequent comparisons, I first converted the raw observation data file to rinex using RTKCONV and then ran a combined-mode PPK solution of the raw data against a nearby CORS station using the demo5 b33f version of RTKPOST and my standard configuration settings for the F9P (ppk.conf in the data download folder). The fix rate in the resulting solution is 99.9% and the CORS station is less than 10 km away, so I have a relatively high confidence in the accuracy of this solution. Note that the base coordinates in the CORS station rinex header are NAD83 while the single receiver solutions will all use the WGS84 datum. To correct for this, I have manually specified the base position in the config file options in WGS84 coordinates adjusted for the date of the data set.

In the image below, the raw observations are on the left, and the ground track of the PPK solution is on the right. The transition in the raw observations where the cycle slips (red ticks) begin indicate when the car started moving.

Raw observations on the left and RTKLIB PPK solution on the right

Since the raw log file from the F9P receiver includes NMEA position messages in addition to the raw observations, I can plot this file as a solution file directly with RTKPLOT. This will extract the NMEA positions from the file and ignore the raw observations.

If I use RTKPLOT to plot both the RTKLIB PPK solution and the F9P real-time (NMEA) solutions, I can then select the “1-2” button to plot the difference between the two solutions. Since the errors in the real-time solution will be much greater than the PPK solution, we can take this plot to indicate the error in the real-time solution.

Difference between RTKLIB PPK solution and F9P real-time standard precision solution

Note that the errors are larger (and lower frequency) during the static portion of the data set on the left half of the plot than they are on the right when the car is moving. This may seem counter-intuitive but it is because the multipath component of the error gets randomized by the movement of the receiver antenna relative to the satellite signals.

If you look at the datasheet for the F9P, you will see that horizontal accuracy is specified as 1.5 m CEP accuracy for a PVT solution and 1.0 m CEP accuracy for an SBAS solution. I didn’t calculate the exact CEP value for the plot above, but it would correspond to where the square root of the sum of the squares of the two horizontal components was less than the spec for 50% of the time. In this case the solution included SBAS augmentation so I would expect the 1.0 m accuracy spec to apply. Just eyeballing the plot, it looks we are getting at least this accuracy during the static portion and even better accuracy during the dynamic portion. This makes sense since the spec is for a static case. I could not find an F9P spec for dynamic accuracy.

Note that I have upgraded the firmware in the F9P module to version 1.13 which was released fairly recently. SBAS support was added to the F9P with the 1.13 upgrade so I suspect if you are running older firmware on your F9P, you may see larger errors. You can check the firmware version running on your module by querying the UBX-MON-VER message from u-center.

Sometimes it’s hard, at least for me, to look at the raw error numbers and visualize what they mean in the real world, so I have shown a snapshot of the two ground tracks below, the green dots are the PPK solution, and the yellow dots are the real-time F9P solution. I suspect for many applications, the level of error in the real-time solution would be acceptable. I was actually surprised to see how good it is.

Ground track of RTKLIB PPK solution (green dots) and F9P real-time solution (yellow dots)

So next, let’s look at the RTKLIB single frequency post-processing solutions. I will start with the “Single” positioning mode solution. This mode gives a very coarse solution and is really only suitable for initial approximate locations for the other solution types but we’ll take a quick look at it anyways. I ran an RTKLIB solution using the same config file as for the PPK solution, I just changed the “Positioning Mode” option from “Kinematic” to “Single. I then plotted the difference between this solution and my reference solution as I did before. Below is a plot of the difference between the two solutions.

Difference between RTKLIB PPK solution and RTKLIB single solution

As you can see, the errors are much larger than the real-time F9P solution and so of very little use.

Next, let’s look at the RTKLIB Kinematic PPP solution to see if there is any opportunity to improve upon the real-time solution here. To create a kinematic PPP solution I used the raw observation and navigation file from the F9P, along with precise ephemeris and clock files, a recent DCB (differential code bias) file, an antenna calibration file, and the ppp.conf config file, all included in the uploaded data set folder. I used very similar configuration settings to the PPK solution with a few exceptions. First, I enabled or configured all the PPP relevant parameters. For now, you can see the details of these settings in the ppp.conf configuration file, I hope to cover them in more detail in a future post. Next, I increased the minimum elevation mask from 15 degrees to 20 degrees based on earlier experiences showing that the RTKLIB PPP solutions are more vulnerable to errors and cycle slips in the low elevation satellites than are the PPK solutions. Also, I increased the outlier threshold from 1 meter to 30 meters since the residuals are much larger in the PPP solution and the outlier handling is different. I then ran two solutions, one with the first rapid precise ephemeris/clock files I was able to find online published after the data was collected (SHAOMGXRAP*.*), and the second solution was run with the final precise ephemeris/clock files (ESAOMGNFIN*.*). Both solutions were run with the most recent DCB files I was able to find which were based on analysis from Nov 2020. There are a number of online repositories of precise ephemeris data but many of these are GPS and GLONASS only, it is more difficult to find precise files that include Galileo and Beidou as well. The CDDIS, ESA, IGS, CODE, and other websites all have different variations of precise ephemeris files available for download but I have not yet found any one site that is best for both rapid and final multi-constellation ephemeris files.

Below are the differences between the two PPP solutions and the same PPK reference solution as used above, with the rapid ephemeris solution on the top, and the final ephemeris solution below.

Difference between RTKLIB PPK solution and RTKLIB PPP solutions. Top solution used rapid precise ephemeris/clock files, Bottom solution used final precise ephemeris/clock files.

In this case the rapid ephemeris/clock files were available the following day, the final ephemeris/clock files were not available until a week after the data was collected. Both solutions show smaller errors than the F9P real-time solution and the errors in the final solution are smaller than in the rapid solution, as would be expected.

PPP solutions typically have long convergence times, so some readers might be asking themselves why they don’t see any signs of this in these PPP solutions. The answer is because they were run in “combined” solution mode meaning the solution is run forwards and backwards and the two combined. Typically in a combined solution, and this includes the 2.4.3 version of RTKLIB, the kalman filter states are reset between the forward pass and the backwards pass to insure the two solutions are independent. In the demo5 code I have chosen not to reset the filter states unless it is a PPK solution with fix-and-hold enabled. This means the filter states will be fully converged at the beginning of the backwards pass and this will improve the overall accuracy of the solution at the possible expense of some theoretical loss of integrity in the solution, although I have not found this to be an issue in my limited testing. The convergence still needs time to occur however, so I would not recommend using this technique on data sets less than half an hour and even that might be marginal.

As you might expect, the errors in the PPP solutions are a fair bit lower than the real-time solution, while still quite a bit larger than the PPK errors. I suspect there are situations where these solutions would be of use, particularly where local CORS stations are not available. The biggest caveat is that the PPP solutions are less robust than either of the other two solution types and it is also more difficult to detect larger errors in the PPP solutions compared to the PPK solutions since there is no verification step from the ambiguity resolution.

Use of static PPP solutions seem to be quite common, I see less use of kinematic PPP solutions, so I was somewhat surprised and pleased to see how well the RTKLIB kinematic PPP solutions did work.

For static PPP solutions I prefer to use the free online CSRS PPP solution service I’ve described in other posts rather than RTKLIB since it is simpler to just submit the observation file than it is to find the precise ephemeris file and I also have more confidence in the accuracy estimates of the CSRS solution. It does take longer to converge than the RTKLIB solution since it is only using GPS and GLONASS satellites but this is only a minor inconvenience for a static measurement. For a kinematic measurement, the smaller number of satellites is a bigger problem but I thought it was worth a shot so I submitted the raw data file to CSRS and specified a kinematic solution. The CSRS solution is not directly plottable with RTKPLOT but I wrote a short python script to convert it to RTKLIB solution format and plotted the difference from the reference solution below.

Difference between RTKLIB PPK solution and CSRS kinematic PPP solution

The solution is excellent while the car is stationary but not much better than the real-time F9P solution when the car is moving, so this is also probably not a useful solution for a moving rover. It is interesting that in this case, unlike all the other solutions, the errors are larger when the car is moving than when it is stationary. This is probably because of the increased number of cycle slips during this time.

While that is probably enough for an initial exploration, I hope to take a closer look at some of these results as well as potential improvements in future posts. Please comment below if you would like to add anything else to the discussion.

[Updated 1/16/21 to correct for an error in the original translation of the CORS station coordinates to WGS84 coordinates for the date of the data set]

Building RTKLIB code in Windows

For most users running RTKLIB in Windows there is probably no need to build their own executables, it’s simpler to just download the pre-built demo5 executables from here or the 2.4.3 executables from here.

However, there are a few reasons why you might choose to download the source code and build your own executables instead. First of all, the pre-built executables are built with a large number of options enabled, but not all options. This means they will work for most user configurations but there are some less common configurations they won’t be able to handle. In addition, because they have so many options enabled, they will run slower and use more memory than executables built with only the minimum required number of options enabled.

Also, the latest available executables will not include the most recent updates to the code, you will need to build your own version to get these. And of course, anyone who wants to make any changes to the code will have to build their own executables.

OK, so let’s say you’ve decided to build your own versions. How do you go about doing it? It’s not a complicated process, most reasonably computer-savvy users would be able to figure it out on their own, but I’ll try to provide a few tips below to make it a little quicker and less painful.

The first step is to download the source code from the Github repository. I will assume you are using the demo5 code which is available here, but the same process will work with the official 2.4.2 or 2.4.3 code as well. Click on the “Code” box on the Github repository page to download the code. If you are familiar with Github you may choose to clone the repository or open with Github desktop but for most users it is probably simplest to just choose the “Download ZIP” option and then unzip the code to your desired location on your local drive.

The next step is to choose your compiler. You can use either Microsoft Visual Studio or Embarcadero C++ Builder. Both have community editions which can be downloaded and used for free for non-commercial use. However, Visual Studio will only build the RTKLIB command line apps, not the GUIs, so unless you already have Visual Studio on your computer and only need the command line apps, I would suggest using the Embarcadero compiler.

Now go back to the RTKLIB code folder you just downloaded and unzipped. Most of the common source code is in the “/src” folder but each RTKLIB app has its own sub-folder inside the “/app” folder. Let’s use RNX2RTKP, the RTKLIB command line post-processing solution app, as an initial example. In the “/app/rnx2rtkp” folder you will see a number of different sub-folders for different build methods.

Build a single app with the Embarcadero C++ Builder

Let’s assume for now you are using the Embarcadero compiler and want to build the 64-bit version of the code. You will want to open the “/bcc_win64” folder and within that folder you will see an Embarcadero project file called “_rnx2rtkp_win64.cbproj”. Double-click on this file to open the 64-bit version of the project in the Embarcadero compiler. If your hardware only supports 32-bit executables, you can select the “/bcc/_rnx2rtkp.cbproj” project file instead. When the compiler opens, it should look something like this:

From the “Project” menu, click on “Make _rnx2rtkp_win64” to compile the code. When done, the result should look like the image below. Make sure it says “Linking: Done” and “Errors: 0”

And that’s it. The result, “_rnx2rtkp_win64.exe”, will be in the “\app\rnx2rtkp\bcc_win64\Release_Build” folder.

The only differences for the GUI apps are that the project files are directly in the app folders rather than in sub-folders and in some cases there are separate app folders for the 32 bit and 64 bit versions. For example, for RTKNAVI, you will find a “app/rtknavi/rtknavi.cbproj” project file and a “app/rtknavi_win64/rtknavi_win64.cbproj” project file.

Build a single command line app with Microsoft Visual Studio.

The instructions for Microsoft Visual Studio are very similar, however you will only be able to build the CONVBIN and RNX2RTKP apps since Visual Studio does not support the GUI apps.

After installing Visual Studio, click on the “msc.sln” project file in the “\app\rnx2rtkp” folder to open the project.

Choose “Build Solution” from the “Build” menu to compile the code. The resulting executable will be in the “app\rnx2rtkp\msc\Release” folder.

The project will default to a 32-bit version but you can add a 64-bit version using the Configuration Manager, just be sure to to check the Build box in the new configuration as shown below.

Building and installing the entire RTKLIB library with the Embarcadero C++ Builder

If you are using the Embarcadero compiler and would like to build all the RTKLIB apps at once instead of one at a time, you can use the “rtklib_consapp.groupproj” and “rtklib_winapp.groupproj” project files in the “/app” folder to build the console apps and GUI apps respectively. From the “Project” menu, select “Make All Projects” for each of these group projects. You will see errors when the GUI project attempts to build the RTKVIDEO and RTKVPLAYER apps but you can ignore these, assuming you don’t plan to use these apps.

After completing the builds, run the batch files “app/install_consapp.bat” and “app/install_winapp.bat” to copy all the executables from the “Release” folders to the single “../RTKLIB_bin/bin” folder. You will need to create the destination folder first to avoid errors when it copies the files.

Modifying the build options with the Embarcadero C++ builder

So far, I’ve assumed you want to build with the default build options but sometimes you will want to change these. I will describe the procedure for the Embarcadero compiler, but the procedure is very similar for Visual Studio.

Open the project in Embarcadero for an individual app and then select “Options” from the Project Menu. Select “C++ Compiler->Directories and Conditionals” from the choices in the left column and then set the “Target” box near the top center to “All configurations – All platforms” as shown in the image below. Click on the three dots at the end of the “Conditional defines” list to open up a list of the current defines.

You can now add or remove any of the “Conditional defines” from the list. The definitions of the default defines are as follows:

WIN32: Use the WIN API – use for 32 bit or 64 bit apps
ENAGAL: Enable Galileo
ENACMP: Enable Beidou
NFREQ=3: Max # of frequencies, 3=L1+L2+L5, must be >=2 for u-blox
NEXOBS=3: # of extra observations, used to store multiple codes per freq

Other optional defines are:

ENALEX: Enable QZSS LEX extension
WIN_DLL: Generate Library as Windows DLL

I have not tested any of these optional defines, so no guarantee if any of them work.

Well, I think that’s about it. I’ve probably forgotten something, but this should be enough to get you started.

In my next post, I will cover building the code in Linux.

Review of a new lower cost alternative to the u-blox M8T receiver – the Uputronics M8

A quick Google search will bring up several options for receivers based on the u-blox F9P dual frequency module but finding receivers based on the single frequency M8T receiver is more difficult. I am a regular user of the CSGShop M8T based receivers. Other than some often-counterfeit options available on Ebay or similar sites, CSGShop is the only reliable source I am aware of for M8T based receivers. CSGShop appears to have recently changed their name to GNSS OEM, but I haven’t got used to their new name yet so for this article I am going to continue to refer to them by their original name. I have been very happy with these receivers, have been recommending them for several years, and will continue to recommend them. However, I recently received a single frequency RTK capable receiver from Uputronics based on the u-blox M8 chip for review that is worth considering as an alternative.

The u-blox M8T module is based on the u-blox M8 chip combined with a crystal oscillator, flash, and several other discrete components. The photo below shows a u-blox receiver module with the cover removed. The chip in the middle of the board is the M8. In this case the module is an M8N but should look nearly identical to the M8T.

U-blox receiver module with cover removed

The Uputronics receiver does not use the M8T module but instead builds its own module starting with the M8 chip combined with a TXCO (temperature controlled crystal oscillator) with specs very similar to the M8T and adds the necessary discrete components. It is running similar or identical firmware to the M8T and like the M8T provides raw observations.

The price for a CSGShop receiver sent to the U.S is $79 +$10 shipping. The Uputronics receiver sent to the U.S is $41 +$11 shipping. It also appears to be available in the U.S from AirSpy.US for a couple dollars more but would presumably have faster shipping. CSG shipping times do tend to be quite long, sometimes taking several weeks to the U.S. so for some users this could be a significant advantage.

Here is a photo of the two receivers with the Uputronics receiver on the right.

CSGShop M8T receiver next to Uputronics M8 receiver

So how do they compare? Functionally they should be very similar since they are both based on the M8 chip and firmware. However, there are still some fairly significant differences between them. First of all, as you can see in the photo, the Uputronics is quite a bit larger despite most of the board being empty. This is because it is designed to fit as a hat onto a Raspberry Pi. The large connector in the foreground is designed to connect directly to the GPIO bus on the Pi. The CSGShop receiver in the photo has a USB interface connector but it is also available with a UART connector instead. The Uputronics board has the UART connections through the Pi connector. For someone just getting started with precision GPS, I prefer the USB interface since it can easily be plugged directly into a PC, but with a little work the UART connection can be translated through an FTDI module to USB and then also plugged into the PC. For connecting to a Pi, or Arduino or other embedded application, the UART interface is usually preferable.

After the interface connector, the most noticeable difference between the two receivers is the lack of non-volatile memory on the Uputronics board. Often, the easiest way to configure a u-blox receiver is to connect to it through the u-blox u-center app on the PC, set it up as desired and then save to flash. This is not an option on the Uputronics board so instead the receiver needs to be configured each time it is used. RTKLIB does provide an option to configure the receiver each time it is run so in principle this is only a minor inconvenience. Unfortunately, the receiver defaults to 9600 baud and this normally need to be increased to 115K baud to provide enough bandwidth for the raw observations. This makes it a two step process, first running RTKLIB at 9600 baud to configure the receiver settings and baud rate and then a second time with the baud rate set to the higher rate. It is not difficult to write a python script to take care of this two step process but it does add unnecessary complication for the beginner user. Fortunately Uputronics says they plan to change the default baud rate to 115K in the near future which will simplify this step.

[Update 9/15/20: See Clive’s comment in the comment section below for a link to instructions for permanently changing the baud rate to 115K. This makes things much easier since the rest of the settings can easily be handled with a .cmd file from within RTKLIB.]

Since the Uputronics board is designed to be attached directly to a Raspberry Pi, I chose this configuration for testing. For comparison purposes, I also connected a CSGShop M8T receiver by USB cable to the Pi4 I used for the experiment as shown in the image below.

Raspberry Pi4 with Uputronics M8 hat and USB-connected CSGShop receiver

I used the STR2STR application in RTKLIB to configure the Uputronics receiver and to stream both receiver outputs wirelessly over TCP/IP and a cell phone hot spot to my laptop where I ran two real-time solutions using RTKNAVI. I hope to write another post in the near future describing the details of configuring the Pi but for now the best I can do is refer you to a slightly out-of-date post I previously wrote to describe logging the results to a file on the Pi. I could have also run the real-time solutions on the Pi using RTKRCV but I will leave this to a future post as well.

So, now we have the two receivers up and running, let’s do a performance comparison. For this, I did my typical drive around the neighborhood with each receiver connected to its own antenna mounted on the roof of the car and used a u-blox F9P receiver at my house with a survey grade antenna on the roof as base station.

I had tested an earlier version of this board in a similar experiment and had got nearly identical results with both receivers so had expected the same for this experiment. Surprisingly, the Uputronics board performed significantly worse than the CSGShop board as seen in the image below. The Uputronics solution is on the left.

Run 1: Uputronics on the left, CSGShop on the right

In addition to a much higher percent of float solution points on the Uputronics solution, it also has many more missing samples. At first I suspected a communication problem but when I looked closely at the raw observation files I see that this is not the case. Here’s an example from the raw observation file from one of the missing epochs in the solution. The fourth column is the phase observation and the fifth column is the quality metric value from the receiver. The fifth field in a rinex file is normally an older rarely used SNR field but the demo5 version of RTKLIB reports the quality metric for u-blox receivers instead. Any phase observation with quality metric over 5 is discarded during the RTKLIB raw to rinex conversion which is why the fourth column is mostly blank in this example.

One epoch from the Uputronics rinex observation file

I ran the experiment a couple more times to make sure this was not a fluke and got similar results. The only significant difference between this experiment and the previous one I had done on the earlier version of the board was that in the previous experiment I had not used the Pi, I had connected the UART pins through FTDI directly to my laptop. This made me suspect that the close proximity of the Uputronics board to the Pi was allowing it to pick up some electromagnetic interference (EMI) from the Pi.

There are only four pins on the GPIO interface between the two boards that are necessary for the receiver (+3.3V, Gnd, RX, and TX) so I soldered a few wires to a couple of headers to separate the two boards as shown below.

Run 2: Uputronics board physically separated from Pi

The electrical tape on the back of the Uputronics board was part of an intermediate experiment with the board still attached since I realized that the painted metal heat sink on the Pi CPU was actually touching the back of the Uputronics board. The tape seemed to help a little but did not solve the problem.

The purpose of this experiment was to physically separate the two boards but it also reduced the number of connections between them since the Uputronics board, in addition to the receiver, provides a real time clock that uses some additional pins. I’m not sure if it was because of the increased separation or the reduced connections, but re-running the experiment in this configuration got me back to where I was on the previous board where both receivers performed very similarly. Here’s the results from this experiment, again the Uputronics results are on the left.

Run 2: Uputronics on the left, CSGShop on the right

The CSGShop still performed slightly better (97% fix vs 94% fix) but I think this difference is too small to be meaningful, especially since the two receivers were using similar but not identical antennas.

So, to summarize, I think this receiver is definitely worth a second look. It has a few shortcomings still, but hopefully these will improve over time, and the cost difference is probably significant enough to justify the extra effort for some users. I’m planning to use it myself in an upcoming project and will try to keep readers up to date with my experiences going forward.

RTKLIB: Tips for using a CORS station as base

Generally, you will get the best RTK/PPK solutions using two matched receivers, one as rover, and one as a local base. This will give you the shortest baseline, maximum number of usable constellations, and no issues with the Glonass hardware biases that come from using unmatched receivers. To do this, you will first need to determine the precise location of your local base receiver. Running a post-processed kinematic (PPK) solution with your own local base receiver as rover and a nearby Continuous Operating Reference Station (CORS) as base is usually the easiest way to do this.

Of course, If you have a CORS station near enough, it is also possible to run solutions directly from your rover to the CORS base but I generally opt for the local base since the solutions will be more accurate and more robust.

For the most part, running a PPK solution against a CORS base is very similar to the local base solutions I describe in many of my other posts but there are a few important differences that I will try to go over in this post. I will describe using CORS stations inside the U.S. with u-blox receivers and the demo5 RTKLIB code since this is what I am most familiar with, but most of the information in this post will apply to other receivers as well as to CORS stations in other countries.

The first step is to find a nearby CORS station. In the U.S, the easiest way to do this is to go to the NOAA User Friendly CORS website and click on the CORS Map link. Zoom into your local area and if you are lucky you should see something like the screen shot below with several nearby choices. If you’re not so lucky you may find only one or two distant choices.

Notice that some stations are GPS only and some are labelled as “GNSS” which indicates that they support multiple constellations, usually GPS and Glonass, but sometimes Galileo as well. Ideally you will find a “GNSS” site within 20 kilometers but if not you may be successful generating fixed solutions with stations up to 100 km or more away. If you are using a dual frequency receiver and solving for a stationary rover you can usually use more distant stations. Be aware that longer baselines will increase accuracy errors so it is a good idea to run longer solutions and average the results from multiple stations if possible. Station sample rate is less important than distance and number of constellations but a higher sample rate is always better if you have the choice.

In addition to the NOAA CORS website, it is also worth checking if your state has its own network of reference stations. These are usually run by the state department of transportation and their data is normally free although you may need to sign up to access it. There is a slightly dated state by state list available here.

If your base station is sampling slower than a 1 second rate then I often find that interpolating between base observation samples improves the solution. In RTKPOST, this is done by setting enabling “Time Interpolation of Base Station Data” in the “Misc” tab of the options menu. In RNX2RTKP, this is done by setting “misc-timeinterp =on” in the config file.

Assuming your rover is stationary, you can run the solution mode as “Static” and this will improve your chance of getting a fixed solution. I prefer to run a “Kinematic” solution if possible, however, since the variation in solution position over time gives some insight into what the accuracy of the solution is, especially if you have collected several hours of observation data.

Since the receiver manufacturers will almost always be different between base and rover in these solutions, you will usually need to deal with the Glonass hardware biases. I describe the different options to account for these in detail in this post so I would recommend reading or reviewing that post for the details. To quickly summarize though, the simplest option is to set “Integer Ambiguity Resolution” in the options menu for Glonass to “Fix-and-Hold” in which case RTKLIB attempts to calibrate the biases. In some cases, this will be good enough to get a fixed solution. However, you are more likely to get a fixed solution if you account for the biases directly. You can do this by setting Glonass ambiguity resolution to the poorly named “Autocal” option and then specifying the biases directly with the “GLO HW bias” option in RTKPOST or “pos2-arthres2” option in the config file. Again, see the above post for the details on how to determine the correct biases and the table of biases by receiver manufacturer. Note that these ambiguity resolution options are only available in the demo5 version of the RTKLIB code.

If the HW biases for both base and rover are close to zero then it’s generally OK to ignore them and set Glonass ambiguity resolution to “on”. This will not be true for a u-blox M8T rover since it’s bias is not zero. However the u-blox F9P bias is close to zero so if you are using it as rover and your base receiver is manufactured by Trimble, Septentrio, Topcon, Spectra Physics, Javad, or Ashtec then go ahead and set Glonass ambiguity resolution to “on”. The receiver manufacturer will be listed in the header of the base rinex file.

The base rinex file header also includes the base antenna type and it’s precise location. Note that the precise location is in the header field labelled “APPROX POSITION XYZ”. Setting the base station position in the options menu to “RINEX Header Position” will cause this value to be used in the solution. If you would also like to include the base station antenna calibration in the solution, then check the “Antenna Type” box and specify “*” in the box below. This tells RTKLIB to use the antenna type listed in the rinex header. You will also need to point to an antenna calibration file in the “Files” tab of the options menu. You can use the “igs14.atx” file that is included with the demo5 binaries for this.

It is important to keep in mind that the accuracy of the solution is going to get worse as the distance to the base station increases as shown in this chart taken from a Novatel tutorial.

If you are using a single frequency receiver then there are usually no good alternatives to using a distant CORS station. The best you can do is collect multiple longer data sets from multiple stations and average the results. If you are using a dual frequency receiver then you also have the option of running a Precise Point Positioning (PPP) solution, either with RTKLIB or through an online service as I describe in this post.

Well, that’s all I can think of at the moment. If anyone else has any additional tips or questions, please add them to the comments below.