Community project
GPS Motion Data Logger
Generated with AIThis 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
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor BreakoutMPU-6050 accelerometer/gyroscope module | 1 | DFRobot SEN0142 MPU-6050 breakout with 3-5 V board input, I2C interface, onboard I2C pull-ups, and i2cdevlib Arduino example coverage. |
Assembly
4 stepsPrepare 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.
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.
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.
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| Pin | Connection | Type |
|---|---|---|
| 3V3 | imu_1 VIN | power |
| GND | imu_1 GND | ground |
| GPIO 21 | imu_1 SDA | i2c |
| GPIO 22 | imu_1 SCL | i2c |
Firmware
ESP32#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.