GPS Speedometer
A dashboard speedometer built on the TinyGPS++ NMEA parser: every byte the NEO-6M sends over UART2 (module TX → GPIO 16/RX2) is pumped through gps.encode(), which validates sentence checksums and commits parsed $GPGGA/$GPRMC fields into gps.location, gps.speed, gps.satellites and gps.altitude. When location.isUpdated() reports a fresh fix the OLED redraws with the speed in large digits, satellites in view, altitude and a running trip odometer summed from TinyGPSPlus::distanceBetween() between consecutive fixes. Above 50 km/h a SPEEDING banner appears and the green LED on GPIO 4 lights; the button on GPIO 14 resets the trip. In the Lab the module behaves like a real receiver — no fix for the first couple of seconds, then a satellite count that climbs as the almanac fills in — and clicking the GPS module reveals a speed slider so you can “drive” the simulated car yourself.
// GPS Speedometer - live speed, trip odometer and sat count from a NEO-6M
// ESP32 DevKit v1. NEO-6M on UART2 (module TX -> GPIO16/RX2, RX -> GPIO17/TX2),
// SSD1306 OLED @0x3C on I2C (SDA=21, SCL=22). Green LED on GPIO 4 lights
// above 50 km/h, button on GPIO 14 (INPUT_PULLUP, other leg to GND) resets
// the trip counter.
//
// TinyGPS++ does the heavy lifting: feed every raw NMEA byte from Serial2
// into gps.encode() and the library checks sentence checksums and exposes the
// parsed fields as gps.location / gps.speed / gps.satellites objects.
// isUpdated() flips true the moment a fresh sentence commits - that is the
// cue to redraw. The trip odometer sums the haversine distance between
// consecutive fixes with TinyGPSPlus::distanceBetween().
// In the Lab, click the GPS module and drag the speed slider to "drive".
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <TinyGPSPlus.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
TinyGPSPlus gps;
const int LED = 4;
const int BTN = 14;
const float SPEED_LIMIT = 50.0; // km/h
double lastLat = 0, lastLng = 0;
double tripKm = 0;
bool hadFix = false;
void drawScreen(float kmh) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("GPS SPEEDOMETER");
display.setTextSize(3);
display.setCursor(10, 16);
display.print(kmh, 1);
display.setTextSize(1);
display.setCursor(0, 44);
display.print("km/h sats ");
display.print(gps.satellites.value());
display.print(" alt ");
display.print(gps.altitude.meters(), 0);
display.print("m");
display.setCursor(0, 54);
display.print("trip ");
display.print(tripKm, 3);
display.print(" km");
if (kmh > SPEED_LIMIT) {
display.setCursor(74, 54);
display.print("SPEEDING!");
}
display.display();
}
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, 16, 17); // NEO-6M on UART2
pinMode(LED, OUTPUT);
pinMode(BTN, INPUT_PULLUP);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Waiting for GPS fix");
display.display();
Serial.println("Waiting for GPS fix...");
}
void loop() {
// pump every byte from the module through the NMEA parser
while (Serial2.available() > 0) {
gps.encode(Serial2.read());
}
if (digitalRead(BTN) == LOW) { // trip reset
tripKm = 0;
Serial.println("Trip reset");
delay(250);
}
if (gps.location.isUpdated()) {
double lat = gps.location.lat();
double lng = gps.location.lng();
if (!hadFix) {
Serial.println("GPS fix acquired");
} else {
// odometer: haversine distance between consecutive fixes
tripKm += TinyGPSPlus::distanceBetween(lastLat, lastLng, lat, lng) / 1000.0;
}
hadFix = true;
lastLat = lat;
lastLng = lng;
float kmh = gps.speed.kmph();
digitalWrite(LED, kmh > SPEED_LIMIT ? HIGH : LOW);
drawScreen(kmh);
Serial.print("Speed ");
Serial.print(kmh, 1);
Serial.print(" km/h sats ");
Serial.print(gps.satellites.value());
Serial.print(" trip ");
Serial.print(tripKm, 3);
Serial.println(" km");
}
delay(20);
}Built with FluxBench.AI — the AI workbench for embedded projects.