623 lines
16 KiB
C++
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);
|
|
} |