main.ino 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. #include "TinyGPSPlus.h"
  2. #include "DHT.h"
  3. #include <Wire.h>
  4. #include <Adafruit_GFX.h>
  5. #include <Adafruit_SSD1306.h>
  6. #include "FS.h"
  7. #include "SD.h"
  8. #include "SPI.h"
  9. #include "Arduino.h"
  10. #include "math.h"
  11. #include "WiFi.h"
  12. #include "WebServer.h"
  13. /*
  14. Uncomment and set up if you want to use custom pins for the SPI communication
  15. #define REASSIGN_PINS
  16. int sck = -1;
  17. int miso = -1;
  18. int mosi = -1;
  19. int cs = -1;
  20. */
  21. // Inicia I2C en pines default ESP32 (21 SDA, 22 SCL)
  22. #define SCREEN_WIDTH 128 // Ancho píxeles
  23. #define SCREEN_HEIGHT 64 // Alto píxeles
  24. #define OLED_RESET -1 // Reset pin (no usado)
  25. // Pines para UART2 (Serial2)
  26. #define RX_PIN 16 // RX del ESP32 conectado a TX del GPS
  27. #define TX_PIN 17 // TX del ESP32 conectado a RX del GPS
  28. #define GPS_BAUD 115200
  29. #define DHTPIN 4 // Digital pin connected to the DHT sensor
  30. #define DHTTYPE DHT22
  31. #define BUTTON_PIN 27 // pin para pulsador
  32. //definicion de tiempos de pulsacion
  33. #define PULASCION_LARGA_MS 2000
  34. #define DURACION_WATCHDOG_MS 10000
  35. #define MEASUREMENT_INTERVAL_S 1 //separación entre mediciones (s)
  36. #define DEG2RAD (M_PI/180.0)
  37. // Objeto TinyGPS++
  38. TinyGPSPlus gps;
  39. HardwareSerial gpsSerial(2); // Usar UART2
  40. DHT dht(DHTPIN, DHTTYPE);
  41. Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
  42. //valores de los sensores
  43. struct SensorData {
  44. double latitude = 0.0;
  45. double longitude = 0.0;
  46. float altura = 0.0;
  47. String tiempo = "";
  48. float temperature = 0.0;
  49. float humidity = 0.0;
  50. float pressure = 0.0;
  51. };
  52. SensorData latestData;
  53. SensorData datosAntiguos;
  54. SemaphoreHandle_t dataMutex; // Mutex para proteger el acceso a latestData
  55. SemaphoreHandle_t buttonSemaphore; // Semáforo para la tarea del botón
  56. WebServer server(80);
  57. bool wifiActivado = false;
  58. unsigned long wifiLastActivity = 0;
  59. const unsigned long WIFI_TIMEOUT_MS = 300000; // 5 minutos
  60. const char* apSSID = "ESP32_GPS_Logger";
  61. const char* apPassword = "12345678";
  62. bool grabando = false; //inicia apagado
  63. bool finalizado = true; //indica que no hay ninguna grabacion ni iniciada ni pausada
  64. TaskHandle_t medicionesHandle = NULL; //para suspend/resume
  65. int pantallaEstado_grab = -1; //maquina de estados cuando se graba ruta
  66. int pantallaEstado_menu = -1; //maquina de estados cuando no se esta grabando ruta
  67. float distancia_total = 0.0;
  68. volatile unsigned long ignore_isr_until = 0; //para debounce
  69. char filename[13];
  70. void OLED_print(const String& line1, const String& line2) {
  71. display.clearDisplay();
  72. display.setTextSize(2);
  73. display.setTextColor(SSD1306_WHITE);
  74. display.setCursor(0, 0);
  75. display.println(line1);
  76. display.println(line2);
  77. display.display();
  78. }
  79. void DHT_test() {
  80. dht.begin();
  81. float h = dht.readHumidity();
  82. float t = dht.readTemperature();
  83. if (isnan(h) || isnan(t)) {
  84. Serial.println("Failed to read from DHT sensor!");
  85. OLED_print("DHT22", "Error");
  86. delay(5000);
  87. DHT_test(); // Reintentar
  88. }
  89. else OLED_print("DHT22", "Correcto");
  90. }
  91. void OLED_test() { //pantallazo a blanco y luego iniciando
  92. // Inicia I2C en pines default ESP32 (21 SDA, 22 SCL)
  93. Wire.begin();
  94. if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Dirección común: 0x3C
  95. Serial.println(F("Error: OLED no encontrado!"));
  96. for (;;); // Para siempre
  97. }
  98. display.clearDisplay();
  99. display.fillScreen(SSD1306_WHITE); // Pantalla blanca
  100. delay(500);
  101. display.display();
  102. display.clearDisplay();
  103. display.setTextSize(2); // Tamaño texto
  104. display.setTextColor(SSD1306_WHITE);
  105. display.setCursor(0, 0); // Posición
  106. display.println("Iniciando...");
  107. display.display(); // Muestra
  108. delay(1000);
  109. }
  110. void SD_test(){
  111. if (!SD.begin()) {
  112. OLED_print("SD Card", "Error\nInserte");
  113. while (!SD.begin());
  114. OLED_print("SD Card", "Insertada");
  115. } else {
  116. OLED_print("SD Card", "Correcto");
  117. }
  118. uint8_t cardType = SD.cardType();
  119. if (cardType == CARD_NONE) {
  120. OLED_print("SD Card", "No detectada");
  121. while (cardType == CARD_NONE) {
  122. delay(1000);
  123. cardType = SD.cardType();
  124. }
  125. OLED_print("SD Card", "Detectada");
  126. }
  127. uint8_t cardSize = SD.cardSize() / (1024 * 1024);
  128. }
  129. void GPS_test_wait() {
  130. // Iniciar Serial2 para GPS
  131. gpsSerial.begin(GPS_BAUD, SERIAL_8N1, RX_PIN, TX_PIN);
  132. while (((gpsSerial.available() > 0) && gps.location.isValid()) && (gps.speed.age() < 2000)) {
  133. gps.encode(gpsSerial.read());
  134. delay(100);
  135. OLED_print("GPS", "Esperando.");
  136. delay(100);
  137. OLED_print("GPS", "Esperando..");
  138. delay(100);
  139. OLED_print("GPS", "Esperando...");
  140. }
  141. OLED_print("GPS", "Encontrado");
  142. }
  143. float calcular_delta_dist(float lat1, float long1, float lat2, float long2){
  144. float R = 6371.0; // Radio de la Tierra en km
  145. float delta_lat = (lat2 - lat1) * DEG2RAD;
  146. float delta_long = (long2 - long1) * DEG2RAD;
  147. lat1 = lat1 * DEG2RAD;
  148. lat2 = lat2 * DEG2RAD;
  149. float a = sin(delta_lat/2)*sin(delta_lat/2)+cos(lat1)*cos(lat2)*sin(delta_long/2)*sin(delta_long/2);
  150. float c = 2 * atan2(sqrt(a),sqrt(1-a));
  151. return R * c; //En km
  152. }
  153. void task_mediciones(void *pvParameters) {
  154. TickType_t xLastWakeTime = xTaskGetTickCount();
  155. while(1) {
  156. // se leen los valores antes de utilizar el semaphore
  157. while (gpsSerial.available() > 0) {
  158. gps.encode(gpsSerial.read());
  159. }
  160. float new_latitude = gps.location.lat();
  161. float new_longitude = gps.location.lng();
  162. float new_altitude = gps.altitude.meters();
  163. String new_fecha = String(gps.date.year())+"-"+String(gps.date.month())+"-"+
  164. String(gps.date.day())+"T"+String(gps.time.hour())+":"+
  165. String(gps.time.minute())+":"+String(gps.time.second())+"."+
  166. String(gps.time.centisecond());
  167. float new_temp = dht.readTemperature();
  168. float new_hum = dht.readHumidity();
  169. float new_press = 0.0; // Placeholder, no hay sensor de presión
  170. if (gps.location.isValid() && datosAntiguos.latitude != 0.0) {
  171. distancia_total += calcular_delta_dist(datosAntiguos.latitude, datosAntiguos.longitude, new_latitude, new_longitude);
  172. }
  173. if (xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE) {
  174. latestData.latitude = new_latitude;
  175. latestData.longitude = new_longitude;
  176. latestData.altura = new_altitude;
  177. latestData.tiempo = new_fecha;
  178. latestData.temperature = new_temp;
  179. latestData.humidity = new_hum;
  180. latestData.pressure = new_press;
  181. datosAntiguos = latestData;
  182. xSemaphoreGive(dataMutex);
  183. }
  184. File file = SD.open(filename, FILE_APPEND);
  185. if (file) {
  186. //Crear la string para escribir en el archivo
  187. file.print(F("\t\t\t<trkpt lat=\""));
  188. file.print(datosAntiguos.latitude, 6);
  189. file.print(F("\" lon=\""));
  190. file.print(datosAntiguos.longitude, 6);
  191. file.println(F("\">"));
  192. file.print(F("\t\t\t\t<ele>"));
  193. file.print(datosAntiguos.altura);
  194. file.println(F("</ele>"));
  195. file.print(F("\t\t\t\t<time>"));
  196. file.print(datosAntiguos.tiempo);
  197. file.println(F("</time>"));
  198. file.println(F("\t\t\t\t<extensions>"));
  199. file.println(F("\t\t\t\t\t<gpxtpx:TrackPointExtension>"));
  200. file.print(F("\t\t\t\t\t\t<gpxtpx:atemp>"));
  201. file.print(datosAntiguos.temperature);
  202. file.println(F("</gpxtpx:atemp>"));
  203. file.println(F("\t\t\t\t\t</gpxtpx:TrackPointExtension>"));
  204. file.print(F("\t\t\t\t\t<custom:humidity>"));
  205. file.print(datosAntiguos.humidity);
  206. file.println(F("</custom:humidity>"));
  207. file.print(F("\t\t\t\t\t<custom:pressure>"));
  208. file.print(datosAntiguos.pressure);
  209. file.println(F("</custom:pressure>"));
  210. file.println(F("\t\t\t\t</extensions>"));
  211. file.println(F("\t\t\t</trkpt>"));
  212. // Escribir datos en el archivo
  213. file.println(frase);
  214. file.close();
  215. }
  216. vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(MEASUREMENT_INTERVAL_S*1000)); // Espera x*1000 milisegundos
  217. }
  218. }
  219. void crear_archivo(){
  220. int num = 1;
  221. sprintf(filename, "/data%03d.gpx", num);
  222. while (SD.exists(filename)) {
  223. num++;
  224. sprintf(filename, "/data%03d.gpx", num);
  225. }
  226. File file = SD.open(filename, FILE_WRITE);
  227. if (file) {
  228. file.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
  229. "<gpx creator=\"ESP32 GPS LOGGER\" version=\"1.1\"\n"
  230. "\txmlns=\"http://www.topografix.com/GPX/1/1\"\n"
  231. "\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
  232. "\txmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"\n"
  233. "\txmlns:gpxdata=\"http://www.cluetrust.com/XML/GPXDATA/1/0\"\n"
  234. "\txsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">\n"
  235. "\t<trk>\n"
  236. "\t\t<name>Rutita</name>\n"
  237. "\t\t<type>hiking</type>\n"
  238. "\t\t<trkseg>");
  239. file.close();
  240. } else {
  241. OLED_print("Error","creando archivo");
  242. }
  243. }
  244. void cerrar_archivo() {
  245. File file = SD.open(filename, FILE_APPEND);
  246. if (file){
  247. file.print("\t\t</trkseg>\n\t</trk>\n</gpx>");
  248. file.close();
  249. }
  250. int num = 1;
  251. sprintf(filename, "/data%03d.gpx", num);
  252. while (SD.exists(filename)) {
  253. num++;
  254. sprintf(filename, "/data%03d.gpx", num);
  255. }
  256. }
  257. void activarWiFi(){
  258. OLED_print("WiFi","Activando...");
  259. WiFi.mode(WIFI_AP);
  260. WiFi.softAP(apSSID, apPassword);
  261. IPAddress IP = WiFi.softAPIP();
  262. OLED_print("WiFi Activo", IP.toString());
  263. delay(2000);
  264. server.on("/", HTTP_GET, []() {
  265. wifiLastActivity = millis();
  266. String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>ESP32 GPS Logger</title>";
  267. html += "<style>body{font-family:Arial;text-align:center;margin:50px;}";
  268. html += "h1{color:#333;} a.button{background:#4CAF50;color:white;padding:20px 40px;";
  269. html += "text-decoration:none;font-size:24px;border-radius:12px;display:inline-block;margin:20px;}</style></head>";
  270. html += "<body><h1>GPS Logger</h1><p>Archivo listo para descargar:</p>";
  271. html += "<a href='/download' class='button' download>Descargar " + String(filename).substring(1) + "</a>";
  272. html += "<hr><p>IP: " + WiFi.softAPIP().toString() + "</p>";
  273. html += "<p>Se apagar&aacute; en 5 min sin uso.</p></body></html>";
  274. server.send(200, "text/html", html);
  275. });
  276. server.on("/download",HTTP_GET, []() {
  277. wifiLastActivity = millis();
  278. if (!SD.exists(filename)) {
  279. server.send(404, "text/plain", "Archivo no encontrado");
  280. return;
  281. }
  282. File file = SD.open(filename, FILE_READ);
  283. if (!file) {
  284. server.send(404, "text/plain", "Error al abrir el archivo");
  285. return;
  286. }
  287. server.streamFile(file, "application/gpx+xml");
  288. file.close();
  289. });
  290. server.begin();
  291. wifiActivado = true;
  292. wifiLastActivity = millis();
  293. }
  294. void desactivarWiFi(){
  295. server.stop();
  296. WiFi.softAPdisconnect(true);
  297. WiFi.mode(WIFI_OFF);
  298. wifiActivado = false;
  299. OLED_print("WiFi","Apagado");
  300. }
  301. void IRAM_ATTR isr_button() {
  302. unsigned long now = millis();
  303. if (now < ignore_isr_until) {
  304. return; // Ignorar interrupción si está dentro del período de debounce
  305. }
  306. static unsigned long lastInterrupt = 0;
  307. if ((now - lastInterrupt) > 300 ){ // debounce de 300 ms
  308. BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  309. xSemaphoreGiveFromISR(buttonSemaphore, &xHigherPriorityTaskWoken);
  310. lastInterrupt = now;
  311. if (xHigherPriorityTaskWoken) {
  312. portYIELD_FROM_ISR();
  313. }
  314. }
  315. }
  316. void drawProgressBar(int x, int y, int w, int h, unsigned long progress, unsigned long total) {
  317. display.drawRect(x, y, w, h, SSD1306_WHITE); // Dibuja el borde
  318. int filledWidth = (progress * w) / total;
  319. display.fillRect(x + 1, y + 1, filledWidth - 2, h - 2, SSD1306_WHITE); // Dibuja la barra llena
  320. display.display();
  321. }
  322. void task_ui(void *pvParameters){
  323. unsigned long pressTime = 0;
  324. unsigned long lastActivity = millis();
  325. bool pantallaOn = true; //comprobar el estado inicial, no se cual sera
  326. bool processingButton = false;
  327. while(1){
  328. if (xSemaphoreTake(buttonSemaphore, pdMS_TO_TICKS(200)) == pdTRUE){ //button pressed
  329. if (processingButton) continue; //evita reentradas
  330. processingButton = true;
  331. pressTime = millis();
  332. lastActivity = millis(); //reset watchdog
  333. if (!pantallaOn){
  334. display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  335. pantallaOn = true;
  336. }
  337. bool timed_out = false;
  338. unsigned long checkTime = millis();
  339. while (digitalRead(BUTTON_PIN) == LOW){
  340. vTaskDelay(pdMS_TO_TICKS(10));
  341. checkTime = millis();
  342. if ((checkTime - pressTime) > DURACION_WATCHDOG_MS){ //10s timeout para evitar bloqueos
  343. timed_out = true;
  344. break;
  345. }
  346. drawProgressBar(0, SCREEN_HEIGHT - 10, SCREEN_WIDTH, 8, checkTime - pressTime, PULASCION_LARGA_MS);
  347. }
  348. ignore_isr_until = millis() + 500; //ignorar nuevas interrupciones durante 500 ms
  349. unsigned long duration = checkTime - pressTime;
  350. if (timed_out){
  351. OLED_print("Apagando","pantalla");
  352. display.ssd1306_command(SSD1306_DISPLAYOFF); //se apaga la
  353. pantallaOn = false;
  354. } else {
  355. if (grabando){
  356. if (duration >= PULASCION_LARGA_MS){
  357. grabando = false;
  358. vTaskSuspend(medicionesHandle);
  359. OLED_print("Ruta","pausada");
  360. } else {
  361. pantallaEstado_grab = (pantallaEstado_grab + 1) % 5; //cicla entre 0-4
  362. SensorData currentData;
  363. if(xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE){
  364. currentData = latestData;
  365. xSemaphoreGive(dataMutex);
  366. }
  367. switch (pantallaEstado_grab){
  368. case 0:
  369. OLED_print("Posicion",String(currentData.longitude) + "," + String(currentData.latitude));
  370. break;
  371. case 1:
  372. OLED_print("Distancia",String(distancia_total)+"km");
  373. break;
  374. case 2:
  375. OLED_print("Altitud",String(gps.altitude.meters(), 1)+"m");
  376. break;
  377. case 3:
  378. OLED_print("Temp/Hum",String(currentData.temperature,1)+"C/"+String(currentData.humidity,1)+"%");
  379. break;
  380. case 4:
  381. OLED_print("Velocidad",String(gps.speed.kmph())+"km/h");
  382. break;
  383. }
  384. }
  385. } else {
  386. if (duration >= PULASCION_LARGA_MS){
  387. switch (pantallaEstado_menu){
  388. case 0:
  389. //activar la ruta y crear el archivo
  390. crear_archivo();
  391. vTaskResume(medicionesHandle);
  392. OLED_print("Ruta","iniciada");
  393. finalizado = false;
  394. break;
  395. case 1:
  396. //cerrar el archivo y cambiar el valor de 'filename'
  397. cerrar_archivo();
  398. finalizado = true;
  399. break;
  400. case 2:
  401. //implementacion wifi
  402. if(!wifiActivado){
  403. activarWiFi();
  404. } else {
  405. desactivarWiFi();
  406. }
  407. break;
  408. }
  409. } else {
  410. pantallaEstado_menu = (pantallaEstado_menu + 1) % 3;
  411. switch (pantallaEstado_menu){
  412. case 0:
  413. if (!finalizado) OLED_print("Reanudar","ruta");
  414. else OLED_print("Iniciar","ruta");
  415. break;
  416. case 1:
  417. if (SD.exists(filename)) {
  418. OLED_print("Finalizar","ruta");
  419. break;
  420. }
  421. pantallaEstado_menu += 1;
  422. case 2:
  423. if (finalizado) {
  424. OLED_print("Conexion","wifi 'WIP'");
  425. break;
  426. }
  427. pantallaEstado_menu += 1;
  428. }
  429. }
  430. }
  431. }
  432. lastActivity = millis(); //reset watchdog
  433. processingButton = false;
  434. vTaskDelay(pdMS_TO_TICKS(100)); //pequeño delay para no busy waiting
  435. }
  436. //check watchdog fuera del boton
  437. if (pantallaOn && ((millis() - lastActivity) > DURACION_WATCHDOG_MS)){
  438. display.ssd1306_command(SSD1306_DISPLAYOFF); //se apaga la pantalla
  439. pantallaOn = false;
  440. }
  441. //check wifi timeout
  442. if (wifiActivado){
  443. server.handleClient();
  444. if (WiFi.softAPgetStationNum() > 0){
  445. wifiLastActivity = millis(); //reset si hay clientes conectados
  446. }
  447. if ((millis() - wifiLastActivity) > WIFI_TIMEOUT_MS){
  448. desactivarWiFi();
  449. }
  450. }
  451. }
  452. }
  453. void setup() {
  454. pinMode(BUTTON_PIN, INPUT_PULLUP);
  455. attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), isr_button, FALLING);
  456. buttonSemaphore = xSemaphoreCreateBinary();
  457. dataMutex = xSemaphoreCreateMutex();
  458. // OLED check
  459. OLED_test();
  460. delay(1000);
  461. // DHT check
  462. DHT_test();
  463. delay(1000);
  464. // SD Card check
  465. SD_test();
  466. delay(1000);
  467. // GPS check
  468. GPS_test_wait();
  469. delay(2000);
  470. // Crear tarea para mediciones
  471. xTaskCreatePinnedToCore(
  472. task_mediciones, // Función de la tarea
  473. "Mediciones", // Nombre de la tarea
  474. 8192, // Tamaño del stack
  475. NULL, // Parámetro de la tarea
  476. 10, // Prioridad de la tarea
  477. &medicionesHandle, // Handle de la tarea
  478. 0 // Núcleo donde se ejecuta
  479. );
  480. xTaskCreatePinnedToCore(
  481. task_ui, // Función de la tarea
  482. "UI", // Nombre de la tarea
  483. 8192, // Tamaño del stack
  484. NULL, // Parámetro de la tarea
  485. 5, // Prioridad de la tarea
  486. NULL, // Handle de la tarea
  487. 1 // Núcleo donde se ejecuta
  488. );
  489. vTaskSuspend(medicionesHandle); //inicia suspendida
  490. }
  491. void loop() {
  492. vTaskDelay(pdMS_TO_TICKS(1000)); // Espera 1 segundo
  493. }