TKK_E32232188/SmartCollar_Mikrokontroller/SmartCollar_Mikrokontroller...

623 lines
16 KiB
C++

/*
* ======================================================
* SMART COLLAR FIRMWARE
* ------------------------------------------------------
* Kirim data sensor (suhu, gerak, GPS, sinyal) ke backend
* tiap 1 detik. Prioritas kirim lewat SIM800L (GPRS),
* fallback otomatis ke WiFi kalau SIM800L gagal.
* ======================================================
*/
#define TINY_GSM_MODEM_SIM800
#include <Wire.h>
#include <Adafruit_MLX90614.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <TinyGPS++.h>
#include <HardwareSerial.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <time.h>
#include <sys/time.h>
#include <TinyGsmClient.h>
#include <ArduinoHttpClient.h>
// Install lewat Library Manager: "ArduinoJson" by Benoit Blanchon
#include <ArduinoJson.h>
// ======================================================
// PIN CONFIG
// ======================================================
#define SDA_PIN 21
#define SCL_PIN 22
#define GPS_RX 16
#define GPS_TX 17
#define SIM_RX 13
#define SIM_TX 12
// ======================================================
// WIFI CONFIG
// ======================================================
const char* WIFI_SSID = "oke";
const char* WIFI_PASSWORD = "12345678";
// ======================================================
// GPRS CONFIG
// ======================================================
const char APN[] = "internet";
// ======================================================
// IDENTITAS DEVICE
// Harus sama persis dengan yang terdaftar di backend.
// ======================================================
const char* SERIAL_NUMBER = "SC001";
const char* DEVICE_SECRET = "eefda1ae-ceb8-47da-b89e-c8be0d6543f2";
const char* FIRMWARE_VERSION = "1.0.0";
const char* HARDWARE_VERSION = "1.0.0";
// ======================================================
// BACKEND CONFIG
// ======================================================
const char* WIFI_BASE_URL = "https://api.smartcollar.my.id";
const char* MONITORING_PATH = "/api/v1/monitoring";
// Host SIM800L TANPA "http://"/"https://".
// Domain KHUSUS untuk jalur SIM800L (backend terpisah dari WiFi),
// jadi harus support plain HTTP di port 80 -- SIM800L via
// TinyGsmClient/HttpClient tidak bisa TLS/HTTPS.
const char GSM_SERVER[] = "sim.smartcollar.my.id";
const int GSM_PORT = 80;
// ======================================================
// KALIBRASI SENSOR
// ======================================================
const float TEMP_OFFSET = 2.0;
// ======================================================
// NTP CONFIG (fallback kalau device sedang pakai WiFi)
// ======================================================
const char* NTP_SERVER_1 = "pool.ntp.org";
const char* NTP_SERVER_2 = "time.google.com";
const long GMT_OFFSET_SEC = 7 * 3600; // WIB = UTC+7
const int DAYLIGHT_OFFSET_SEC = 0;
const unsigned long NTP_SYNC_TIMEOUT_MS = 10000;
// ======================================================
// TIMING
// ======================================================
const unsigned long SEND_INTERVAL_MS = 1000; // kirim tiap 1 detik
const unsigned long WIFI_CONNECT_TIMEOUT_MS = 15000;
const unsigned long WIFI_RECONNECT_COOLDOWN_MS = 5000;
// ======================================================
// GLOBAL OBJECTS
// ======================================================
Adafruit_MLX90614 mlx;
Adafruit_MPU6050 mpu;
TinyGPSPlus gps;
HardwareSerial SerialGPS(2);
HardwareSerial SerialSIM(1);
TinyGsm modem(SerialSIM);
TinyGsmClient gsmClient(modem);
HttpClient gsmHttp(gsmClient, GSM_SERVER, GSM_PORT);
const unsigned long WIFI_HTTP_TIMEOUT_MS = 10000;
// ======================================================
// GLOBAL STATE
// ======================================================
float baseAccelX = 0;
bool simConnected = false;
unsigned long lastSendMillis = 0;
unsigned long lastWifiReconnectAttempt = 0;
// ======================================================
// STRUCT DATA SENSOR (biar gampang dibaca & di-passing)
// ======================================================
struct SensorReading {
float temperature;
float accelX;
double latitude;
double longitude;
String linkMaps;
int signalStrength;
};
// ======================================================
// WIFI
// ======================================================
void connectWiFi() {
Serial.println();
Serial.println("CONNECT WIFI");
WiFi.mode(WIFI_STA);
WiFi.setSleep(false); // cegah ESP32 disconnect random akibat modem-sleep
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long startAttempt = millis();
while (WiFi.status() != WL_CONNECTED &&
millis() - startAttempt < WIFI_CONNECT_TIMEOUT_MS) {
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print("WIFI CONNECTED, IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("WIFI FAILED");
}
}
// Reconnect non-blocking, dipanggil tiap loop() supaya WiFi yang
// putus di tengah jalan langsung dicoba nyambung lagi di background
// tanpa nge-block pembacaan sensor/GPS.
void reconnectWiFiIfNeeded() {
if (WiFi.status() == WL_CONNECTED) {
return;
}
unsigned long now = millis();
if (now - lastWifiReconnectAttempt < WIFI_RECONNECT_COOLDOWN_MS) {
return;
}
lastWifiReconnectAttempt = now;
Serial.println();
Serial.println("WIFI DISCONNECTED -> RECONNECT (background)");
WiFi.reconnect();
}
// ======================================================
// SIM800L / GPRS
// ======================================================
bool connectSIM800() {
Serial.println();
Serial.println("INIT SIM800L");
SerialSIM.begin(9600, SERIAL_8N1, SIM_RX, SIM_TX);
delay(3000);
if (!modem.restart()) {
Serial.println("SIM800 NOT RESPOND");
return false;
}
Serial.println("SIM800 OK");
if (!modem.waitForNetwork()) {
Serial.println("NETWORK FAILED");
return false;
}
Serial.println("NETWORK OK");
if (!modem.gprsConnect(APN, "", "")) {
Serial.println("GPRS FAILED");
return false;
}
Serial.print("GPRS CONNECTED, IP: ");
Serial.println(modem.localIP());
return true;
}
void reconnectSIM800() {
if (!modem.isNetworkConnected()) {
Serial.println();
Serial.println("NETWORK LOST");
if (modem.waitForNetwork()) {
Serial.println("NETWORK RECONNECTED");
} else {
Serial.println("NETWORK FAILED");
}
}
if (!modem.isGprsConnected()) {
Serial.println();
Serial.println("GPRS LOST");
if (modem.gprsConnect(APN, "", "")) {
Serial.println("GPRS RECONNECTED");
simConnected = true;
} else {
Serial.println("GPRS FAILED");
simConnected = false;
}
}
}
// ======================================================
// SYNC WAKTU (biar field "uptime" isinya Unix timestamp asli,
// sama seperti Date.now() di simulator -- bukan sekadar detik
// sejak boot / millis())
//
// CATATAN: sync waktu dari modem (AT+CCLK) SEMENTARA DINONAKTIFKAN
// karena gak semua provider SIM support & berisiko bikin proses
// nyangkut kalau modem/network gak respon sesuai harapan. Uptime
// akan fallback ke millis() kalau device cuma pakai SIM800L tanpa
// WiFi sama sekali -- gak ideal, tapi jauh lebih aman/stabil dulu
// sampai koneksi SIM800L ke backend beres.
// ======================================================
// Sync lewat NTP (butuh WiFi aktif)
bool syncTimeFromNTP() {
Serial.println();
Serial.println("SYNC WAKTU (NTP)...");
configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER_1, NTP_SERVER_2);
unsigned long startAttempt = millis();
time_t now = time(nullptr);
while (now < 1700000000 && millis() - startAttempt < NTP_SYNC_TIMEOUT_MS) {
delay(300);
Serial.print(".");
now = time(nullptr);
}
Serial.println();
if (now >= 1700000000) {
Serial.print("WAKTU TERSINKRON DARI NTP: ");
Serial.println(now);
return true;
}
Serial.println("NTP SYNC GAGAL");
return false;
}
// Coba sync waktu lewat NTP kalau WiFi aktif. Kalau device murni
// pakai SIM800L tanpa WiFi, uptime fallback ke millis() (lihat
// getUnixTimestamp() di bawah).
void syncTimeIfPossible() {
if (WiFi.status() == WL_CONNECTED) {
syncTimeFromNTP();
} else {
Serial.println();
Serial.println("WIFI TIDAK AKTIF -- uptime pakai fallback millis()");
}
}
// Ambil Unix timestamp (detik sejak epoch), sama format dengan
// Math.floor(Date.now() / 1000) di simulator.
unsigned long getUnixTimestamp() {
time_t now = time(nullptr);
if (now >= 1700000000) {
return (unsigned long)now;
}
// Fallback kalau waktu belum/gagal sync sama sekali
return millis() / 1000;
}
// ======================================================
// SENSOR SETUP
// ======================================================
void setupMPU() {
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
}
void calibrateMPU() {
sensors_event_t a, g, t;
mpu.getEvent(&a, &g, &t);
baseAccelX = a.acceleration.x;
}
// ======================================================
// SIGNAL STRENGTH
// ======================================================
int getSignalStrength() {
if (WiFi.status() == WL_CONNECTED) {
return WiFi.RSSI();
}
if (simConnected) {
int csq = modem.getSignalQuality();
return (csq == 99) ? -113 : (-113 + csq * 2);
}
return 0;
}
// ======================================================
// BACA SEMUA SENSOR
// ======================================================
SensorReading readSensors() {
SensorReading reading;
// Suhu
float rawTemp = mlx.readObjectTempC();
reading.temperature = isnan(rawTemp) ? 0 : rawTemp + TEMP_OFFSET;
// Gerak (accelX mentah, status dihitung di backend)
sensors_event_t a, g, t;
mpu.getEvent(&a, &g, &t);
reading.accelX = a.acceleration.x - baseAccelX;
// GPS
reading.latitude = 0;
reading.longitude = 0;
reading.linkMaps = "-";
if (gps.location.isValid()) {
reading.latitude = gps.location.lat();
reading.longitude = gps.location.lng();
reading.linkMaps = "https://www.google.com/maps?q=" +
String(reading.latitude, 6) + "," +
String(reading.longitude, 6);
}
// Sinyal
reading.signalStrength = getSignalStrength();
return reading;
}
// ======================================================
// BANGUN PAYLOAD JSON
// Struktur & nama field disamakan persis dengan simulator.
// ======================================================
String buildPayload(const SensorReading& reading) {
StaticJsonDocument<512> doc;
doc["serialNumber"] = SERIAL_NUMBER;
doc["deviceSecret"] = DEVICE_SECRET;
JsonObject device = doc.createNestedObject("device");
device["signal"] = reading.signalStrength;
device["firmwareVersion"] = FIRMWARE_VERSION;
device["hardwareVersion"] = HARDWARE_VERSION;
device["uptime"] = getUnixTimestamp();
JsonObject sensor = doc.createNestedObject("sensor");
sensor["temperature"] = round(reading.temperature * 100) / 100.0;
JsonObject gpsObj = sensor.createNestedObject("gps");
gpsObj["latitude"] = reading.latitude;
gpsObj["longitude"] = reading.longitude;
gpsObj["linkMaps"] = reading.linkMaps;
JsonObject movement = sensor.createNestedObject("movement");
movement["accelX"] = round(reading.accelX * 100) / 100.0;
String payload;
serializeJson(doc, payload);
if (doc.overflowed()) {
Serial.println();
Serial.println("WARNING: JSON BUFFER OVERFLOW! Payload mungkin kepotong, perbesar StaticJsonDocument.");
}
return payload;
}
// ======================================================
// KIRIM VIA SIM800L
// return true kalau berhasil (statusCode > 0)
// ======================================================
bool sendViaSIM800(const String& payload) {
Serial.println();
Serial.println("---- KIRIM VIA SIM800L ----");
Serial.print("Host : ");
Serial.println(GSM_SERVER);
Serial.print("Path : ");
Serial.println(MONITORING_PATH);
Serial.print("Payload: ");
Serial.println(payload);
gsmHttp.beginRequest();
gsmHttp.post(MONITORING_PATH);
gsmHttp.sendHeader("Content-Type", "application/json");
gsmHttp.sendHeader("Content-Length", payload.length());
gsmHttp.beginBody();
gsmHttp.print(payload);
gsmHttp.endRequest();
int statusCode = gsmHttp.responseStatusCode();
String response = gsmHttp.responseBody();
Serial.print("Status Code : ");
Serial.println(statusCode);
Serial.print("Response : ");
Serial.println(response.length() > 0 ? response : "(kosong)");
return statusCode > 0;
}
// ======================================================
// KIRIM VIA WIFI (HTTPS)
// ======================================================
bool sendViaWiFi(const String& payload) {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println();
Serial.println("NO INTERNET (WiFi gagal connect)");
return false;
}
String url = String(WIFI_BASE_URL) + String(MONITORING_PATH);
Serial.println();
Serial.println("---- KIRIM VIA WIFI ----");
Serial.print("URL : ");
Serial.println(url);
Serial.print("Payload: ");
Serial.println(payload);
// ESP32 HTTPClient wajib pakai WiFiClientSecure untuk https,
// kalau tidak, request bisa gagal diam-diam (httpCode negatif).
WiFiClientSecure secureClient;
secureClient.setInsecure();
secureClient.setTimeout(WIFI_HTTP_TIMEOUT_MS);
HTTPClient http;
http.setTimeout(WIFI_HTTP_TIMEOUT_MS);
http.setConnectTimeout(WIFI_HTTP_TIMEOUT_MS);
http.begin(secureClient, url);
http.addHeader("Content-Type", "application/json");
int httpCode = http.POST(payload);
Serial.print("HTTP Code : ");
Serial.println(httpCode);
bool success = httpCode > 0 && httpCode < 400;
if (httpCode > 0) {
String body = http.getString();
Serial.print("Response : ");
Serial.println(body.length() > 0 ? body : "(kosong)");
} else {
Serial.print("HTTP Error: ");
Serial.println(http.errorToString(httpCode));
}
http.end();
return success;
}
// ======================================================
// KIRIM DATA — PRIORITAS SIM800L, FALLBACK WIFI
// ======================================================
void sendToBackend(const String& payload) {
bool success = false;
if (simConnected) {
success = sendViaSIM800(payload);
if (!success) {
Serial.println();
Serial.println("SIM800L GAGAL -> COBA WIFI");
}
}
if (!success) {
success = sendViaWiFi(payload);
}
Serial.println();
if (success) {
Serial.println("===== DATA BERHASIL TERKIRIM =====");
} else {
Serial.println("===== DATA GAGAL TERKIRIM (cek koneksi/backend) =====");
}
}
// ======================================================
// SETUP
// ======================================================
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println();
Serial.println("SMART COLLAR START");
Serial.print("Serial Number : ");
Serial.println(SERIAL_NUMBER);
// Koneksi: prioritas SIM800L, fallback WiFi
simConnected = connectSIM800();
if (!simConnected) {
connectWiFi();
}
syncTimeIfPossible();
// I2C + sensor
Wire.begin(SDA_PIN, SCL_PIN);
Serial.println(mlx.begin() ? "MLX OK" : "MLX FAILED");
if (mpu.begin()) {
Serial.println("MPU OK");
setupMPU();
calibrateMPU();
} else {
Serial.println("MPU FAILED");
}
// GPS
SerialGPS.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
Serial.println("GPS READY");
}
// ======================================================
// LOOP
// ======================================================
void loop() {
reconnectSIM800();
if (!simConnected) {
reconnectWiFiIfNeeded();
}
// Update data GPS terus-menerus (non-blocking)
while (SerialGPS.available()) {
gps.encode(SerialGPS.read());
}
unsigned long currentMillis = millis();
if (currentMillis - lastSendMillis < SEND_INTERVAL_MS) {
return;
}
lastSendMillis = currentMillis;
SensorReading reading = readSensors();
String payload = buildPayload(reading);
Serial.println();
Serial.println(payload);
sendToBackend(payload);
}