Community project
GPS Boat Telemetry Logger
This GPS boat telemetry logger captures position, motion, and sensor data from a vessel and stores it locally while transmitting updates via long-range radio. Built around an ESP32 microcontroller, the system combines a u-blox NEO-M8N GPS receiver, MPU-6050 6-axis IMU, microSD card storage, and an EBYTE LoRa radio module to create a complete maritime data acquisition platform.
The guide provides a complete parts list, wiring diagram, power distribution schematic, and step-by-step assembly instructions. You'll learn how to integrate the GPS, IMU, and SD card reader, configure the 915 MHz radio link, and validate all subsystems before deployment. The included firmware handles concurrent sensor sampling, data logging, and radio transmission with error tracking and queue management.
Wiring diagram

Gather all the parts
Assemble it in 8 steps
1. Conferir módulos antes de energizar
Usar somente ESP32 DevKit V1 clássico com módulo ESP32-WROOM-32. Conferir entrada de alimentação 3,3 V e UART 3,3 V do breakout NEO-M8N; não usar NEO-6M ou placa GPS com entrada somente 5 V. O MPU escolhido é DFRobot SEN0142 e o leitor é Adafruit 4682. Não considerar a montagem revisada em bancada.
- Modelos genéricos com regulador ou pinagem diferente não são substitutos automáticos.
2. Montar e medir alimentação separadamente
Sem ESP32 e sensores ligados, conectar a saída USB de 5 V do power bank à entrada VIN do módulo AMS1117-3.3. Seu GND vai ao negativo USB. Medir 5 V na entrada e 3,3 V em VOUT. Usar módulo regulador completo, com capacitores de estabilidade montados.
- Nunca abrir o power bank, carregar molhado ou alimentar com célula de lítio sem proteção.
- Na bancada, desconectar o power bank antes de ligar o USB do computador ao ESP32. Não há isolamento contra retorno entre duas fontes.
3. Distribuir energia e desacoplar
A saída USB de 5 V alimenta o pino VIN/5V do DevKit, VCC do E220 e VIN do regulador. VOUT de 3,3 V alimenta GPS, VIN da IMU e 3V do leitor. Todos os GND são comuns. NÃO unir VOUT ao pino 3V3 do DevKit. Colocar 470 uF e 100 nF junto ao rádio; 100 uF e 100 nF junto ao leitor. Negativo dos eletrolíticos vai ao GND.
- 5 V no leitor ou nos sinais do ESP32 pode destruir componentes.
- Não alimentar o E220 pelo 3V3 do ESP32; usar fios curtos e alimentação em estrela, fora dos trilhos frágeis da protoboard.
4. Ligar GPS e IMU
GPS TX → GPIO16 (dados recebidos); GPS RX → GPIO17 (comandos de configuração). IMU SDA → GPIO21 (dados I²C); SCL → GPIO22 (relógio). SEN0142 já possui resistores de pull-up; alimentá-lo com 3,3 V mantém o barramento em 3,3 V. Deixar INT sem ligação.
5. Ligar leitor e inserir cartão
CLK → GPIO18 (relógio SPI), SO/MISO → GPIO19 (dados para ESP32), SI/MOSI → GPIO23 (dados para cartão), CS → GPIO4 (seleção). Usar fios menores que 10 cm. Com alimentação desligada, inserir cartão microSD de 8 GB formatado FAT32. DET e pinos extras SDIO ficam desconectados.
6. Ligar E220 e antenas
E220 pino 3 RXD → GPIO26 (comandos e telemetria), pino 4 TXD → GPIO25 (retorno), pino 1 M0 → GPIO32, pino 2 M1 → GPIO33, pino 5 AUX → GPIO27 (ocupado em nível baixo). Rosquear antena LoRa SMA compatível e encaixar antena GPS fornecida para o breakout. Não conectar GPIO a pino RF.
- Nunca transmitir sem antena. Transmissão vem bloqueada no firmware enquanto Anatel e configuração da margem não forem validadas.
7. Validar em bancada antes de fechar
Executar roteiro em data/TESTES.md: medir tensões sob carga, aquecimento, leitura/escrita SD, épocas GPS reais a 100 ms e funcionamento sem rádio. Conectar uma única fonte por vez. Confirmar boot e registro automático antes de cada saída.
- Não entrar na água com protoboard ou bateria exposta. Compilação não comprova revisão elétrica nem impermeabilidade.
8. Fixar a unidade no barco
Depois dos ensaios, migrar da protoboard para placa soldada com conectores travados. Fixar IMU rigidamente, registrar orientação dos eixos e colocar antena GPS sob tampa plástica voltada ao céu. Manter antena LoRa vertical e afastada do GPS, corpo, água e conversor. Instalar power bank e cabos internos em caixa vedada com alívio de tração; nenhuma tela, botão ou cabo externo é necessário na remada. Carregar somente em local seco e fora da caixa fechada.
Review all connections
1. Connections between "battery" and "ESP32"
2. Connections between "regulator" and "ESP32"
3. Connections between "gps" and "ESP32"
4. Connections between "imu" and "ESP32"
5. Connections between "sd" and "ESP32"
6. Connections between "card" and "ESP32"
7. Connections between "radio" and "ESP32"
8. Connections between "c_radio_bulk" and "ESP32"
9. Connections between "c_radio_hf" and "ESP32"
10. Connections between "c_sd_bulk" and "ESP32"
11. Connections between "c_sd_hf" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <Preferences.h>
#include <esp_timer.h>
#include <atomic>
#include <stdarg.h>
#include <math.h>
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
#include <Adafruit_MPU6050.h>
#define FREQUENCY_900
#define E220_22
#include <LoRa_E220.h>
#include "config.h"
#include "protocol.h"
HardwareSerial gpsPort(2), radioPort(1);
SFE_UBLOX_GNSS gnss;
Adafruit_MPU6050 imu;
LoRa_E220 e220(RADIO_RX, RADIO_TX, &radioPort, RADIO_AUX, RADIO_M0, RADIO_M1, UART_BPS_RATE_9600);
struct LogLine { char text[320]; uint16_t len; };
QueueHandle_t logQueue;
std::atomic<uint32_t> logDrops{0}, sdErrors{0}, radioSent{0}, radioSkipped{0};
std::atomic<bool> sdOK{false};
bool gpsPresent = false, gpsConfigOK = false, imuOK = false, radioOK = false;
uint64_t sessionId;
uint32_t logSequence = 0, gpsEpochs = 0, gpsGood = 0, gpsDt100 = 0, gpsDtBad = 0, gpsDup = 0;
uint32_t imuSamples = 0, imuDeadlines = 0, lastTow = 0;
bool hadTow = false;
UBX_NAV_PVT_data_t pvt{};
uint64_t lastGpsUs = 0, lastImuUs = 0;
sensors_event_t acc{}, gyr{}, temp{};
portMUX_TYPE snapshotMux = portMUX_INITIALIZER_UNLOCKED;
Telemetry latest{};
void logLine(const char *format, ...) {
LogLine line{};
va_list args;
va_start(args, format);
int n = vsnprintf(line.text, sizeof(line.text) - 7, format, args);
va_end(args);
if (n < 0 || n >= int(sizeof(line.text) - 7)) { ++logDrops; return; }
uint16_t crc = crc16(reinterpret_cast<uint8_t *>(line.text), n);
int m = snprintf(line.text + n, sizeof(line.text) - n, ",%04X\n", crc);
line.len = n + m;
if (xQueueSend(logQueue, &line, 0) != pdTRUE) ++logDrops;
}
// Dias desde 1970-01-01, calendário gregoriano, algoritmo civil padrão.
int64_t utcMillis(const UBX_NAV_PVT_data_t &v) {
if (!v.valid.bits.validDate || !v.valid.bits.validTime || !v.valid.bits.fullyResolved || v.sec > 59) return 0;
int y = v.year; unsigned m = v.month, d = v.day;
y -= m <= 2;
const int era = (y >= 0 ? y : y - 399) / 400;
const unsigned yoe = unsigned(y - era * 400);
const unsigned doy = (153 * (int(m) + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
int64_t days = int64_t(era) * 146097 + doe - 719468;
return ((days * 24 + v.hour) * 3600 + v.min * 60 + v.sec) * 1000 + v.nano / 1000000;
}
void onPVT(UBX_NAV_PVT_data_t *v) {
if (hadTow && v->iTOW == lastTow) { ++gpsDup; return; }
uint32_t dt = hadTow ? (v->iTOW + 604800000ULL - lastTow) % 604800000ULL : 0;
if (hadTow) { if (dt == 100) ++gpsDt100; else ++gpsDtBad; }
hadTow = true; lastTow = v->iTOW;
pvt = *v; lastGpsUs = esp_timer_get_time(); ++gpsEpochs;
bool fix = v->flags.bits.gnssFixOK && (v->fixType == 2 || v->fixType == 3) && !v->flags3.bits.invalidLlh;
if (fix) ++gpsGood;
logLine("G,%016llX,%lu,%llu,%lu,%04u-%02u-%02uT%02u:%02u:%02u,%ld,%u,%u,%u,%u,%ld,%ld,%ld,%ld,%ld,%lu,%lu,%u,%lu",
(unsigned long long)sessionId, (unsigned long)++logSequence, (unsigned long long)lastGpsUs,
(unsigned long)v->iTOW, v->year, v->month, v->day, v->hour, v->min, v->sec,
(long)v->nano, v->valid.all, fix, v->fixType, v->numSV,
(long)v->lat, (long)v->lon, (long)v->hMSL, (long)v->gSpeed, (long)v->headMot,
(unsigned long)v->hAcc, (unsigned long)v->sAcc, v->pDOP, (unsigned long)dt);
}
bool initGPS() {
// A velocidade configurada não é salva em flash. Detectar também uma configuração anterior.
const uint32_t baudRates[] = {9600, 38400, 115200, 57600, 19200, 4800};
bool found = false;
gpsPort.setRxBufferSize(4096);
for (uint32_t b : baudRates) {
gpsPort.begin(b, SERIAL_8N1, GPS_RX, GPS_TX);
delay(80);
if (gnss.begin(gpsPort, 500)) { found = true; break; }
gpsPort.end();
}
if (!found) return false;
gnss.setSerialRate(115200);
delay(100);
gpsPort.updateBaudRate(115200);
delay(100);
bool ok = gnss.setUART1Output(COM_TYPE_UBX, 600);
// M8N: 10 Hz com GPS apenas. Não assumir que múltiplas constelações mantêm 10 Hz.
ok = gnss.enableGNSS(true, SFE_UBLOX_GNSS_ID_GPS, 600) && ok;
ok = gnss.enableGNSS(false, SFE_UBLOX_GNSS_ID_GLONASS, 600) && ok;
ok = gnss.enableGNSS(false, SFE_UBLOX_GNSS_ID_BEIDOU, 600) && ok;
gnss.enableGNSS(false, SFE_UBLOX_GNSS_ID_GALILEO, 600);
gnss.enableGNSS(false, SFE_UBLOX_GNSS_ID_SBAS, 600);
gnss.enableGNSS(false, SFE_UBLOX_GNSS_ID_QZSS, 600);
ok = gnss.setNavigationFrequency(10, 600) && ok;
ok = gnss.setAutoPVTcallbackPtr(onPVT, 600) && ok;
gpsConfigOK = ok;
return true;
}
bool waitAux(uint32_t timeoutMs) {
uint32_t start = millis(), highSince = 0;
while (millis() - start < timeoutMs) {
if (!digitalRead(RADIO_AUX)) highSince = 0;
else if (!highSince) highSince = millis();
else if (millis() - highSince >= 3) return true;
delay(1);
}
return false;
}
bool initRadio() {
// Inicializar somente na partida; nenhum timeout de rádio bloqueia aquisição em operação.
digitalWrite(RADIO_M0, HIGH); digitalWrite(RADIO_M1, HIGH);
pinMode(RADIO_M0, OUTPUT); pinMode(RADIO_M1, OUTPUT); pinMode(RADIO_AUX, INPUT);
delay(100);
if (!waitAux(1500) || !e220.begin()) return false;
Configuration cfg{};
cfg.ADDH = 0; cfg.ADDL = 1;
cfg.SPED.uartBaudRate = UART_BPS_9600;
cfg.SPED.airDataRate = AIR_DATA_RATE_010_24;
cfg.SPED.uartParity = MODE_00_8N1;
cfg.OPTION.subPacketSetting = SPS_200_00;
cfg.OPTION.RSSIAmbientNoise = RSSI_AMBIENT_NOISE_DISABLED;
cfg.OPTION.transmissionPower = POWER_10; // Menor potência, ensaio inicial; não promessa de 3 km.
cfg.CHAN = RADIO_CHANNEL;
cfg.TRANSMISSION_MODE.enableRSSI = RSSI_DISABLED;
cfg.TRANSMISSION_MODE.fixedTransmission = FT_TRANSPARENT_TRANSMISSION;
cfg.TRANSMISSION_MODE.enableLBT = LBT_ENABLED;
cfg.TRANSMISSION_MODE.WORPeriod = WOR_2000_011;
cfg.CRYPT.CRYPT_H = 0; cfg.CRYPT.CRYPT_L = 0;
auto status = e220.setConfiguration(cfg, WRITE_CFG_PWR_DWN_LOSE);
if (status.code != E220_SUCCESS) return false;
auto readback = e220.getConfiguration();
if (readback.status.code != E220_SUCCESS) return false;
auto r = static_cast<Configuration *>(readback.data);
bool ok = r && memcmp(reinterpret_cast<uint8_t *>(r) + 3, reinterpret_cast<uint8_t *>(&cfg) + 3, 8) == 0;
readback.close();
if (!RF_TX_ENABLED) {
digitalWrite(RADIO_M0, HIGH); digitalWrite(RADIO_M1, HIGH); // Modo configuração: não transmitir.
}
return ok;
}
int16_t scaled(float value, float factor) {
float v = value * factor;
if (v > 32767) return 32767;
if (v < -32768) return -32768;
return int16_t(lroundf(v));
}
void updateSnapshot(uint64_t now) {
Telemetry t{};
t.magic[0] = 'R'; t.magic[1] = 'B'; t.version = 1; t.type = 1; t.length = sizeof(t);
t.session = sessionId; t.mono_us = now; t.utc_ms = utcMillis(pvt); t.itow_ms = pvt.iTOW;
t.lat_e7 = pvt.lat; t.lon_e7 = pvt.lon; t.speed_mm_s = pvt.gSpeed; t.course_e5 = pvt.headMot; t.hacc_mm = pvt.hAcc;
uint64_t age = lastGpsUs ? (now - lastGpsUs) / 1000 : 65535;
t.gps_age_ms = age > 65535 ? 65535 : age;
bool fix = lastGpsUs && age <= 500 && pvt.flags.bits.gnssFixOK && (pvt.fixType == 2 || pvt.fixType == 3) && !pvt.flags3.bits.invalidLlh;
t.flags = (fix ? 1 : 0) | ((t.utc_ms && age <= 500) ? 2 : 0) |
((imuOK && lastImuUs && now - lastImuUs <= 50000) ? 4 : 0) | (sdOK.load() ? 8 : 0) |
(gpsConfigOK ? 16 : 0) | (radioOK ? 32 : 0) | 64; // bit 6: métricas de remo ainda não integradas.
t.satellites = pvt.numSV; t.fix_type = pvt.fixType;
t.ax_mg = scaled(acc.acceleration.x, 1000.0f / 9.80665f);
t.ay_mg = scaled(acc.acceleration.y, 1000.0f / 9.80665f);
t.az_mg = scaled(acc.acceleration.z, 1000.0f / 9.80665f);
t.gx_mrad_s = scaled(gyr.gyro.x, 1000); t.gy_mrad_s = scaled(gyr.gyro.y, 1000); t.gz_mrad_s = scaled(gyr.gyro.z, 1000);
t.log_drops = logDrops.load(); t.sd_errors = sdErrors.load();
portENTER_CRITICAL(&snapshotMux); latest = t; portEXIT_CRITICAL(&snapshotMux);
}
void acquisitionTask(void *) {
uint64_t nextImu = esp_timer_get_time(), nextStatus = nextImu + 1000000;
for (;;) {
if (gpsPresent) { gnss.checkUblox(); gnss.checkCallbacks(); }
uint64_t now = esp_timer_get_time();
if (now >= nextImu) {
uint64_t missed = (now - nextImu) / 10000;
imuDeadlines += missed;
nextImu += (missed + 1) * 10000; // Não fabricar amostras atrasadas.
if (imuOK) {
// Verificar ACK no endereço antes da leitura; não registrar leitura antiga se I2C falhar.
Wire.beginTransmission(0x68);
bool ack = Wire.endTransmission() == 0;
if (ack && imu.getEvent(&acc, &gyr, &temp)) {
lastImuUs = esp_timer_get_time(); ++imuSamples;
logLine("I,%016llX,%lu,%llu,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.3f",
(unsigned long long)sessionId, (unsigned long)++logSequence, (unsigned long long)lastImuUs,
acc.acceleration.x, acc.acceleration.y, acc.acceleration.z,
gyr.gyro.x, gyr.gyro.y, gyr.gyro.z, temp.temperature);
} else ++imuDeadlines;
}
updateSnapshot(esp_timer_get_time());
}
if (now >= nextStatus) {
nextStatus = now + 1000000;
logLine("S,%016llX,%lu,%llu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u",
(unsigned long long)sessionId, (unsigned long)++logSequence, (unsigned long long)now,
(unsigned long)gpsEpochs, (unsigned long)gpsGood, (unsigned long)gpsDt100,
(unsigned long)gpsDtBad, (unsigned long)gpsDup, (unsigned long)imuSamples,
(unsigned long)imuDeadlines, (unsigned long)logDrops.load(), (unsigned long)sdErrors.load(),
(unsigned long)radioSent.load(), (unsigned long)radioSkipped.load(),
gpsConfigOK, imuOK, sdOK.load(), radioOK, RF_TX_ENABLED);
Serial.printf("GPS epochs=%lu good=%lu dt100=%lu bad=%lu dup=%lu IMU=%lu late=%lu SD=%d errors=%lu drops=%lu RF=%lu skipped=%lu TX=%d\n",
(unsigned long)gpsEpochs, (unsigned long)gpsGood, (unsigned long)gpsDt100,
(unsigned long)gpsDtBad, (unsigned long)gpsDup, (unsigned long)imuSamples,
(unsigned long)imuDeadlines, sdOK.load(), (unsigned long)sdErrors.load(),
(unsigned long)logDrops.load(), (unsigned long)radioSent.load(), (unsigned long)radioSkipped.load(), RF_TX_ENABLED);
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
void storageTask(void *) {
File file;
bool mounted = false;
uint32_t retryAt = 0, flushedAt = 0, segment = 0;
size_t fileSize = 0, used = 0;
uint32_t bufferedRecords = 0;
uint8_t buffer[4096];
LogLine line{};
for (;;) {
uint32_t now = millis();
if (!file) {
sdOK = false;
if (int32_t(now - retryAt) < 0) { vTaskDelay(pdMS_TO_TICKS(20)); continue; }
retryAt = now + 2000;
if (mounted) SD.end();
mounted = SD.begin(SD_CS, SPI, 4000000);
if (!mounted || SD.cardType() == CARD_NONE) { ++sdErrors; continue; }
// Verificar escrita e leitura antes de aceitar o cartão.
SD.remove("/rb_test.bin");
File test = SD.open("/rb_test.bin", FILE_WRITE);
const uint8_t pattern[] = {0x52, 0x42, 0x0A, 0xFF};
bool passed = test && test.write(pattern, sizeof(pattern)) == sizeof(pattern);
if (test) { test.flush(); test.close(); }
test = SD.open("/rb_test.bin", FILE_READ);
uint8_t actual[4]{};
passed = passed && test && test.read(actual, 4) == 4 && memcmp(pattern, actual, 4) == 0;
if (test) test.close();
SD.remove("/rb_test.bin");
if (!passed) { ++sdErrors; continue; }
char path[64];
do { snprintf(path, sizeof(path), "/RB_%016llX_%04lu.csv", (unsigned long long)sessionId, (unsigned long)segment++); }
while (SD.exists(path));
file = SD.open(path, FILE_WRITE);
if (!file) { ++sdErrors; continue; }
fileSize = 0; flushedAt = now; sdOK = true;
Serial.printf("SD: %s (%llu MB)\n", path, (unsigned long long)(SD.cardSize() / 1048576));
}
if (xQueueReceive(logQueue, &line, pdMS_TO_TICKS(10)) == pdTRUE) {
if (used + line.len > sizeof(buffer)) {
size_t count = file.write(buffer, used);
if (count != used) {
++sdErrors; sdOK = false; file.close();
// Não duplicar silenciosamente blocos parcialmente escritos: contar perda explícita.
logDrops.fetch_add(bufferedRecords); used = 0; bufferedRecords = 0;
} else { fileSize += used; used = 0; bufferedRecords = 0; }
}
memcpy(buffer + used, line.text, line.len); used += line.len; ++bufferedRecords;
}
if (file && now - flushedAt >= 1000) {
if (used && file.write(buffer, used) != used) { ++sdErrors; logDrops.fetch_add(bufferedRecords); sdOK = false; file.close(); }
fileSize += used; used = 0; bufferedRecords = 0;
if (file) {
file.flush();
if (file.getWriteError()) { ++sdErrors; sdOK = false; file.close(); }
}
flushedAt = now;
}
if (file && fileSize >= 64 * 1024 * 1024) { file.flush(); file.close(); }
vTaskDelay(pdMS_TO_TICKS(1));
}
}
void radioTask(void *) {
uint32_t nextAt = millis() + 1000, seq = 0, highSince = 0;
for (;;) {
uint32_t now = millis();
// Drenar qualquer entrada; este protocolo de barco é telemetria sem ACK.
while (radioPort.available()) radioPort.read();
if (!digitalRead(RADIO_AUX)) highSince = 0;
else if (!highSince) highSince = now;
if (int32_t(now - nextAt) >= 0) {
nextAt = now + 1000;
Telemetry t;
portENTER_CRITICAL(&snapshotMux); t = latest; portEXIT_CRITICAL(&snapshotMux);
t.sequence = ++seq; // Sequência também avança quando a oportunidade é descartada.
t.crc = crc16(reinterpret_cast<uint8_t *>(&t), sizeof(t) - 2);
if (RF_TX_ENABLED && radioOK && highSince && now - highSince >= 3 && radioPort.availableForWrite() >= int(sizeof(t))) {
if (radioPort.write(reinterpret_cast<const uint8_t *>(&t), sizeof(t)) == sizeof(t)) ++radioSent;
else ++radioSkipped;
} else ++radioSkipped;
}
vTaskDelay(pdMS_TO_TICKS(2));
}
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_OFF); btStop();
Preferences prefs;
uint32_t boot = 0;
if (prefs.begin("remo", false)) {
boot = prefs.getUInt("boot", 0) + 1;
if (!prefs.putUInt("boot", boot)) Serial.println("AVISO: contador de sessao nao persistiu");
prefs.end();
}
if (!boot) { boot = esp_random(); Serial.println("AVISO: sessao aleatoria, NVS indisponivel"); }
sessionId = ((ESP.getEfuseMac() & 0xFFFFFFFFULL) << 32) | boot;
Serial.printf("BARCO RB v1 sessao=%016llX; TX=%d; sem metricas de remo inferidas\n", (unsigned long long)sessionId, RF_TX_ENABLED);
logQueue = xQueueCreate(256, sizeof(LogLine)); // Aproximadamente 2,3 segundos a 110 registros/s.
if (!logQueue) { Serial.println("ERRO RAM: nao iniciar viagem"); while (true) delay(1000); }
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH);
Wire.begin(I2C_SDA, I2C_SCL); Wire.setClock(400000); Wire.setTimeOut(10);
imuOK = imu.begin(0x68, &Wire);
if (imuOK) {
imu.setAccelerometerRange(MPU6050_RANGE_8_G);
imu.setGyroRange(MPU6050_RANGE_250_DEG);
imu.setFilterBandwidth(MPU6050_BAND_21_HZ);
imu.setSampleRateDivisor(9); // 1 kHz / (9+1) = 100 Hz com filtro ligado.
}
gpsPresent = initGPS();
radioOK = initRadio();
Serial.printf("INIT GPS=%d CFG10Hz=%d IMU=%d RADIO_CFG=%d\n", gpsPresent, gpsConfigOK, imuOK, radioOK);
updateSnapshot(esp_timer_get_time());
// Aquisição de maior prioridade e SD/radio em tarefas separadas.
bool tasks = xTaskCreatePinnedToCore(storageTask, "sd", 8192, nullptr, 1, nullptr, 0) == pdPASS;
tasks = (xTaskCreatePinnedToCore(radioTask, "radio", 4096, nullptr, 1, nullptr, 0) == pdPASS) && tasks;
tasks = (xTaskCreatePinnedToCore(acquisitionTask, "acq", 12288, nullptr, 3, nullptr, 1) == pdPASS) && tasks;
if (!tasks) { Serial.println("ERRO tarefas: nao iniciar viagem"); while (true) delay(1000); }
}
void loop() { delay(1000); }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.




