
The Goouuu Tech GT-U16 is a high-precision GNSS module that supports dual-frequency L1 + L5 bands, enabling fast and accurate positioning with support for multiple satellite constellations including GPS, GLONASS, Galileo, BeiDou, and QZSS. Its compatibility with the NMEA 0183 protocol enables seamless integration with a wide range of microcontrollers, making it a powerful option for precision navigation and GPS projects.
In this article, we will walk you step-by-step through the process of working with the GT-U16 RTK module. We’ll begin by covering the hardware setup and interfacing the receiver with an Arduino. Then, we’ll look at parsing raw NMEA data into human-readable formats so you can easily extract key information like position and signal strength. Also at Circuit Digest, we’ve created many other Arduino projects and tutorials as well, all freely available, so be sure to check them out if you’re interested in learning more.
We will also explore important RTK and correction concepts, helping you understand the technology that underpins this receiver’s precision. From there, we’ll move on to graphical data visualization using iNavTool R5.0.1 and dive into signal quality analysis, including interpreting SNR and different satellite constellations.
Finally, we’ll wrap up with practical use-cases for the GT-U16 RTK module and discuss future enhancements and potential directions for further development. By the end of this article, you’ll have a clear understanding of the GT-U16’s capabilities and how to integrate this powerful module into your own projects.
Hardware Requirements
Before diving into the code, let’s list all the hardware you need:
NOTE : You can choose either a Ceramic Patch Antenna with U.FL connector or an Active Multiband GNSS Antenna, depending on your project needs and availability. Both options are compatible for interfacing and parsing GNSS data using the Goouuu Tech GT-U16 RTK L1+L5 module.
GT-U16-Connection with Active Multiband GNSS Antenna

GT U16 Connection with Ceramic Patch Antenna with UFL

Understanding the GT-U16 Interface
The module outputs GNSS data over UART using the NMEA 0183 protocol with Default baud rate, which is typically 115200
NMEA sentence format
Here’s a concise table listing the common NMEA talker IDs for each satellite constellation.
For example, the sentence for Global Positioning System Fixed Data for GPS should be "$GPGGA".
Pinout of GT-U16
GND - Power supply (3.3 V - 5V).
VCC - Positive Voltage Input Pin.
TXD - Transmit data from the GT-U16 to the host (e.g. Arduino RX).
RXD - Receive data into the GT-U16 from the host (e.g. Arduino TX).
PPS - Pulse-per-second output for precise timing.

VCC - 3.3V.
GND - GND.
TXD - D0(Arduino RX).
RXD - D1(Arduino TX).
Warning: When programming the arduino uno remove connection from D0&D1 pins so you wouldn't get any upload error.

Code Explanation – Raw GNSS Data Passthrough
Our first task is to read the raw NMEA data from the GT-U16 and output it to the Serial Monitor:
Preprocessor Definitions
#define GPSSerial Serial
#define DEBUGSerial Serial
In this section, we define two aliases for the same hardware Serial interface. GPSSerial will represent the serial port connected to the GNSS receiver (e.g. GT-U16), and DEBUGSerial will represent the serial port we use to print debug information to the computer. Even though both names point to the same Serial object, this makes the code clearer and more flexible allowing easy changes later if we use different serial ports for GPS and debugging.
Setup Function
void setup()
{
GPSSerial.begin(115200);
DEBUGSerial.begin(115200);
DEBUGSerial.println("Waiting for GNSS data...");
}
In the setup() function, we initialize both GPSSerial and DEBUGSerial at a baud rate of 115200. This matches the default communication rate of most GNSS receivers like the GT-U16. After initializing the serial port for debugging, we print a startup message "Waiting for GNSS data..." so the user knows the device is running and awaiting GPS data.
Loop Function
void loop()
{
while (GPSSerial.available()) {
DEBUGSerial.write(GPSSerial.read());
}
}
In the loop() function, the code continuously checks whether any data is available from the GPS receiver by calling GPSSerial.available(). If there’s data present, it’s read one byte at a time with GPSSerial.read() and immediately sent to DEBUGSerial.write() for output to the PC or serial monitor. This effectively forwards the raw NMEA data stream from the GPS to your computer for observation or further processing.
Output Example

Parsing NMEA Data
Our next step was to convert raw NMEA sentences into understandable data: time, location, altitude, speed, satellite count, and signal strength. Let's go through them all.
Definitions and Global Variables
#define GPSSerial Serial
#define DEBUGSerial Serial
const unsigned long PRINT_INTERVAL = 1000;
// GNSS Data
String utcTime = "--:--:--";
String localTime = "--:--:--";
String latitude = "--";
String longitude = "--";
String altitude = "--";
String fixQuality = "--";
String speedKmh = "--";
// Satellite Signal Info
int gpsCount = 0, beidouCount = 0, galileoCount = 0, qzssCount = 0, glonassCount = 0;
String snrList = "";
String nmeaBuffer = "";
At the top, the GPSSerial and DEBUGSerial macros are defined as Serial, allowing the code to use a single hardware serial interface for both GPS data and debug output. Next, several global variables are declared to store GNSS data such as UTC and local time, latitude, longitude, altitude, fixed quality, and speed. Variables for satellite counts (gpsCount, beidouCount, galileoCount, qzssCount, glonassCount) as well as a buffer (nmeaBuffer) and an snrList string for signal strength are also set up.
setup() Initialization
void setup() {
GPSSerial.begin(115200);
DEBUGSerial.begin(115200);
DEBUGSerial.println("Waiting for GNSS data...");
}
In the setup() function, both GPSSerial and DEBUGSerial are initialized at 115200 baud rate. A startup message — "Waiting for GNSS data..." — is printed so the user knows the device is ready and waiting for data.
loop() Core Logic
void loop() {
readGNSSData();
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= PRINT_INTERVAL) {
lastPrint = millis();
printGNSSInfo();
resetCounters();
}
}
In the main loop() function, readGNSSData() is called continuously to process incoming NMEA sentences. A lastPrint timestamp is also maintained so that printGNSSInfo() is invoked every PRINT_INTERVAL (1 second). Once printing is done, resetCounters() clears satellite counts and SNR list so that fresh data is gathered for the next cycle.
readGNSSData() and NMEA Processing
void readGNSSData() {
while (GPSSerial.available()) {
char c = GPSSerial.read();
if (c == '\n') {
processNMEALine(nmeaBuffer);
nmeaBuffer = "";
} else if (c != '\r') {
nmeaBuffer += c;
}
}
}
The readGNSSData() function reads data one character at a time from GPSSerial. Complete NMEA sentences are assembled into nmeaBuffer. Once a newline (\n) is detected, processNMEALine() is invoked to handle the full sentence, then the buffer is cleared for the next one.
processNMEALine() Dispatching
void processNMEALine(const String &line) {
if (!line.startsWith("$")) return;
if (line.startsWith("$GPGSV") || line.startsWith("$GBGSV") ||
line.startsWith("$GAGSV") || line.startsWith("$GQGSV") ||
line.startsWith("$GLGSV")) {
parseGSVLine(line);
} else if (line.startsWith("$GNRMC")) {
parseRMC(line);
} else if (line.startsWith("$GNGGA")) {
parseGGA(line);
}
}
This function first validates the sentence format, then routes each NMEA message to its appropriate parser. $GPGSV, $GBGSV, $GAGSV, $GQGSV, and $GLGSV are passed to parseGSVLine() for satellite information. $GNRMC and $GNGGA are routed to parseRMC() and parseGGA() to extract time, position, and altitude.
parseRMC() and Time Conversion
void parseRMC(const String &line) {
int commas[13], idx = -1;
for (int i = 0; i < 13; i++) {
idx = line.indexOf(',', idx + 1);
if (idx == -1) break;
commas[i] = idx;
}
if (commas[1] - commas[0] > 1) {
String rawTime = line.substring(commas[0] + 1, commas[1]);
int hour = rawTime.substring(0, 2).toInt();
int minute = rawTime.substring(2, 4).toInt();
int second = rawTime.substring(4, 6).toInt();
utcTime = formatTime(hour, minute, second);
// Local time adjustment (+5:30)
minute += 30;
if (minute >= 60) {
minute -= 60;
hour += 1;
}
hour = (hour + 5) % 24;
localTime = formatTime(hour, minute, second);
}
if (commas[7] > commas[6]) {
float knots = line.substring(commas[6] + 1, commas[7]).toFloat();
speedKmh = String(knots * 1.852, 1);
}
}
parseRMC() parses the UTC time from the NMEA $GNRMC sentence and adjusts it for local time (+5:30 offset). It also extracts the speed in knots and converts it to km/h for user-friendly output.
parseGGA() and Position Extraction
void parseGGA(const String &line) {
int commas[15], idx = -1;
for (int i = 0; i < 15; i++) {
idx = line.indexOf(',', idx + 1);
if (idx == -1) break;
commas[i] = idx;
}
String latVal = line.substring(commas[1] + 1, commas[2]);
String latDir = line.substring(commas[2] + 1, commas[3]);
String lonVal = line.substring(commas[3] + 1, commas[4]);
String lonDir = line.substring(commas[4] + 1, commas[5]);
latitude = convertLatLon(latVal, latDir);
longitude = convertLatLon(lonVal, lonDir);
fixQuality = line.substring(commas[5] + 1, commas[6]);
altitude = line.substring(commas[8] + 1, commas[9]) + " m";
}
parseGGA() extracts raw latitude and longitude from the NMEA $GNGGA sentence. It then converts these into decimal degrees (convertLatLon()). It also parses the altitude and the GPS fix status (fixQuality), which tells you the quality of the positional fix.
convertLatLon() Helper
String convertLatLon(const String &val, const String &dir) {
if (val.length() < 4) return "--";
int degLen = (dir == "N" || dir == "S") ? 2 : 3;
float degrees = val.substring(0, degLen).toFloat();
float minutes = val.substring(degLen).toFloat();
float decimal = degrees + (minutes / 60.0);
if (dir == "S" || dir == "W") decimal *= -1;
return String(decimal, 6);
}
This helper converts NMEA latitude and longitude, given as degrees and minutes, into pure decimal degrees. It also accounts for north/south and east/west hemispheres.
formatTime() and describeFixQuality()
String formatTime(int h, int m, int s) {
char buffer[9];
sprintf(buffer, "%02d:%02d:%02d", h, m, s);
return String(buffer);
}
String describeFixQuality(const String &code) {
if (code == "0") return "0 → Not fixed";
if (code == "1") return "1 → Standalone";
if (code == "2") return "2 → DGPS Fix";
if (code == "3") return "3 → PPS Fix";
if (code == "4") return "4 → RTK Fixed";
if (code == "5") return "5 → RTK Float";
return code + " → Unknown";
}
The formatTime() helper formats hours, minutes, and seconds into a human-readable HH:MM:SS string. The describeFixQuality() helper returns a textual description of the numeric fix quality value (e.g. "Standalone", "RTK Fixed"), making output easier to understand.
parseGSVLine() for Satellite Data
void parseGSVLine(const String &line) {
if (line.startsWith("$GPGSV")) gpsCount += countSatellitesInLine(line);
else if (line.startsWith("$GBGSV")) beidouCount += countSatellitesInLine(line);
else if (line.startsWith("$GAGSV")) galileoCount += countSatellitesInLine(line);
else if (line.startsWith("$GQGSV")) qzssCount += countSatellitesInLine(line);
else if (line.startsWith("$GLGSV")) glonassCount += countSatellitesInLine(line);
int fieldStart = 0, fieldEnd = 0, fieldCount = 0;
while ((fieldEnd = line.indexOf(',', fieldStart)) != -1) {
String field = line.substring(fieldStart, fieldEnd);
fieldStart = fieldEnd + 1;
fieldCount++;
if (fieldCount >= 7 && ((fieldCount - 7) % 4 == 0)) {
appendSNR(field);
}
}
fieldEnd = line.indexOf('*');
if (fieldEnd != -1 && fieldStart < fieldEnd) {
appendSNR(line.substring(fieldStart, fieldEnd));
}
}
This function parses NMEA $GxGSV sentences to increment satellite counts for each constellation. It also extracts SNR (Signal-to-Noise Ratio) values for each visible satellite and passes them to appendSNR()
countSatellitesInLine() and appendSNR()
int countSatellitesInLine(const String &line) {
int count = 0, idx = -1;
for (int i = 0; i < 20; i++) {
idx = line.indexOf(',', idx + 1);
if (idx == -1) break;
if (i >= 4 && (i - 4) % 4 == 0) count++;
}
return count;
}
void appendSNR(const String &field) {
if (field.length() > 0 && isDigit(field[0])) {
int snr = field.toInt();
if (snr > 0 && snr <= 99) {
snrList += String(snr) + " dB ";
}
}
}
The helper countSatellitesInLine() counts satellites described in a GSV message. appendSNR() adds valid SNR values to a string list for signal strength analysis.
summarizeSNR()
void summarizeSNR() {
int excellent = 0, good = 0, fair = 0, weak = 0, invalid = 0;
int start = 0;
while (start >= 0 && start < snrList.length()) {
int end = snrList.indexOf(" dB", start);
if (end == -1) break;
int val = snrList.substring(start, end).toInt();
if (val >= 40) excellent++;
else if (val >= 30) good++;
else if (val >= 20) fair++;
else if (val >= 1) weak++;
else invalid++;
start = snrList.indexOf(" ", end);
if (start == -1) break;
start += 2;
}
DEBUGSerial.println("---- Signal Strength Summary ----");
DEBUGSerial.print("Excellent (≥40 dB) : "); DEBUGSerial.println(excellent);
DEBUGSerial.print("Good (30–39 dB): "); DEBUGSerial.println(good);
DEBUGSerial.print("Fair (20–29 dB): "); DEBUGSerial.println(fair);
DEBUGSerial.print("Weak (1–19 dB) : "); DEBUGSerial.println(weak);
DEBUGSerial.print("Invalid (0 or N/A) : "); DEBUGSerial.println(invalid);
}
This utility processes the SNR list into categories — “Excellent,” “Good,” “Fair,” “Weak,” or “Invalid” — so that you can quickly see overall signal health. It prints a summary report over DEBUGSerial
printGNSSInfo() Output Formatting
void printGNSSInfo() {
DEBUGSerial.println("\n====== GNSS INFO ======");
DEBUGSerial.print("Fix Quality: "); DEBUGSerial.println(describeFixQuality(fixQuality));
if (fixQuality != "0" && fixQuality != "") {
DEBUGSerial.print("UTC Time : "); DEBUGSerial.println(utcTime);
DEBUGSerial.print("Local Time : "); DEBUGSerial.println(localTime);
DEBUGSerial.print("Latitude : "); DEBUGSerial.println(latitude);
DEBUGSerial.print("Longitude : "); DEBUGSerial.println(longitude);
DEBUGSerial.print("Speed : "); DEBUGSerial.print(speedKmh); DEBUGSerial.println(" km/h");
DEBUGSerial.print("Altitude : "); DEBUGSerial.println(altitude);
} else {
DEBUGSerial.println("UTC Time : --");
DEBUGSerial.println("Local Time : --");
DEBUGSerial.println("Latitude : --");
DEBUGSerial.println("Longitude : --");
DEBUGSerial.println("Speed : --");
DEBUGSerial.println("Altitude : --");
}
DEBUGSerial.println("---- Satellites Visible ----");
DEBUGSerial.print("GPS(USA) : "); DEBUGSerial.println(gpsCount);
DEBUGSerial.print("BeiDou(China) : "); DEBUGSerial.println(beidouCount);
DEBUGSerial.print("Galileo(Europe) : "); DEBUGSerial.println(galileoCount);
DEBUGSerial.print("QZSS(Japan) : "); DEBUGSerial.println(qzssCount);
DEBUGSerial.print("GLONASS(Russia) : "); DEBUGSerial.println(glonassCount);
summarizeSNR();
DEBUGSerial.println("===================================");
}
This function gathers all parsed data and neatly formats it into human-readable output. It includes fixed quality, UTC/local time, position, speed, altitude, as well as satellite counts for each constellation. Finally, it calls summarizeSNR() to print the signal quality summary.
resetCounters()
void resetCounters() {
gpsCount = beidouCount = galileoCount = qzssCount = glonassCount = 0;
snrList = "";
}
At the end of each cycle, this helper resets all satellite counters and the SNR list so that each new interval reports fresh data without retaining the old counts.
Output Example

Real-Time Kinematic (RTK) and GNSS Corrections
Achieving centimeter-level positioning accuracy is now a practical reality with Real-Time Kinematic (RTK) and other GNSS correction techniques. Many industries today such as precision agriculture, surveying, geospatial mapping, and construction require accurate geolocation to support automation, machine control, and enhanced data integrity.
Standard GNSS solutions typically produce positions with meter-level accuracy, which is insufficient for most of these tasks. RTK and Network RTK (NRTK) correction methods overcome these limitations and enable real-time positioning at the centimeter level.
⇒ Error Sources in GNSS
GNSS receivers calculate their position by measuring the travel time of signals from multiple satellites. This time is converted into a distance measurement. However, these signals are impacted by a range of errors including:
satellite clock and orbit inaccuracies.
atmospheric interference in the ionosphere and troposphere.
Without proper correction, these errors accumulate and degrade positional accuracy, limiting the practical use of traditional standalone GNSS solutions.
⇒ How RTK Improves Accuracy
RTK improves on traditional GNSS by using carrier-phase measurements rather than simple code-based ranging. This phase-based approach is far more precise because the wavelength of the carrier signal is only about 19 cm.
To achieve centimeter-level accuracy, RTK algorithms need to resolve the integer ambiguity, the number of full wavelengths between the satellite and receiver through a process known as Integer Ambiguity Resolution (IAR). Once resolved, RTK can deliver highly accurate positions as long as correction data is continuously available.
⇒ Base Station and Correction Transmission
In RTK setups, a stationary base station is established at a precisely surveyed location. This base station tracks the same satellite signals as the moving rover and computes its own range errors. It then transmits these error corrections often as RTCM messages to the rover receiver in real-time.
These messages can be sent by:
Radio,
Cellular networks (using NTRIP protocols), or
Other wireless links.
With these corrections applied, the rover can rapidly converge to a fixed solution with centimeter-level accuracy.
» What is RTCM Data?
RTCM stands for Radio Technical Commission for Maritime Services, an international standards body that defines communication protocols for GNSS corrections. RTCM data consists of structured messages that contain the correction information required for accurate positioning.
By using a common format, RTCM messages ensure interoperability between different GNSS receivers and RTK equipment. This open standard allows users to mix and match hardware and software components as long as they support RTCM data, making it easier to implement RTK across a broad range of devices and platforms.
» What is NRTK (Network RTK)?
Network RTK (NRTK) is a real-time GNSS correction service that uses a network of reference stations instead of a single base. Data from multiple stations is sent to a central processor, which models regional errors and sends tailored corrections to the rover.
This provides consistent, centimeter-level accuracy over a much larger area than traditional RTK, making NRTK ideal for:
Precision farming.
Surveying.
Mapping.
Autonomous systems.
Note: Network RTK(NRTK) only valid if your receiver/rover atleast within 50Km from nearby base station unless it wont give valuable positional information.
» GT-U16 RTK L1+L5: Initial Observations and Development Insights
Even though the manufacturer, Goouuu Tech, states that the GT-U16 RTK L1+L5 module supports RTK and can calculate RTCM data, we haven’t been able to fully test these features yet due to the lack of publicly available information especially since most documentation is not detailed and only in Chinese.
We are currently working on configuring the GT-U16 as both a portable base station and rover for transmitting and receiving RTCM data, but this is still under development. In this article, we’re simply introducing the module and explaining their functions, and once we complete our testing and setup, we’ll publish a part two with detailed results and instructions.
» Pinout Guide for GT-U16 SDK Usage
In order to connect with iNavTool you will need USB to TTL converter and connect them properly as shown here,


After connecting it properly insert it on your PC and install iNavTool software(from GitHub) then select the correct port of your USB to TTL module like this,

Working with iNavTool Software
We were able to obtain a Software Development Kit (SDK) called iNavTool from ICOE (Shanghai) Technologies Co., Ltd. This tool provides a graphical user interface that displays all the previously mentioned GNSS data in real time, including the visible satellite constellations, signal strength, latitude, longitude, and positional mapping,etc.
Configuration Capabilities
Additionally, iNavTool allows you to configure the GT-U16 RTK module directly from its settings panel. These options include adjustments to the module’s sample rate, bandwidth, and GNSS band selection. However, the documentation is still incomplete, so some features and their effects are not fully explained. Until further testing is complete, it’s recommended to take note of all default configuration values before making any changes. We are continuing to explore these options and will provide updated findings once our tests are complete.
Current Status and Future Updates
Although the GT-U16 RTK module looks promising, there’s still a lot to explore, especially around its base/rover capabilities and RTCM data handling. Our testing is ongoing, and we’ll update you as we discover more. In the meantime, we encourage you to give it a try in your own projects and feel free to share your experience or ask any questions. Have you tested this module already? What challenges did you face? Let us know, so we can grow this knowledge base together!
This article walks you through the process of interfacing and parsing GNSS data using the Goouuu Tech GT-U16 RTK L1+L5 module with an Arduino. It begins with hardware setup, including the correct connections and recommended antennas, followed by code examples that read and decode NMEA messages into useful data like time, position, altitude, and satellite signal strength. RTK and NRTK concepts are explained to highlight the role of RTCM correction data in obtaining centimeter-level accuracy. Finally, the use of iNavTool software for configuration and real-time satellite monitoring is covered.
Complete Arduino Code for GT U16 RTK L1 L5 Module
Projects Related to GNSS Modules
If you are looking for some project ideas related to GNSS Modules, below are some useful links
How Does a NEO-6M GPS Module Work and How to Interface it with ESP32
Learn how to interface the NEO-6M GPS module with ESP32 to get real-time location data. This step-by-step tutorial guides you through serial communication, parsing NMEA sentences, and displaying latitude/longitude using Arduino IDE.
How to Interface and Configure a GPS module for CC3D Flight Controller
Learn how to interface and configure a GPS module with a CC3D flight controller for improved drone navigation.
How to Select the Best GSM/GPS Module for a Vehicle or Asset Tracking Applications
Compare top GPS-GSM modules for your next tracking or IoT project. Find the best fit for real-time location, SMS alerts, and remote monitoring in this quick guide for makers and engineers.
Test Code
#define GPSSerial Serial
#define DEBUGSerial Serial
void setup()
{
GPSSerial.begin(115200);
DEBUGSerial.begin(115200);
DEBUGSerial.println("Waiting for GNSS data...");
}
void loop()
{
while (GPSSerial.available()) {
DEBUGSerial.write(GPSSerial.read());
}
}
Complete Project Code
#define GPSSerial Serial
#define DEBUGSerial Serial
const unsigned long PRINT_INTERVAL = 1000;
// GNSS Data
String utcTime = "--:--:--";
String localTime = "--:--:--";
String latitude = "--";
String longitude = "--";
String altitude = "--";
String fixQuality = "--";
String speedKmh = "--";
// Satellite Signal Info
int gpsCount = 0, beidouCount = 0, galileoCount = 0, qzssCount = 0, glonassCount = 0;
String snrList = "";
String nmeaBuffer = "";
void setup() {
GPSSerial.begin(115200);
DEBUGSerial.begin(115200);
DEBUGSerial.println("Waiting for GNSS data...");
}
void loop() {
readGNSSData();
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= PRINT_INTERVAL) {
lastPrint = millis();
printGNSSInfo();
resetCounters();
}
}
// ===================== GNSS Data Parsing =======================
void readGNSSData() {
while (GPSSerial.available()) {
char c = GPSSerial.read();
if (c == '\n') {
processNMEALine(nmeaBuffer);
nmeaBuffer = "";
} else if (c != '\r') {
nmeaBuffer += c;
}
}
}
void processNMEALine(const String &line) {
if (!line.startsWith("$")) return;
if (line.startsWith("$GPGSV") || line.startsWith("$GBGSV") ||
line.startsWith("$GAGSV") || line.startsWith("$GQGSV") ||
line.startsWith("$GLGSV")) {
parseGSVLine(line);
} else if (line.startsWith("$GNRMC")) {
parseRMC(line);
} else if (line.startsWith("$GNGGA")) {
parseGGA(line);
}
}
void parseRMC(const String &line) {
int commas[13], idx = -1;
for (int i = 0; i < 13; i++) {
idx = line.indexOf(',', idx + 1);
if (idx == -1) break;
commas[i] = idx;
}
if (commas[1] - commas[0] > 1) {
String rawTime = line.substring(commas[0] + 1, commas[1]);
int hour = rawTime.substring(0, 2).toInt();
int minute = rawTime.substring(2, 4).toInt();
int second = rawTime.substring(4, 6).toInt();
utcTime = formatTime(hour, minute, second);
// Local time adjustment (+5:30)
minute += 30;
if (minute >= 60) {
minute -= 60;
hour += 1;
}
hour = (hour + 5) % 24;
localTime = formatTime(hour, minute, second);
}
if (commas[7] > commas[6]) {
float knots = line.substring(commas[6] + 1, commas[7]).toFloat();
speedKmh = String(knots * 1.852, 1);
}
}
void parseGGA(const String &line) {
int commas[15], idx = -1;
for (int i = 0; i < 15; i++) {
idx = line.indexOf(',', idx + 1);
if (idx == -1) break;
commas[i] = idx;
}
String latVal = line.substring(commas[1] + 1, commas[2]);
String latDir = line.substring(commas[2] + 1, commas[3]);
String lonVal = line.substring(commas[3] + 1, commas[4]);
String lonDir = line.substring(commas[4] + 1, commas[5]);
latitude = convertLatLon(latVal, latDir);
longitude = convertLatLon(lonVal, lonDir);
fixQuality = line.substring(commas[5] + 1, commas[6]);
altitude = line.substring(commas[8] + 1, commas[9]) + " m";
}
String convertLatLon(const String &val, const String &dir) {
if (val.length() < 4) return "--";
int degLen = (dir == "N" || dir == "S") ? 2 : 3;
float degrees = val.substring(0, degLen).toFloat();
float minutes = val.substring(degLen).toFloat();
float decimal = degrees + (minutes / 60.0);
if (dir == "S" || dir == "W") decimal *= -1;
return String(decimal, 6);
}
String formatTime(int h, int m, int s) {
char buffer[9];
sprintf(buffer, "%02d:%02d:%02d", h, m, s);
return String(buffer);
}
String describeFixQuality(const String &code) {
if (code == "0") return "0 → Not fixed";
if (code == "1") return "1 → Standalone";
if (code == "2") return "2 → DGPS Fix";
if (code == "3") return "3 → PPS Fix";
if (code == "4") return "4 → RTK Fixed";
if (code == "5") return "5 → RTK Float";
return code + " → Unknown";
}
// ====================== Satellite Info ==========================
void parseGSVLine(const String &line) {
if (line.startsWith("$GPGSV")) gpsCount += countSatellitesInLine(line);
else if (line.startsWith("$GBGSV")) beidouCount += countSatellitesInLine(line);
else if (line.startsWith("$GAGSV")) galileoCount += countSatellitesInLine(line);
else if (line.startsWith("$GQGSV")) qzssCount += countSatellitesInLine(line);
else if (line.startsWith("$GLGSV")) glonassCount += countSatellitesInLine(line);
int fieldStart = 0, fieldEnd = 0, fieldCount = 0;
while ((fieldEnd = line.indexOf(',', fieldStart)) != -1) {
String field = line.substring(fieldStart, fieldEnd);
fieldStart = fieldEnd + 1;
fieldCount++;
if (fieldCount >= 7 && ((fieldCount - 7) % 4 == 0)) {
appendSNR(field);
}
}
fieldEnd = line.indexOf('*');
if (fieldEnd != -1 && fieldStart < fieldEnd) {
appendSNR(line.substring(fieldStart, fieldEnd));
}
}
int countSatellitesInLine(const String &line) {
int count = 0, idx = -1;
for (int i = 0; i < 20; i++) {
idx = line.indexOf(',', idx + 1);
if (idx == -1) break;
if (i >= 4 && (i - 4) % 4 == 0) count++;
}
return count;
}
void appendSNR(const String &field) {
if (field.length() > 0 && isDigit(field[0])) {
int snr = field.toInt();
if (snr > 0 && snr <= 99) {
snrList += String(snr) + " dB ";
}
}
}
void summarizeSNR() {
int excellent = 0, good = 0, fair = 0, weak = 0, invalid = 0;
int start = 0;
while (start >= 0 && start < snrList.length()) {
int end = snrList.indexOf(" dB", start);
if (end == -1) break;
int val = snrList.substring(start, end).toInt();
if (val >= 40) excellent++;
else if (val >= 30) good++;
else if (val >= 20) fair++;
else if (val >= 1) weak++;
else invalid++;
start = snrList.indexOf(" ", end);
if (start == -1) break;
start += 2;
}
DEBUGSerial.println("---- Signal Strength Summary ----");
DEBUGSerial.print("Excellent (≥40 dB) : "); DEBUGSerial.println(excellent);
DEBUGSerial.print("Good (30–39 dB): "); DEBUGSerial.println(good);
DEBUGSerial.print("Fair (20–29 dB): "); DEBUGSerial.println(fair);
DEBUGSerial.print("Weak (1–19 dB) : "); DEBUGSerial.println(weak);
DEBUGSerial.print("Invalid (0 or N/A) : "); DEBUGSerial.println(invalid);
}
// ======================= Display ===========================
void printGNSSInfo() {
DEBUGSerial.println("\n====== GNSS INFO ======");
DEBUGSerial.print("Fix Quality: "); DEBUGSerial.println(describeFixQuality(fixQuality));
if (fixQuality != "0" && fixQuality != "") {
DEBUGSerial.print("UTC Time : "); DEBUGSerial.println(utcTime);
DEBUGSerial.print("Local Time : "); DEBUGSerial.println(localTime);
DEBUGSerial.print("Latitude : "); DEBUGSerial.println(latitude);
DEBUGSerial.print("Longitude : "); DEBUGSerial.println(longitude);
DEBUGSerial.print("Speed : "); DEBUGSerial.print(speedKmh); DEBUGSerial.println(" km/h");
DEBUGSerial.print("Altitude : "); DEBUGSerial.println(altitude);
} else {
DEBUGSerial.println("UTC Time : --");
DEBUGSerial.println("Local Time : --");
DEBUGSerial.println("Latitude : --");
DEBUGSerial.println("Longitude : --");
DEBUGSerial.println("Speed : --");
DEBUGSerial.println("Altitude : --");
}
DEBUGSerial.println("---- Satellites Visible ----");
DEBUGSerial.print("GPS(USA) : "); DEBUGSerial.println(gpsCount);
DEBUGSerial.print("BeiDou(China) : "); DEBUGSerial.println(beidouCount);
DEBUGSerial.print("Galileo(Europe) : "); DEBUGSerial.println(galileoCount);
DEBUGSerial.print("QZSS(Japan) : "); DEBUGSerial.println(qzssCount);
DEBUGSerial.print("GLONASS(Russia) : "); DEBUGSerial.println(glonassCount);
summarizeSNR();
DEBUGSerial.println("===================================");
}
void resetCounters() {
gpsCount = beidouCount = galileoCount = qzssCount = glonassCount = 0;
snrList = "";
}