Skip to content



Create a GNSS Base Station

A simple base station can streams GNSS correction information to an Networked Transport of RTCM via Internet Protocol (NTRIP) caster. Its data can be used for Real Time Kinematic (RTK) or Post Processed Kinematic (PPK), which helps to get cm-level accuracy.

Last update: 2022-06-04


Install an Operating System#

A quick method using the official Raspberry Pi tool:

  1. Download, install and run Raspberry Pi Imager.

  2. Select Operating System: Raspberry Pi OS (other)Raspberry Pi OS Lite.

  3. Press ctrl-shift-x to show Advanced Options:

    • Set Hostname, e.g. raspberrypi.local
    • Enable SSH after setting up an account, e.g. user: pi / cccc
  4. Select the target microSD Card and write the image.

  5. Boot the Raspberry Pi board after inserting the microSD Card.

  6. Use an SSH client to connect to raspberrypi.local with username pi and password cccc.

Some tweaks can be applied after login:

  • To use some list commands, run:

    nano ~/.bashrc
    

    and enable alias for ls commands.

  • Set Wifi Settings

    Run sudo raspi-config, select System Options, then select Wireless LAN, set the Country Code to US.

    Set priority for Wifi by adding below config at the end of the /etc/dhcpcd.conf file:

    sudo nano /etc/dhcpcd.conf
    
    /etc/dhcpcd.conf
    interface wlan0
    metric 100
    
  • Update repos:

    sudo apt update
    
  • Install build tools:

    sudo apt install git cmake
    

Create Wifi Access Point#

Follow the guide Setting up a Routed Wireless Access Point:

Install packages:

sudo DEBIAN_FRONTEND=noninteractive apt install -y \
    hostapd \
    dnsmasq \
    netfilter-persistent \
    iptables-persistent

Set the static IP for the gateway by going to the end of the /etc/dhcpcd.conf file and add the following:

sudo nano /etc/dhcpcd.conf
/etc/dhcpcd.conf
interface wlan0
    static ip_address=192.168.4.1/24
    nohook wpa_supplicant

Configure the DHCP and DNS services:

sudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.orig
sudo nano /etc/dnsmasq.conf

Add the following to the file and save it:

/etc/dnsmasq.conf
# Listening interface
interface=wlan0 

# Pool of IP addresses served via DHCP
dhcp-range=192.168.4.2,192.168.4.20,255.255.255.0,24h

# Local wireless DNS domain
domain=wlan     

# Alias for this router
address=/gw.wlan/192.168.4.1

Unblock Wifi:

sudo rfkill unblock wlan

Create the hostapd configuration file:

sudo nano /etc/hostapd/hostapd.conf

Add configs as below, note the SSID, and PassPhrase:

/etc/hostapd/hostapd.conf
country_code=US
interface=wlan0
ssid=RPI_BASE
hw_mode=g
channel=7
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=TestTestTest
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

Start the hostapd service:

sudo systemctl unmask hostapd
sudo systemctl enable hostapd

Reboot if needed and recheck the wlan0 interface.

iw wlan0 info
Interface wlan0
        ifindex 3
        wdev 0x1
        addr b8:27:eb:a9:f5:fc
        ssid RPI_BASE
        type AP
        wiphy 0
        channel 7 (2442 MHz), width: 20 MHz, center1: 2442 MHz
        txpower 31.00 dBm

To see devices connected to the Pi Access point:

iw dev wlan0 station dump

Switching between Access Point and Client mode

  • Disable Access Point:

    sudo systemctl disable hostapd dnsmasq
    

    Comment the static ip config in /etc/dhcpcd.conf:

    sudo nano /etc/dhcpcd.conf
    
    /etc/dhcpcd.conf
    #interface wlan0
    #    static ip_address=192.168.4.1/24
    #    nohook wpa_supplicant
    

    Restart:

    sudo reboot
    
  • Enable Access Point:

    sudo systemctl enable hostapd dnsmasq
    

    Uncomment the static IP config in /etc/dhcpcd.conf:

    sudo nano /etc/dhcpcd.conf
    
    /etc/dhcpcd.conf
    interface wlan0
        static ip_address=192.168.4.1/24
        nohook wpa_supplicant
    

    Restart:

    sudo reboot
    

Peripheral Configuration#

Need to enable SPI, I2C, and UART.

Run sudo raspi-config and select Interface Options:

  1. Select SPI, and choose Yes to enable SPI

  2. Select I2C, and choose Yes to enable I2C

  3. Select Serial, and choose No to disable shell, then in next screen, choose Yes to enable Hardware Serial port

  4. Run:

    groups
    

    to check if the current user is added into groups gpio,spi, i2c and dialout.

    If not, run:

    sudo usermod -a -G gpio,spi,i2c,dialout $USER
    

    to add the current user to necessary groups.

  5. Reboot and list all enabled interfaces by run:

    ll /dev/i2c*
    ll /dev/spi*
    ll /dev/serial*
    

    for example:

    crw-rw---- 1 root i2c 89, 1 Dec 19 17:10 /dev/i2c-1
    crw-rw---- 1 root i2c 89, 2 Dec 19 17:10 /dev/i2c-2
    crw-rw---- 1 root spi 153, 0 Dec 19 17:10 /dev/spidev0.0
    crw-rw---- 1 root spi 153, 1 Dec 19 17:10 /dev/spidev0.1
    lrwxrwxrwx 1 root root 5 Dec 19 17:10 /dev/serial0 -> ttyS0
    lrwxrwxrwx 1 root root 7 Dec 19 17:10 /dev/serial1 -> ttyAMA0
    

Check Serial with GNSS module

Install COM app:

sudo apt install -y picocom

Then try to talk to the GNSS module connected to the mini UART1:

picocom /dev/ttyS0 -b 115200

To send versiona\r\n, type: versiona, enter, ctrl-j.
The GNSS module should reply:

$command,versiona,response: OK*45
#VERSIONA,98,GPS,FINE,2189,51932000,0,0,18,722;"UB4B0M","R3.00Build21213","B123G125R12E15a5bS1Z125-HRBMDFS0011N1-S20-P20-A3L:2120/Jan/6","2330304000024-HV4001210403092","2101327772076","2020/Mar/19"*bb111567

To enable echo: ctrl-a, ctr-c.
To exit: ctrl-a, ctrl-x.

If the supplying power is not sufficient, COM port on GNSS module will not work. Check the dropping voltage on the power input.

Check SPI with nRF24

Download RF24 library:

git clone https://github.com/vuquangtrong/RF24 && \
cd RF24 && \
./configure --driver=SPIDEV && \
make && \
sudo make install && \
cd ..

Make new file:

nano rf24_tx.cpp
rf24_tx.cpp
#include <iostream>    // cin, cout, endl
#include <RF24/RF24.h>

// create RF24 instance
RF24 radio(24 /* CE = sys_gpio_24 */,
            0 /* CSN = 0 means spidev0.0 */
            /* default speed is 10 Mbps */);

// max payload of RF24 is 32 bytes
uint8_t payload[32];

int main(int argc, char** argv) {
    // perform hardware check
    if (!radio.begin()) {
        std::cout << "radio hardware is not responding!!" << std::endl;
        return 0; // quit now
    }

    radio.setPayloadSize(32);
    radio.setChannel(100); // 2400 + 100 = 2500 MHz, out of WiFi band

    // address, defaut length is 5
    uint8_t tx_address[6] = "1Addr"; // write to
    radio.openWritingPipe(tx_address); // always uses pipe 0

    // For debugging info
    radio.printDetails();       // (smaller) function that prints raw register values
    radio.printPrettyDetails(); // (larger) function that prints human readable data

    // Start
    std::cout << "Start TX" << std::endl;
    radio.stopListening(); // put radio in TX mode

    while(true) {
        radio.write(&payload, 32); // transmit
    }
}

Compile:

g++ -Ofast -Wall -pthread  rf24_tx.cpp -lrf24 -o rf24_tx

Run and check the log:

================ SPI Configuration ================
CSN Pin         = /dev/spidev0.0
CE Pin          = Custom GPIO24
SPI Speedz      = 10 Mhz
================ NRF Configuration ================
STATUS          = 0x0e RX_DR=0 TX_DS=0 MAX_RT=0 RX_P_NO=7 TX_FULL=0
RX_ADDR_P0-1    = 0x7264644131 0x65646f4e31
RX_ADDR_P2-5    = 0xc3 0xc4 0xc5 0xc6
TX_ADDR         = 0x7264644131
RX_PW_P0-6      = 0x20 0x20 0x20 0x20 0x20 0x20
EN_AA           = 0x3f
EN_RXADDR       = 0x03
RF_CH           = 0x64
RF_SETUP        = 0x03
CONFIG          = 0x0e
DYNPD/FEATURE   = 0x00 0x00
Data Rate       = 1 MBPS
Model           = nRF24L01+
CRC Length      = 16 bits
PA Power        = PA_LOW
ARC             = 0
================ SPI Configuration ================
CSN Pin                 = /dev/spidev0.0
CE Pin                  = Custom GPIO24
SPI Frequency           = 10 Mhz
================ NRF Configuration ================
Channel                 = 100 (~ 2500 MHz)
RF Data Rate            = 1 MBPS
RF Power Amplifier      = PA_LOW
RF Low Noise Amplifier  = Enabled
CRC Length              = 16 bits
Address Length          = 5 bytes
Static Payload Length   = 32 bytes
Auto Retry Delay        = 1500 microseconds
Auto Retry Attempts     = 15 maximum
Packets lost on
    current channel     = 0
Retry attempts made for
    last transmission   = 0
Multicast               = Disabled
Custom ACK Payload      = Disabled
Dynamic Payloads        = Disabled
Auto Acknowledgment     = Enabled
Primary Mode            = TX
TX address              = 0x7264644131
pipe 0 ( open ) bound   = 0x7264644131
pipe 1 ( open ) bound   = 0x65646f4e31
pipe 2 (closed) bound   = 0xc3
pipe 3 (closed) bound   = 0xc4
pipe 4 (closed) bound   = 0xc5
pipe 5 (closed) bound   = 0xc6
Start TX
Check I2C with OLED

Install i2c dev tools:

sudo apt install i2c-tools

Detect devices on I2C bus 1 /dev/i2c-1:

sudo i2cdetect -y 1

Check if 0x3c is shown in the scanned address when an OLED 0.91in is connected.

Download SSD1306 Library:

git clone https://github.com/vuquangtrong/OLED_SSD1306_I2C_Linux.git && \
cd OLED_SSD1306_I2C_Linux && \
make && \
sudo make install && \
cd ..

Write a small program:

nano oled_progres_bar.c
oled_progres_bar.c
#include <string.h>
#include <unistd.h>
#include <SSD1306/ssd1306.h>

int main() {
    char counter = 0;
    char buffer[4];
    SSD1306_Init("/dev/i2c-1");
    while(1) {
        sprintf(buffer, "%d", counter++);
        SSD1306_Clear();
        SSD1306_WriteString(0,0, "counter:", &Font_7x10, SSD1306_WHITE, SSD1306_OVERRIDE);
        SSD1306_WriteString(0,10, buffer, &Font_11x18, SSD1306_WHITE, SSD1306_OVERRIDE);
        SSD1306_DrawRectangle(0,28,128,4,SSD1306_WHITE);
        SSD1306_DrawFilledRectangle(0,28,counter*128/256,4,SSD1306_WHITE);
        SSD1306_Screen_Update();
        sleep(0.2);
    }
    return 0;
}

Compile:

gcc oled_progress_bar.c -lssd1306 -o oled_progress_bar

Run and see the screen updated.

Compile RTKLib#

Download source code of the RTKLib 2.4.3 (beta):

git clone https://github.com/tomojitakasu/RTKLIB.git -b rtklib_2.4.3

Build str2str app:

cd RTKLIB/app/consapp/str2str/gcc && \
make
Build all

Build dependent libs if using rnx2rtkp:

sudo apt install gfortran && \
cd RTKLIB/lib/iers/gcc && \
make

Build all apps:

cd RTKLIB/app/consapp && \
make

Compile NTRIP Caster#

Install build tool:

sudo apt install cmake
sudo apt install libev-dev

Download source code:

git clone https://github.com/tisyang/ntripcaster.git && \
cd ntripcaster && \
git submodule update --init

Build app:

mkdir build  && \
cd build  && \
cmake ..  && \
make

Local test#

Copy ntripcaster and RTKLib str2str to a new folder.

Create a new ntripcaster.json to set up the caster, see the parameters as below:

Parameters

  • max_client and max_source is the number of connected agents,
    value of 0 means unlimitted.

  • tokens_client sets policy for clients in the format: "username:password": "mountpoint",
    value of * means any mountpoint.

  • tokens_source sets policy for sources in the format: "password": "mountpoint",
    value of * means any mountpoint.

{
        "listen_addr":"0.0.0.0",
        "listen_port": 2101,
        "max_client": 0,
        "max_source": 0,
        "max_pending": 10,
        "tokens_client": {
                "test:test": "*"
        },
        "tokens_source": {
                "test": "*"
        }
}

Run the NTRIP Caster:

./ntripcaster

Configure GNSS module via shell:

stty -F /dev/ttyS0 115200
mode base time 60 1.0 2.0

Check the position:

echo "gngga 1" >> /dev/ttyS0

When the type of processed position is 7, meaning base is fixed, then we can get RTCM messages:

gnss.cmd
unlog
rtcm1006 10
rtcm1033 10
rtcm1074 1 
rtcm1124 1 
rtcm1084 1 
rtcm1094 1 

Stream RTCM messages to a local NTRIP Caster at localhost using username test at the mount point UB4B0M:

./str2str \
    -in serial://ttyS0:115200 \
    -out ntrips://:test@localhost:2101/UB4B0M \
    -c gnss.cmd

Stream RTCM messages to a remote NTRIP Caster at 103.166.182.209 using username oegalaxy at the mount point UB4B0M:

./str2str \
    -in serial://ttyS0:115200 \
    -out ntrips://:oegalaxy@103.166.182.209:2101/UB4B0M \
    -c gnss.cmd

A sample script to configure GNSS module and run NTRIP streamer:

#!/bin/bash

# converter
function ddmm2dec() {
    d=$(bc <<< "$1/100")
    m=$(bc <<< "$1-$d*100")
    m=$(bc <<< "scale=6;$m/60")
    echo $d$m
}

# set up COM port
stty -F /dev/ttyS0 115200

# request base mode
echo "unlog" >> /dev/ttyS0
echo "mode base time 60 1.0 2.0" >> /dev/ttyS0
echo "gngga 1" >> /dev/ttyS0

# check the log
lat=''
lon=''
alt=''

while read -r line < /dev/ttyS0; do
    echo $line
    fix=$(echo $line | awk -F',' '{print $7}')

    # gps position mode is fixed, then exit the loop
    if [[ $fix == '7' ]]; then
        lat=$(echo $line | awk -F',' '{print $3}')
        lat=$(ddmm2dec $lat)

        lon=$(echo $line | awk -F',' '{print $5}')
        lon=$(ddmm2dec $lon)

        alt=$(echo $line | awk -F',' '{print $10}')
        break
    fi
done

echo $lat $lon $alt

# clear output
echo "unlog" >> /dev/ttyS0

# request streamer
# from ttyS0, to localhost:2101 using test account at the test mountpoint
./str2str \
    -in serial://ttyS0:115200 \
    -out ntrips://:test@localhost:2101/test \
    -c gnss.cmd

Create System Services#

NTRIP Service#

  • Run at startup, listen to Local RTK messages and broadcast to clients

ntripcaster.service
[Unit]
Description=NTRIP Server
After=multi-user.target

[Service]
Type=simple
User=pi
Group=pi
ExecStart=/home/pi/base/ntripcaster /home/pi/base/ntripcaster.json
Restart=on-abort

[Install]
WantedBy=multi-user.target
ntripcaster.json
{
        "listen_addr":"0.0.0.0",
        "listen_port": 2101,
        "max_client": 0,
        "max_source": 0,
        "max_pending": 10,
        "tokens_client": {
                "test:test": "*"
        },
        "tokens_source": {
                "test": "*"
        }
}

Install the service:

sudo cp ntripcaster.service /usr/lib/systemd
sudo systemctl enable ntripcaster.service

Button Service#

  • Run at startup, show a welcome message
  • Handle the User button: hold more than 3 seconds to restart Local RTK service

button.py
#!/usr/bin/python

import RPi.GPIO as GPIO
import time, subprocess, signal, os

# first message
subprocess.Popen('/home/pi/base/start')

# use BCM mode, see low level pin number
GPIO.setmode(GPIO.BCM)

# BCM 24  = BOARD 18
# Pull down to make it GND by default
GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

count = 0

try:

    while True:
        time.sleep(1)
        button = GPIO.input(24)
        print("Button: ", button)

        if button == 1:
            count += 1
            if count == 3:
                p =  subprocess.Popen(['ps', '-A'], stdout=subprocess.PIPE)
                out, err = p.communicate()
                for line in out.splitlines():
                    #print(line)
                    if (b'local_rtk' in line) or (b'str2str' in line):
                        pid = int(line.split(None, 1)[0])
                        os.kill(pid, signal.SIGKILL)
                # run new
                time.sleep(1)
                subprocess.Popen('/home/pi/base/local_rtk')
        else:
            count = 0

except KeyboardInterrupt:
    print("Exit")

GPIO.cleanup()
button.service
[Unit]
Description=Button
After=multi-user.target

[Service]
Type=simple
User=pi
Group=pi
ExecStart=/usr/bin/python /home/pi/base/button.py
Restart=on-abort

[Install]
WantedBy=multi-user.target

Install the service:

sudo cp ntripcaster.service /usr/lib/systemd
sudo systemctl enable ntripcaster.service

Local RTK application#

  • Called by Button service when user presses and holds more 3 seconds
  • Handle the sequence to control GNSS module via serial
  • Run local streaming server

#include <iostream>
#include <string>
#include <iomanip>
#include <vector>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <serial/SerialPort.h>
#include "oled.h"
#include "fonts.h"

using namespace std;

SerialPort gnss;
I2C i2c1(1);
Oled lcd(&i2c1);

double lat;
double lon;
double alt;
int fix_mode;
int timeout;
char msg[32] = {0};
char buffer[1024] = {0};

void handle_Ctrl_C (int s) {
    try {
        cout << "Terminating..." << endl;
        gnss.writeString("unlog\r\n");
        gnss.closeDevice();
    } catch (...) {
       cout << "error while closing..." << endl;
    }

    exit(0);
}


vector<string> stringSplit(const string &s, const char delimiter)
{
    vector<string> tokens;
    string token;
    istringstream tokenStream(s);
    while (getline(tokenStream, token, delimiter))
    {
        tokens.push_back(token);
    }
    return tokens;
}

template <class Type>
Type stringToNum(const string &str)
{
    istringstream iss(str);
    Type num;
    iss >> num;
    return num;
}

double convertNmeaToDouble(const std::string &val, const std::string &dir) {
    int dot = val.find('.');
    std::string degree = val.substr(0, dot-2);
    std::string minute = val.substr(dot-2);

    double ret = stringToNum<double>(degree) + stringToNum<double>(minute)/60;

    if (dir == "S" || dir == "W") {
        ret = -ret;
    }

    return ret;
}

void Oled_msg(char *msg) {
    lcd.clear();
    lcd.text(0, 26, msg, Oled::DOUBLE_SIZE);
    lcd.display();
}

void Oled_pos(double &lat, double &lon, double &alt, int &fix_mode) {
    lcd.clear();

    snprintf(msg, 32, "%10.6f", lat);
    lcd.text(8, 4, msg /*, Oled::DOUBLE_SIZE*/);

    snprintf(msg, 32, "%10.6f", lon);
    lcd.text(8, 4+12, msg /*, Oled::DOUBLE_SIZE*/);

    snprintf(msg, 32, "%10.6f", alt);
    lcd.text(8, 4+24, msg /*, Oled::DOUBLE_SIZE */);

    switch(fix_mode) {
        case 0:
            snprintf(msg, 32, "%s", "INVALID");
            break;
        case 1:
            snprintf(msg, 32, "%s", "SINGLE ");
            break;
        case 2:
            snprintf(msg, 32, "%s", "DIFFPOS");
            break;
        case 4:
            snprintf(msg, 32, "%s", "RTK-FIX");
            break;
        case 5:
            snprintf(msg, 32, "%s", "RTK-FLT");
            break;
        case 6:
            snprintf(msg, 32, "%s", "INSPOS ");
            break;
        case 7:
            snprintf(msg, 32, "%s", "BASEFIX");
            break;
        default:
            snprintf(msg, 32, "%s", "-------");
            break;
    }
    lcd.text(4, 8+36, msg, Oled::DOUBLE_SIZE);

    if (fix_mode != 7) {
        snprintf(msg, 32, "%3d", timeout);
        lcd.text(8*12, 8+36, msg /*, Oled::DOUBLE_SIZE */);
    }

    lcd.display();
}

int main (/*int argc, char *argv[]*/) {

    // register handler
    struct sigaction sigHandler;
    sigHandler.sa_handler = handle_Ctrl_C;
    sigemptyset(&sigHandler.sa_mask);
    sigHandler.sa_flags = 0;
    sigaction(SIGINT, &sigHandler, NULL);

    // start OLED
    lcd.init();
    lcd.text(0, 26, "Initializing...");
    lcd.display();

    // talk to GNSS module
    if (gnss.openDevice("/dev/ttyS0", 115200) != 1) {
        snprintf(msg, 32, "%s", "ERROR!");
        Oled_msg(msg);
        return -1;
    }

    snprintf(msg, 32, "%s", "GNSS OK!");
    Oled_msg(msg);

RESTART:

    timeout = 120;

    // request base mode
    gnss.writeString("unlog\r\n");
    gnss.writeString("mode base time 60\r\n");
    gnss.writeString("gngga 1\r\n");
    gnss.flushReceiver();

    while(1) {
       int n = gnss.readString(buffer, '\n', 1024);
       if (n > 0) {
            string line = string(buffer, n);
            cout << line;

            // $GNGGA,090031.00,2057.59811809,N,10546.17292292,E,1,18,2.2,16.4378,M,-28.2478,M,,*64
            vector<string> message = stringSplit(line, ',');
            if (message[0] == "$GNGGA" && message[2] != "") {
                lat = convertNmeaToDouble(message[2], message[3]);
                lon = convertNmeaToDouble(message[4], message[5]);
                alt = stringToNum<double>(message[9]);
                fix_mode = stringToNum<int>(message[6]);

                Oled_pos(lat, lon, alt, fix_mode);

                if (fix_mode == 7) {
                    cout << "Base fixed at " << lat << ", " << lon << ", " << alt << endl;
                    break;
                }
            } else {
                snprintf(msg, 32, "WAIT %d", timeout);
                Oled_msg(msg);
            }

            timeout--;
            if (timeout == 0) {
                goto RESTART;
            }
       }
    }

    gnss.writeString("unlog\r\n");

    // Stream RTCM3 to local ntripcaster
    // password = test
    // mountpoint = test

    char cmd[1024] =
    "/home/pi/base/str2str "
        "-in serial://ttyS0:115200 "
        "-out ntrips://:test@localhost:2101/test "
        "-c /home/pi/base/gnss.cmd ";

    cout << "Run:" << endl;
    cout << cmd << endl;
    system(cmd);

    // Close the serial device
    cout << "Closing..." << endl;
    gnss.writeString("unlog\r\n");
    gnss.closeDevice();

    return 0;
}
unlog
rtcm1006 com1 10
rtcm1033 com1 10
rtcm1074 com1 1 
rtcm1124 com1 1 
rtcm1084 com1 1 
rtcm1094 com1 1 

References#

https://www.petig.eu/rtk/

https://github.com/eringerli/RpiNtripBase

https://github.com/tisyang/ntripcaster

https://github.com/vbulat2003/ntripcaster2

http://www.hiddenvision.co.uk/ez/

Comments