Learning Goals 5 min
Cluster D's capstone: a battery-powered tracker that knows where it is (GPS) and reports its location to a base station kilometres away (LoRa). The same architecture is in livestock collars, asset trackers, and missing-pet finders. By the end of this lesson you will:
- Wire a NEO-6M / NEO-M8 GPS module to an Arduino UART (TinyGPSPlus library).
- Pack lat / lon / time / battery % into a 12-byte LoRa packet.
- Build a base station that receives, decodes, and displays the location on serial (and could forward to MQTT / a map).
Warm-Up 10 min
Hardware (tracker):
- Arduino-class board (small, ideally Nano or Pro Mini for the form factor).
- GPS module (NEO-6M is cheapest, NEO-M8 is faster fix).
- GPS antenna (often built into the module).
- LoRa module (RFM95 + antenna).
- LiPo / coin cell with regulator.
- Small enclosure.
Hardware (base):
- Arduino-class board + LoRa module + antenna.
- USB to laptop.
GPS fix takes time
First time a GPS module powers on, it takes 30 s – 5 min to find satellites and compute a position. Subsequent starts (within hours) are faster. Plan your build with the antenna outside or near a window.
New Concept · Pack a tiny binary payload 20 min
GPS data via TinyGPSPlus
#include <TinyGPSPlus.h>
#include <SoftwareSerial.h>
TinyGPSPlus gps;
SoftwareSerial gpsSerial(4, 5); // RX, TX
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600); // NEO-6M default
}
void loop() {
while (gpsSerial.available()) gps.encode(gpsSerial.read());
if (gps.location.isUpdated()) {
Serial.print("Lat: "); Serial.print(gps.location.lat(), 6);
Serial.print(" Lon: "); Serial.print(gps.location.lng(), 6);
Serial.print(" Sats: "); Serial.println(gps.satellites.value());
}
}TinyGPSPlus parses NMEA sentences from the GPS and exposes lat / lon / time / speed / satellites as easy-to-read fields.
Packing into a binary packet
LoRa airtime is precious. Sending "lat=3.123456,lon=101.654321" as a string is 30+ bytes. Pack into a binary layout:
| Bytes | Field | Format |
|---|---|---|
| 0–3 | Latitude × 1e7 | int32 (4 bytes signed) |
| 4–7 | Longitude × 1e7 | int32 |
| 8 | Satellites | uint8 |
| 9 | Battery % | uint8 |
| 10–11 | Speed × 10 (km/h) | uint16 |
12 bytes total. At SF9 (~5 kbps) that's ~20 ms airtime — comfortably inside duty cycle limits even at 1 packet per minute.
void packAndSend() {
uint8_t buf[12];
int32_t lat = (int32_t)(gps.location.lat() * 10000000);
int32_t lon = (int32_t)(gps.location.lng() * 10000000);
buf[0] = lat >> 24; buf[1] = lat >> 16; buf[2] = lat >> 8; buf[3] = lat;
buf[4] = lon >> 24; buf[5] = lon >> 16; buf[6] = lon >> 8; buf[7] = lon;
buf[8] = gps.satellites.value();
buf[9] = readBatteryPct();
uint16_t speed = (uint16_t)(gps.speed.kmph() * 10);
buf[10] = speed >> 8; buf[11] = speed;
LoRa.beginPacket();
LoRa.write(buf, 12);
LoRa.endPacket();
}Base station: receive, decode, print
void onPacket(int size) {
if (size != 12) return;
uint8_t buf[12];
LoRa.readBytes(buf, 12);
int32_t lat = ((int32_t)buf[0] << 24) | ((int32_t)buf[1] << 16) |
((int32_t)buf[2] << 8) | (int32_t)buf[3];
int32_t lon = ((int32_t)buf[4] << 24) | ((int32_t)buf[5] << 16) |
((int32_t)buf[6] << 8) | (int32_t)buf[7];
uint8_t sats = buf[8];
uint8_t bat = buf[9];
uint16_t spd = ((uint16_t)buf[10] << 8) | buf[11];
Serial.print("Lat: "); Serial.print(lat / 1e7, 6);
Serial.print(" Lon: "); Serial.print(lon / 1e7, 6);
Serial.print(" Sats="); Serial.print(sats);
Serial.print(" Bat="); Serial.print(bat); Serial.print("%");
Serial.print(" Spd="); Serial.print(spd / 10.0); Serial.println(" km/h");
}Worked Example · End-to-end tracker 30 min
Step 1 — tracker hardware
- GPS module on D4 / D5 (SoftwareSerial).
- RFM95 on SPI (D10/11/12/13 + D9 reset + D2 DIO0).
- Battery monitor: voltage divider into A0.
Step 2 — tracker sketch
// L04-22 · LoRa GPS tracker
#include <TinyGPSPlus.h>
#include <SoftwareSerial.h>
#include <SPI.h>
#include <LoRa.h>
TinyGPSPlus gps;
SoftwareSerial gpsSerial(4, 5);
const long FREQ = 433E6;
unsigned long lastSendAt = 0;
const unsigned long SEND_INTERVAL_MS = 60000; // every minute
uint8_t readBatteryPct() {
int raw = analogRead(A0);
// map your divider's voltage range to 0..100
return constrain(map(raw, 600, 800, 0, 100), 0, 100);
}
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600);
LoRa.setPins(10, 9, 2);
if (!LoRa.begin(FREQ)) { Serial.println("# LoRa fail"); while (true); }
LoRa.setSpreadingFactor(9);
LoRa.setTxPower(14);
Serial.println("# Tracker ready");
}
void loop() {
while (gpsSerial.available()) gps.encode(gpsSerial.read());
if (millis() - lastSendAt >= SEND_INTERVAL_MS && gps.location.isValid()) {
lastSendAt = millis();
uint8_t buf[12];
int32_t lat = (int32_t)(gps.location.lat() * 10000000);
int32_t lon = (int32_t)(gps.location.lng() * 10000000);
buf[0] = lat >> 24; buf[1] = lat >> 16; buf[2] = lat >> 8; buf[3] = lat;
buf[4] = lon >> 24; buf[5] = lon >> 16; buf[6] = lon >> 8; buf[7] = lon;
buf[8] = gps.satellites.value();
buf[9] = readBatteryPct();
uint16_t speed = (uint16_t)(gps.speed.kmph() * 10);
buf[10] = speed >> 8; buf[11] = speed;
LoRa.beginPacket();
LoRa.write(buf, 12);
LoRa.endPacket();
Serial.print("# sent at "); Serial.print(gps.location.lat(), 5);
Serial.print(","); Serial.println(gps.location.lng(), 5);
}
}Step 3 — base station sketch
From §3 plus the LoRa boilerplate from L04-18.
Step 4 — verify outdoors
Take the tracker outside. Wait for GPS fix (the GPS module's LED stops blinking when it has a fix). Within a minute, base receives a packet.
Step 5 — log positions
Base prints each position to Serial. You can copy-paste into Google Maps to visualise. For automation: forward the lat/lon via MQTT (L04-14) to Home Assistant's map card, or to a web dashboard.
Step 6 — go for a walk
Battery-powered tracker on you. Walk 500 m. Base stays at home. You should see position updates every minute, even from a couple of streets away.
Try It Yourself 15 min
Goal: Add altitude to the packet (gps.altitude.meters() — 2 more bytes).
Goal: Add a CRC-16 to verify packet integrity. Base discards packets with wrong CRC.
Goal: Base forwards positions to a public map service. Either: publish to MQTT (L04-14) → Home Assistant map card; OR HTTP POST to traccar.org's demo server to visualise live.
Mini-Challenge · Battery + sleep 10 min
- Measure tracker idle current.
- Measure transmit-burst current.
- Calculate runtime on a 2000 mAh LiPo.
- Identify the biggest power drain: GPS continuously, transmit bursts, or microcontroller idle. Plan optimisations (deep sleep, GPS hot-fix) for L04-37.
Typical: GPS continuously running is the worst drain (~40 mA). LoRa transmit is a quick spike (~100 mA for < 100 ms). Microcontroller idle: ~5 mA. Big win: cycle the GPS off between fixes (the M8 series supports backup mode).
Recap 5 min
GPS + LoRa tracker = NEO-6M for position + TinyGPSPlus library + 12-byte binary packet + LoRa to a base. Range is whatever your LoRa SF / antenna delivers (1–10 km). Battery life depends entirely on GPS duty cycle. Cluster E starts tomorrow with closed-loop control theory.
- NEO-6M / NEO-M8
- Common low-cost GPS receiver modules. UART output, NMEA sentences at 9600 baud.
- NMEA sentences
- Plain-text GPS data format. Each sentence (
$GPGGA,$GPRMC, ...) carries a specific kind of data. Parsed by libraries. - TinyGPSPlus
- The standard Arduino library for parsing NMEA. Exposes lat / lon / altitude / time / satellites as easy fields.
- First fix time (TTFF)
- How long the GPS takes to compute a position from a cold start. NEO-6M: 30 s – 5 min.
- Binary packing
- Sending data as raw bytes instead of strings. Saves airtime on LoRa.
- Endianness
- The byte order for multi-byte values. Network protocols usually use big-endian (MSB first). Both endpoints must agree.
- Battery monitor
- A voltage divider on A0 + a calibration map to convert raw ADC reading to battery %.
- Cold / hot fix
- Cold fix = no prior data, full search. Hot fix = recent data still valid, fast lock. Backup battery on GPS retains ephemeris.
Homework 5 min
- Run a tracker test outside. Walk 500 m. Note position updates.
- Map the positions on Google Maps. Save the screenshot.
- Read ahead to ARD-L04-23 (Open vs Closed Loop). Cluster E (Robotics + control theory) starts.