Community project

GPS Motion Data Logger

jofremarianoh

Published August 22, 2026

ESP32
Photo of GPS Motion Data LoggerGenerated with AI

This project combines GPS positioning with 6-axis motion sensing to create a portable data logger that tracks location, speed, and acceleration in real time. The ESP32 microcontroller reads acceleration data from an MPU-6050 sensor while a cellular modem uploads GPS coordinates and motion samples to ThingSpeak for cloud storage and analysis.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the MPU-6050 breakout to the ESP32, connecting the cellular modem, and securing the electronics in a protective enclosure. You'll also get the complete firmware with configuration steps for your cellular provider and ThingSpeak account, enabling you to start logging motion data within minutes.

Wiring diagram

Interactive · read-only
Wiring diagram for GPS Motion Data Logger

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor BreakoutMPU-6050 accelerometer/gyroscope module1DFRobot SEN0142 MPU-6050 breakout with 3-5 V board input, I2C interface, onboard I2C pull-ups, and i2cdevlib Arduino example coverage.

Assembly

4 steps
  1. Prepare the cellular board

    Insert the activated data SIM into the LilyGO T-SIM7600G-H SIM holder while the board is unplugged. Screw the supplied 4G antenna onto the connector marked for the cellular modem and screw the supplied GPS/GNSS antenna onto the GNSS connector. Keep the GPS antenna facing upward with as much clear sky above it as possible.

    • Tip: Put the two antennas where they will not be crushed, soaked, or shielded by a metal enclosure.
    • Tip: The kart body can block satellite signals, so mount the GPS antenna high and away from large metal parts when possible.
    • Do not power the board without its cellular antenna attached — transmitting without an antenna can damage the modem.
    • Do not use a SIM that requires entering a PIN unless you have disabled its PIN first; otherwise the modem cannot register on the mobile network.
  2. Wire the acceleration sensor

    With the board unplugged, connect the MPU-6050 VIN pin to the LilyGO board 3V3 pin (power), MPU-6050 GND to a LilyGO GND pin (ground), MPU-6050 SDA to GPIO21 (data), and MPU-6050 SCL to GPIO22 (clock).

    • Tip: Use short wires and tape or strain-relief them so kart vibration cannot pull them loose.
    • Tip: Keep the sensor firmly attached to the kart frame; loose mounting makes its readings misleading.
    • Make sure VIN and GND are not swapped — swapped power can damage the sensor.
    • Do not connect the MPU-6050 to the board's 5 V pin; this design uses the safer 3.3 V supply.
  3. Mount and protect the electronics

    Place the LilyGO board and MPU-6050 in a non-metal enclosure or protected compartment. Hold the MPU-6050 flat and note which direction is forward before fastening it; this makes its X, Y, and Z acceleration readings meaningful.

    • Tip: Use foam tape or small rubber mounts to reduce sharp vibration while keeping the sensor orientation fixed.
    • Tip: Leave access to the USB connector and do not cover the modem or antennas tightly.
    • Keep the electronics away from hot engine parts, moving chains, and fuel.
    • A fully metal box can severely reduce both 4G and GPS reception.
  4. Provide USB battery power

    Connect a good-quality USB battery pack to the LilyGO board's USB connector using a short, secure cable. The USB battery powers both the ESP32 and the built-in 4G modem.

    • Tip: Choose a USB battery that can provide at least 2 A at 5 V; the modem draws brief high-current bursts while connecting and sending data.
    • Tip: Secure the cable so vibration cannot interrupt power during a run.
    • Do not use a weak USB port or thin damaged cable — a voltage drop can make the cellular modem restart while it sends data.

Pin assignments

Board wiring reference
PinConnectionType
3V3imu_1 VINpower
GNDimu_1 GNDground
GPIO 21imu_1 SDAi2c
GPIO 22imu_1 SCLi2c

Firmware

ESP32
main.cppDeploy to device
#define TINY_GSM_MODEM_SIM7600
#define TINY_GSM_RX_BUFFER 1024

#include <Arduino.h>
#include <Wire.h>
#include <TinyGsmClient.h>



struct Sample {
  float ax;
  float ay;
  float az;
  float lat;
  float lon;
  float speed;
  bool fix;
};

// Forward declarations
bool writeMpuRegister(uint8_t reg, uint8_t value);
bool readMpuAcceleration(float &ax, float &ay, float &az);
void powerModem();
bool connectCellular();
void updateLocation();
String fieldValue(float value, uint8_t decimals);
void storeSample(unsigned long sampleTime);
void uploadBatchToThingSpeak();

constexpr int I2C_SDA_PIN = 21;
constexpr int I2C_SCL_PIN = 22;
constexpr int MODEM_RX_PIN = 26;
constexpr int MODEM_TX_PIN = 27;
constexpr int MODEM_PWRKEY_PIN = 4;
constexpr int MODEM_FLIGHT_PIN = 25;
constexpr uint8_t MPU_ADDRESS = 0x68;
constexpr unsigned long SAMPLE_INTERVAL_MS = 100;
// ThingSpeak accepts sequential bulk uploads no more often than every 15 seconds.
constexpr unsigned long UPLOAD_INTERVAL_MS = 15000;
constexpr uint16_t BATCH_CAPACITY = UPLOAD_INTERVAL_MS / SAMPLE_INTERVAL_MS;

// Fill in these three values before pressing Deploy.
const char APN[] = "YOUR_SIM_APN";
const char APN_USER[] = "";
const char APN_PASS[] = "";
const char THINGSPEAK_HOST[] = "api.thingspeak.com";
const char THINGSPEAK_CHANNEL_ID[] = "YOUR_THINGSPEAK_CHANNEL_ID";
const char THINGSPEAK_WRITE_KEY[] = "YOUR_THINGSPEAK_WRITE_KEY";

HardwareSerial SerialAT(2);
TinyGsm modem(SerialAT);
TinyGsmClient client(modem);

bool mpuReady = false;
bool networkReady = false;
bool gpsReady = false;
unsigned long lastSampleMs = 0;
unsigned long lastUploadMs = 0;
float axG = NAN;
float ayG = NAN;
float azG = NAN;
float latitude = NAN;
float longitude = NAN;
float speedKmh = NAN;
bool gpsFix = false;



Sample samples[BATCH_CAPACITY];
uint16_t sampleCount = 0;

bool writeMpuRegister(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(reg);
  Wire.write(value);
  return Wire.endTransmission() == 0;
}

bool readMpuAcceleration(float &ax, float &ay, float &az) {
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(0x3B);
  if (Wire.endTransmission(false) != 0 || Wire.requestFrom(MPU_ADDRESS, (uint8_t)6) != 6) {
    return false;
  }
  const int16_t rawX = (int16_t)((Wire.read() << 8) | Wire.read());
  const int16_t rawY = (int16_t)((Wire.read() << 8) | Wire.read());
  const int16_t rawZ = (int16_t)((Wire.read() << 8) | Wire.read());
  ax = rawX / 16384.0f;
  ay = rawY / 16384.0f;
  az = rawZ / 16384.0f;
  return true;
}

void powerModem() {
  pinMode(MODEM_FLIGHT_PIN, OUTPUT);
  digitalWrite(MODEM_FLIGHT_PIN, HIGH);
  pinMode(MODEM_PWRKEY_PIN, OUTPUT);
  digitalWrite(MODEM_PWRKEY_PIN, HIGH);
  delay(300);
  digitalWrite(MODEM_PWRKEY_PIN, LOW);
  delay(1500);
  digitalWrite(MODEM_PWRKEY_PIN, HIGH);
  delay(8000);
}

bool connectCellular() {
  if (!modem.restart()) {
    Serial.println("# Modem did not answer. Check that the board has USB power and its antennas are attached.");
    return false;
  }
  Serial.println("# Waiting for the mobile network...");
  if (!modem.waitForNetwork(60000L)) {
    Serial.println("# No mobile network yet. Check the SIM, coverage, and 4G antenna.");
    return false;
  }
  if (!modem.gprsConnect(APN, APN_USER, APN_PASS)) {
    Serial.println("# Mobile data connection failed. Check the APN value from your SIM provider.");
    return false;
  }
  gpsReady = modem.enableGPS();
  if (gpsReady) {
    // SIM7600 uses 1 for 1 Hz; every other value selects its 10 Hz GNSS rate.
    gpsReady = modem.setGPSOutputRate(10);
  }
  Serial.println(gpsReady ? "# Mobile data and built-in GPS are ready at 10 samples per second." : "# Mobile data is ready; built-in GPS could not be enabled at 10 samples per second yet.");
  return true;
}

void updateLocation() {
  // The SIM7600 GNSS engine runs at 10 Hz, so each 100 ms record gets its latest fix.
  if (!gpsReady) return;
  float accuracy = NAN;
  int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0;
  const bool valid = modem.getGPS(&latitude, &longitude, &speedKmh, &accuracy,
                                  &year, &month, &day, &hour, &minute, &second);
  gpsFix = valid;
  if (!valid) {
    latitude = NAN;
    longitude = NAN;
    speedKmh = NAN;
  }
}

String fieldValue(float value, uint8_t decimals) {
  return isnan(value) ? String() : String(value, decimals);
}

void storeSample(unsigned long sampleTime) {
  if (sampleCount >= BATCH_CAPACITY) return;
  samples[sampleCount++] = {axG, ayG, azG, latitude, longitude, speedKmh, gpsFix};
}

void uploadBatchToThingSpeak() {
  if (sampleCount == 0) return;
  if (!networkReady) {
    networkReady = connectCellular();
    if (!networkReady) return;
  }

  // Entries are timestamped relative to the preceding entry, 0.1 seconds apart.
  String body = String("{\"write_api_key\":\"") + THINGSPEAK_WRITE_KEY + "\",\"updates\":[";
  for (uint16_t i = 0; i < sampleCount; ++i) {
    if (i) body += ',';
    body += String("{\"delta_t\":") + (i == 0 ? "0" : "0.1") +
            ",\"field1\":" + fieldValue(samples[i].ax, 3) +
            ",\"field2\":" + fieldValue(samples[i].ay, 3) +
            ",\"field3\":" + fieldValue(samples[i].az, 3) +
            ",\"field4\":" + fieldValue(samples[i].lat, 6) +
            ",\"field5\":" + fieldValue(samples[i].lon, 6) +
            ",\"field6\":" + fieldValue(samples[i].speed, 2) +
            ",\"status\":\"" + (samples[i].fix ? "gps_fix" : "waiting_for_gps") + "\"}";
  }
  body += "]}";

  if (!client.connect(THINGSPEAK_HOST, 80)) {
    Serial.println("# Could not reach ThingSpeak; keeping this batch for a later retry.");
    networkReady = false;
    return;
  }
  const String path = String("/channels/") + THINGSPEAK_CHANNEL_ID + "/bulk_update.json";
  client.print(String("POST ") + path + " HTTP/1.1\r\nHost: " + THINGSPEAK_HOST +
               "\r\nContent-Type: application/json\r\nContent-Length: " + body.length() +
               "\r\nConnection: close\r\n\r\n" + body);
  const unsigned long deadline = millis() + 10000;
  while (client.connected() && millis() < deadline) {
    while (client.available()) Serial.write(client.read());
  }
  client.stop();
  sampleCount = 0;
  Serial.println("# 100 ms sample batch uploaded to ThingSpeak.");
}

void setup() {
  Serial.begin(115200);
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  Wire.setClock(400000);
  mpuReady = writeMpuRegister(0x6B, 0x00);
  if (!mpuReady) Serial.println("# MPU-6050 was not found. Check its power and two data wires.");

  SerialAT.begin(115200, SERIAL_8N1, MODEM_RX_PIN, MODEM_TX_PIN);
  powerModem();
  networkReady = connectCellular();
}

void loop() {
  const unsigned long now = millis();
  if (now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    if (mpuReady && !readMpuAcceleration(axG, ayG, azG)) mpuReady = false;
    updateLocation();
    storeSample(now);
    Serial.printf("sample_ms=%lu accel_g=%.3f,%.3f,%.3f lat=%.6f lon=%.6f speed_kmh=%.2f fix=%s\n",
                  now, axG, ayG, azG, latitude, longitude, speedKmh, gpsFix ? "yes" : "no");
  }
  if (now - lastUploadMs >= UPLOAD_INTERVAL_MS || sampleCount >= BATCH_CAPACITY) {
    lastUploadMs = now;
    uploadBatchToThingSpeak();
  }
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik