/* * ====================================================== * 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 #include #include #include #include #include #include #include #include #include #include #include #include // Install lewat Library Manager: "ArduinoJson" by Benoit Blanchon #include // ====================================================== // 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); }