// === Бібліотеки ===
#include <Wire.h> // Підключення бібліотеки для протоколу I2C
#include <Adafruit_GFX.h> // Підключення графічної бібліотеки Adafruit
#include <Adafruit_SSD1306.h> // Підключення бібліотеки для дисплею SSD1306
#include <SPI.h> // Підключення бібліотеки для протоколу SPI
#include <SD.h> // Підключення бібліотеки для роботи з SD картою
#include <TinyGPS++.h> // Підключення бібліотеки для парсингу GPS даних
#include <WiFi.h> // Підключення бібліотеки WiFi для IoT
#include <HTTPClient.h> // Підключення клієнта HTTP для ThingSpeak
// === Конфігурація ===
#define SCREEN_WIDTH 128 // Ширина екрану в пікселях
#define SCREEN_HEIGHT 64 // Висота екрану в пікселях
#define OLED_RESET -1 // Пін скидання
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Створення об'єкту дисплею
// === Апаратні додатки ===
#define LED_PIN 4 // Пін для червоного світлодіода
#define BUZZER_PIN 15 // Пін для п'єзодинаміка
// === IoT Налаштування ===
const char* WIFI_SSID = "Wokwi-GUEST"; // Назва WiFi мережі
const char* WIFI_PASS = ""; // Пароль WiFi мережі
const char* TS_API_KEY = "PJG2U38CSHLP8TLO"; // Ваш Write API Key з ThingSpeak
const char* TS_SERVER = "http://api.thingspeak.com/update"; // Адреса серверу ThingSpeak
// === GPS Налаштування ===
#define GPS_RX_PIN 16 // Пін RX ESP32 підключений до TX GPS
#define GPS_TX_PIN 17 // Пін TX ESP32 підключений до RX GPS
#define GPS_BAUD 9600 // Швидкість передачі даних GPS модулем
HardwareSerial gpsSerial(2); // Використання апаратного Serial2
TinyGPSPlus gps; // Створення об'єкту для обробки GPS
// === НАЛАШТУВАННЯ СИМУЛЯЦІЇ ===
#define SIMULATION_MODE // Розкоментуйте цей рядок для симуляції
// // Закоментуйте цей рядок щоб бачити реальні дані
// === НАЛАШТУВАННЯ ДЕБАГУ ===
#define DEBUG_MODE // Розкоментуйте для детального виводу в Serial Monitor
//#define DISABLE_GPS_SERIAL // Розкоментуйте щоб вимкнути GPS Serial для тестування
// === SD Карта ===
#define SD_CS_PIN 5 // Пін Chip Select для SD карти
// === Логіка та Метрики ===
File logFile; // Об'єкт файлу для логування
double lastLat = 0.0, lastLon = 0.0; // Змінні для збереження попередніх координат
unsigned long lastLogTime = 0; // Час останнього запису в лог
unsigned long lastDisplayTime = 0; // Час останнього оновлення дисплею
unsigned long lastBlinkTime = 0; // Час останнього перемикання LED
unsigned long lastCloudTime = 0; // Час останньої відправки в хмару
unsigned long lastMetricsTime = 0; // Час останнього виводу метрик
unsigned long totalLogAttempts = 0; // Загальна кількість спроб запису
unsigned long actualLogsMade = 0; // Фактична кількість записів
// === MVP Оптимізація ===
const unsigned long LOG_INTERVAL = 5000; // Інтервал спроби запису
const double MIN_DISTANCE_METERS = 10.0; // Поріг Smart Logging
const unsigned long DISPLAY_INTERVAL = 1000; // Частота оновлення екрану
const unsigned long BLINK_INTERVAL = 1000; // Період блимання LED у нормальному стані
const unsigned long CLOUD_INTERVAL = 20000; // Інтервал відправки на ThingSpeak
const unsigned long METRICS_INTERVAL = 60000; // Інтервал виводу метрик
// === Допоміжні функції логування ===
String getTimestamp() { // Отримання мітки часу
char buf[10];
if (gps.time.isValid()) { // Якщо час від GPS доступний
sprintf(buf, "%02d:%02d:%02d", gps.time.hour(), gps.time.minute(), gps.time.second());
} else { // Якщо немає GPS часу, використовуємо uptime
unsigned long s = millis() / 1000;
sprintf(buf, "%02d:%02d:%02d", (int)(s/3600)%24, (int)(s/60)%60, (int)s%60); // Форматування часу в буфер
}
return String(buf); // Повернення мітки часу
}
void logJournal(String module, String message) { // Структурований вивід у журнал
#ifdef DEBUG_MODE
String logEntry = "[ЖУРНАЛ] [" + getTimestamp() + "] [" + module + "] " + message;
Serial.println(logEntry); // Вивід у Serial Monitor
// Дублювання логу на SD карту
File sysLog = SD.open("/system.log", FILE_APPEND); // Відкриття файлу системи
if (sysLog) { // Якщо файл відкрито
sysLog.println(logEntry); // Запис події
sysLog.close(); // Закриття
}
#endif
}
void printMetrics() { // Вивід метрик логування
float optimization = 0; // Відсоток оптимізації
if (totalLogAttempts > 0) { // Якщо були спроби логування
optimization = ((float)(totalLogAttempts - actualLogsMade) / totalLogAttempts) * 100.0; // Розрахунок економії місця
}
Serial.println("\n====== МЕТРИКИ ЛОГУВАННЯ ======"); // Заголовок таблиці
Serial.print("Записів без фільтрації: "); Serial.println(totalLogAttempts); // Спроби без Smart Logging
Serial.print("Записів з Smart Logging: "); Serial.println(actualLogsMade); // Реальні записи на SD
Serial.print("Оптимізація: -"); Serial.print(optimization, 1); Serial.println(" %"); // Ефективність алгоритму
Serial.println("==============================\n"); // Кінець таблиці
}
void setup() { // Функція налаштування
Serial.begin(115200); // Запуск серійного порту для дебагу
delay(1000); // Затримка для стабілізації Serial
Serial.println("\n=================================");
Serial.println(" GPS Tracker BOOTING... ");
Serial.println("=================================");
logJournal("СИСТЕМА", "Старт системи GPS Tracker"); // Лог старту системи
// Налаштування пінів
pinMode(LED_PIN, OUTPUT); // Налаштування піна LED на вихід
pinMode(BUZZER_PIN, OUTPUT); // Налаштування піна Buzzer на вихід
// Звукова індикація запуску
tone(BUZZER_PIN, 1000); // Запуск звуку 1000Гц
delay(10); // Мінімальний клік 10мс
noTone(BUZZER_PIN); // Примусова зупинка звуку
// Налаштування OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Ініціалізація дисплею за адресою 0x3C
logJournal("СИСТЕМА", "Помилка ініціалізації дисплею"); // Лог помилки дисплею
while(true); // Зупинка при помилці
}
logJournal("СИСТЕМА", "Дисплей ініціалізовано успішно"); // Лог успіху дисплею
display.clearDisplay(); // Очистка буферу дисплею
display.setTextColor(WHITE); // Встановлення білого кольору тексту
display.setCursor(0,0); // Встановлення курсору в початок
display.println("Connecting WiFi..."); // Повідомлення про підключення WiFi
display.display(); // Оновлення екрану
// Підключення WiFi
WiFi.begin(WIFI_SSID, WIFI_PASS); // Старт підключення
logJournal("WiFi", "Підключення до WiFi..."); // Лог спроби підключення
int wifi_timeout = 0; // Лічильник тайм-ауту
while (WiFi.status() != WL_CONNECTED && wifi_timeout < 20) { // Чекаємо підключення макс 10с
delay(500); // Пауза 0.5с
Serial.print("."); // Статус в консоль
wifi_timeout++; // Інкремент лічильника
}
display.clearDisplay(); // Очистка екрану
display.setCursor(0,0); // Курсор в початок
if (WiFi.status() == WL_CONNECTED) { // Якщо підключено
logJournal("WiFi", "Підключення успішне. IP: " + WiFi.localIP().toString()); // Лог успішного підключення
display.println("WiFi: OK"); // Успіх на екран
display.println(WiFi.localIP()); // Вивід IP адреси
} else { // Якщо помилка
logJournal("WiFi", "Помилка підключення WiFi"); // Лог помилки WiFi
display.println("WiFi: FAIL"); // Помилка на екран
}
display.display(); // Оновлення екрану
delay(1000); // Пауза щоб прочитати IP
// Налаштування SD карти
if (!SD.begin(SD_CS_PIN)) { // Спроба ініціалізації SD карти
logJournal("SD", "Помилка SD карти"); // Лог помилки SD
} else { // Якщо карта знайдена
logJournal("SD", "SD карта готова до роботи"); // Лог успіху SD
logFile = SD.open("/gpslog.csv", FILE_APPEND); // Відкриття файлу
if (logFile) { // Якщо відкрито
if (logFile.size() == 0) { // Заголовок якщо файл новий
logFile.println("Date,Time,Lat,Lon,Speed,Alt,Sats,Heap,RSSI,Dist"); // Розширений заголовок
}
logFile.close(); // Закриття
}
}
// Налаштування GPS
#ifndef DISABLE_GPS_SERIAL
gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); // Запуск Serial для GPS
logJournal("GPS", "Модуль GPS активовано"); // Лог активації GPS
#else
logJournal("GPS", "Модуль GPS ВИМКНЕНО для тестування Serial Monitor"); // Лог що GPS вимкнено
#endif
logJournal("СИСТЕМА", "Ініціалізація завершена. Готовність до роботи."); // Лог готовності
Serial.println("=================================");
Serial.println(" SYSTEM READY - STARTING LOGS ");
Serial.println("=================================\n");
delay(100); // Пауза
}
void loop() { // Основний цикл програми
// Отримання даних GPS
#ifndef DISABLE_GPS_SERIAL
while (gpsSerial.available() > 0) {
char c = gpsSerial.read();
gps.encode(c);
// Опціонально: виводити сирі GPS дані (розкоментуйте для дебагу GPS)
// Serial.print(c);
}
#endif
// Індикація роботи LED
if (millis() - lastBlinkTime > BLINK_INTERVAL) { // Таймер блимання
lastBlinkTime = millis(); // Оновлення часу
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Інверсія LED
#ifdef DEBUG_MODE
Serial.print("."); // Пульс у консоль для перевірки зв'язку
#endif
}
// Відправка в хмару ThingSpeak
if (millis() - lastCloudTime > CLOUD_INTERVAL) { // Таймер хмари
lastCloudTime = millis(); // Оновлення часу
sendToThingSpeak(); // Виклик функції відправки
}
// Оновлення дисплею
if (millis() - lastDisplayTime > DISPLAY_INTERVAL) { // Таймер дисплею
lastDisplayTime = millis(); // Оновлення часу
updateDisplay(); // Оновлення екрану
processSmartLogging(); // Логування на SD
}
// Вивід метрик
if (millis() - lastMetricsTime > METRICS_INTERVAL) { // Таймер метрик
lastMetricsTime = millis(); // Оновлення часу
printMetrics(); // Виклик функції метрик
}
}
void sendToThingSpeak() { // Функція відправки на ThingSpeak
if (WiFi.status() == WL_CONNECTED) { // Якщо є WiFi
HTTPClient http; // Створення клієнта
double lat = gps.location.isValid() ? gps.location.lat() : 0.0; // Отримання широти
double lon = gps.location.isValid() ? gps.location.lng() : 0.0; // Отримання довготи
int sats = gps.satellites.value(); // Супутники
#ifdef SIMULATION_MODE // Якщо симуляція
if (lat == 0.0) { lat = 50.4501; lon = 30.5234; sats = 14; } // Фейкові координати Києва
#endif
// Формування URL запиту
String url = String(TS_SERVER) + "?api_key=" + TS_API_KEY; // Базовий URL
url += "&field1=" + String(lat, 6); // Поле 1: Широта
url += "&field2=" + String(lon, 6); // Поле 2: Довгота
url += "&field3=" + String(sats); // Поле 3: Супутники
url += "&field4=" + String(ESP.getFreeHeap()); // Поле 4: Пам'ять
http.begin(url); // Старт з'єднання
int httpCode = http.GET(); // Виконання GET запиту
if (httpCode > 0) { // Якщо код > 0
logJournal("ThingSpeak", "Відправка успішна. Код: " + String(httpCode)); // Лог підтвердження
String details = "Lat:" + String(lat,4) + " Lon:" + String(lon,4) +
" Sats:" + String(sats) + " FreeHeap:" + String(ESP.getFreeHeap());
logJournal("МЕТРИКИ", details); // Лог метрик
// Звукове підтвердження
tone(BUZZER_PIN, 2000, 50); // Короткий писк успіху
} else { // Якщо помилка
logJournal("ThingSpeak", "Помилка відправки: " + http.errorToString(httpCode)); // Лог помилки
}
http.end(); // Закриття з'єднання
}
}
void updateDisplay() { // Функція малювання інтерфейсу
display.clearDisplay(); // Очистка екрану
display.setCursor(0, 0); // Курсор зверху
display.println("ESP32 IoT Tracker"); // Назва проекту
display.drawLine(0, 9, 128, 9, WHITE); // Лінія
display.setCursor(0, 12); // Курсор даних
if (gps.location.isValid()) { // Якщо координати є
display.print("Lat: "); display.println(gps.location.lat(), 4); // Широта
display.print("Lon: "); display.println(gps.location.lng(), 4); // Довгота
display.print("Sats: "); display.println(gps.satellites.value()); // Супутники
} else { // Якщо немає
#ifdef SIMULATION_MODE // Симуляція
display.println("Simulating GPS..."); // Повідомлення
display.println("Lat: 50.4501"); // Фейк широта
display.println("Lon: 30.5234"); // Фейк довгота
#else // Реал
display.println("Waiting for Fix..."); // Очікування
#endif
}
// Статус WiFi внизу
display.setCursor(0, 50); // Курсор низ
if(WiFi.status() == WL_CONNECTED) display.print("WiFi: ON"); // WiFi є
else display.print("WiFi: OFF"); // WiFi немає
display.display(); // Оновлення пікселів
} // Кінець функції updateDisplay
void processSmartLogging() { // Функція запису на SD
if (!gps.location.isValid()) return; // Вихід якщо немає координат
totalLogAttempts++; // Кожна спроба логування
// Розрахунок дистанції для Smart Logging
double distance = TinyGPSPlus::distanceBetween( // Функція розрахунку відстані
gps.location.lat(), gps.location.lng(), // Поточні координати
lastLat, lastLon // Попередні координати
);
if (distance < MIN_DISTANCE_METERS && actualLogsMade > 0) { // Якщо рух незначний
return; // Пропускаємо запис
}
// Якщо дистанція достатня або це перший запис
logFile = SD.open("/gpslog.csv", FILE_APPEND); // Відкриття
if (logFile) { // Успіх
actualLogsMade++; // Фактичний запис
lastLat = gps.location.lat(); // Збереження поточної широти
lastLon = gps.location.lng(); // Збереження поточної довготи
digitalWrite(LED_PIN, HIGH); delay(20); digitalWrite(LED_PIN, LOW); // Швидкий блік
logFile.print(gps.date.year()); logFile.print("-"); // Рік
logFile.print(gps.date.month()); logFile.print("-"); // Місяць
logFile.print(gps.date.day()); logFile.print(","); // День
logFile.print(gps.time.hour()); logFile.print(":"); // Година
logFile.print(gps.time.minute()); logFile.print(":"); // Хвилина
logFile.print(gps.time.second()); logFile.print(","); // Секунда
logFile.print(gps.location.lat(), 6); logFile.print(","); // Координата широти
logFile.print(gps.location.lng(), 6); logFile.print(","); // Координата довготи
logFile.print(gps.speed.kmph()); logFile.print(","); // Швидкість км/год
logFile.print(gps.altitude.meters()); logFile.print(","); // Висота над рівнем моря
logFile.print(gps.satellites.value()); logFile.print(","); // Кількість супутників
logFile.print(ESP.getFreeHeap()); logFile.print(","); // Вільна пам'ять
logFile.print(WiFi.RSSI()); logFile.print(","); // Рівень сигналу WiFi
logFile.println(distance); // Дистанція від попередньої точки
logFile.close(); // Закриття
}
}