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
Table of Content
Install an Operating System#
A quick method using the official Raspberry Pi tool:
-
Download, install and run Raspberry Pi Imager.
-
Select Operating System: Raspberry Pi OS (other) → Raspberry Pi OS Lite.
-
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
- Set Hostname, e.g.
-
Select the target microSD Card and write the image.
-
Boot the Raspberry Pi board after inserting the microSD Card.
-
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 toUS
.Set priority for Wifi by adding below config at the end of the
/etc/dhcpcd.conf
file:sudo nano /etc/dhcpcd.conf
/etc/dhcpcd.confinterface 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
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:
# 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:
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.confinterface 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:
-
Select SPI, and choose
Yes
to enable SPI -
Select I2C, and choose
Yes
to enable I2C -
Select Serial, and choose
No
to disable shell, then in next screen, chooseYes
to enable Hardware Serial port -
Run:
groups
to check if the current user is added into groups
gpio
,spi
,i2c
anddialout
.If not, run:
sudo usermod -a -G gpio,spi,i2c,dialout $USER
to add the current user to necessary groups.
-
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
#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
#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
andmax_source
is the number of connected agents,
value of0
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:
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
[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
{
"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
#!/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()
[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://github.com/eringerli/RpiNtripBase
https://github.com/tisyang/ntripcaster
https://github.com/vbulat2003/ntripcaster2
http://www.hiddenvision.co.uk/ez/