// ===================== KONFIGURASI BLYNK =====================
#define BLYNK_TEMPLATE_ID "TMPL6NypHt4sx"
#define BLYNK_TEMPLATE_NAME "SWARFS"
#define BLYNK_PRINT Serial
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <BlynkSimpleEsp32.h>
#include "time.h"
#include <ArduinoJson.h>
#include <WebServer.h>
#include <Preferences.h>
#include <vector>
#include <ESPmDNS.h>
#include "LittleFS.h"
// Pin ESP32-S3 (Khusus Hardware SWARFS)
static const int ledPin = 6;
static const int triggerPin = 4;
static const int echoPin = 5;
// Variabel Konfigurasi Dinamis
Preferences preferences;
WebServer server(80);
bool ledBurstActive = false;
int ledBurstStep = 0;
unsigned long ledTimer = 0;
String conf_ssid, conf_pass;
String conf_blynk_id, conf_blynk_name, conf_blynk_token;
String conf_gs_url, conf_api_server_ip;
String conf_device_id, conf_nama_titik, conf_psm, conf_estate, conf_divisi, conf_blok;
String conf_latitude, conf_longitude;
// ===================== Variabel Sensor & Interval =====================
unsigned long intervalMs = 60000;
float TinggiSensor = 400;
float ElevasiDasar = 4000;
float levelAmanMax = 200;
float levelWaspadaMax = 300;
unsigned long periodeRata2 = 60;
float sensorOffset = 0.0;
char bmkgAdm2[10] = "33.17"; // Default Kabupaten Rembang
char cuacaSekarang[80] = "-";
float emaValue = 0.0;
bool emaInitialized = false;
float emaAlpha = 0.3;
std::vector<float> tinggiSamples;
// ===================== Format Data Buffer =====================
struct DataBuffer {
String device_id;
String nama_titik;
String psm;
String estate;
String divisi;
String blok;
String status;
String log_lat;
String log_long;
float tinggi_muka_air;
float elevasi_muka_air;
float jarak;
float kecepatan;
String kategori_perubahan;
String time_forecast;
String created_at;
};
// ===================== Timer & Flags =====================
unsigned long lastSensorRead = 0;
unsigned long lastWifiRetry = 0;
unsigned long lastBmkgSync = 0;
static const unsigned long wifiRetryInterval = 30000;
bool configUpdated = false;
unsigned long lastConfigUpdate = 0;
bool perluRestart = false;
unsigned long waktuRestart = 0;
// Flag pemicu update BMKG dari Blynk agar tidak mengganggu jalannya setup
bool perluUpdateBmkg = false;
// ===================== Variabel Global Biasa =====================
float Jarak = 0, TinggiAir = 0, Elevasi = 0;
static const char* ntpServer = "pool.ntp.org";
static const long gmtOffset_sec = 25200;
static const int daylightOffset_sec = 0;
unsigned int dataCounter = 0;
// ===================== DESAIN HALAMAN WEB PORTAL HTML =====================
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Low-Cost Water Level</title>
<style>
body { font-family: sans-serif; padding: 20px; background: #f4f4f4; }
.card { background: white; padding: 15px; margin-bottom: 15px; border-radius: 8px; }
input { width: 100%; padding: 8px; margin: 5px 0; box-sizing: border-box; }
button { width: 100%; padding: 12px; background: #2e7d32; color: white; border: none; font-weight: bold; border-radius: 4px; }
</style>
</head>
<body>
<form action="/save" method="POST">
<div class="card">
<h3>1. WiFi</h3>
<input name="ssid" placeholder="SSID"><input name="pass" type="password" placeholder="Password">
</div>
<div class="card">
<h3>2. Blynk</h3>
<input name="blynk_id" placeholder="Template ID"><input name="blynk_name" placeholder="Template Name"><input name="blynk_token" placeholder="Auth Token">
</div>
<div class="card">
<h3>3. Cloud & Server</h3>
<input name="gs_url" placeholder="Google Sheet Script URL">
<input name="api_server_ip" placeholder="API Server URL/IP">
</div>
<div class="card">
<h3>4. Identity</h3>
<input name="device_id" placeholder="Device ID"><input name="nama_titik" placeholder="Nama Titik">
<input name="psm" placeholder="PSM"><input name="estate" placeholder="Estate">
<input name="divisi" placeholder="Divisi"><input name="blok" placeholder="Blok">
</div>
<div class="card">
<h3>5. Location</h3>
<input name="lat" type="text" placeholder="Latitude (Contoh: -6.712345)">
<input name="long" type="text" placeholder="Longitude (Contoh: 111.345678)">
</div>
<button type="submit">SAVE & RESTART</button>
</form>
</body>
</html>
)rawliteral";
void handleRoot() {
server.send_P(200, "text/html", index_html);
}
void loadConfig();
void handleSave() {
preferences.begin("swarfs", false);
preferences.putString("ssid", server.arg("ssid"));
preferences.putString("pass", server.arg("pass"));
preferences.putString("blynk_id", server.arg("blynk_id"));
preferences.putString("blynk_name", server.arg("blynk_name"));
preferences.putString("blynk_token", server.arg("blynk_token"));
preferences.putString("gs_url", server.arg("gs_url"));
preferences.putString("api_server", server.arg("api_server_ip"));
preferences.putString("dev_id", server.arg("device_id"));
preferences.putString("titik", server.arg("nama_titik"));
preferences.putString("psm", server.arg("psm"));
preferences.putString("estate", server.arg("estate"));
preferences.putString("divisi", server.arg("divisi"));
preferences.putString("blok", server.arg("blok"));
// PERBAIKAN BERSIHKAN KOORDINAT SPASI & NEWLINE KARENA COPY PASTE
String latInput = server.arg("lat");
String longInput = server.arg("long");
latInput.trim();
longInput.trim();
preferences.putString("lat", latInput);
preferences.putString("long", longInput);
preferences.end();
loadConfig();
server.send(200, "text/html", "<h2>Konfigurasi Tersimpan!</h2><p>Alat akan restart otomatis...</p>");
perluRestart = true;
waktuRestart = millis();
}
void loadConfig() {
preferences.begin("swarfs", true);
conf_ssid = preferences.getString("ssid", "");
conf_pass = preferences.getString("pass", "");
conf_blynk_id = preferences.getString("blynk_id", "");
conf_blynk_name = preferences.getString("blynk_name", "");
conf_blynk_token = preferences.getString("blynk_token", "");
conf_gs_url = preferences.getString("gs_url", "");
conf_api_server_ip = preferences.getString("api_server", "");
conf_device_id = preferences.getString("dev_id", "");
conf_nama_titik = preferences.getString("titik", "");
conf_psm = preferences.getString("psm", "");
conf_estate = preferences.getString("estate", "");
conf_divisi = preferences.getString("divisi", "");
conf_blok = preferences.getString("blok", "");
conf_latitude = preferences.getString("lat", "");
conf_longitude = preferences.getString("long", "");
intervalMs = preferences.getULong("interval", 60000);
TinggiSensor = preferences.getFloat("t_sensor", 400.0);
ElevasiDasar = preferences.getFloat("el_dasar", 4000.0);
levelAmanMax = preferences.getFloat("lv_aman", 200.0);
levelWaspadaMax = preferences.getFloat("lv_waspada", 300.0);
periodeRata2 = preferences.getULong("p_rata", 60);
sensorOffset = preferences.getFloat("offset", 0.0);
String savedAdm2 = preferences.getString("adm2", "33.17");
savedAdm2.toCharArray(bmkgAdm2, sizeof(bmkgAdm2));
preferences.end();
float approxSamples = max(1.0f, (float)periodeRata2 / ((float)intervalMs / 1000.0f / 60.0f));
emaAlpha = 2.0f / (approxSamples + 1.0f);
if(emaAlpha < 0.01f) emaAlpha = 0.01f; if(emaAlpha > 0.9f) emaAlpha = 0.9f;
}
void tampilkanNilaiKonfigurasi() {
Serial.println(F("\n=== Nilai Konfigurasi (Terbaru) ==="));
Serial.printf("Device ID : %s | Estate: %s | PSM: %s\n", conf_device_id.length() > 0 ? conf_device_id.c_str() : "BELUM DIATUR", conf_estate.c_str(), conf_psm.c_str());
Serial.printf("Koordinat : %s, %s\n", conf_latitude.c_str(), conf_longitude.c_str());
Serial.printf("Tinggi Sensor: %.2f cm | Elevasi Dasar: %.2f cm | Offset: %.2f cm\n", TinggiSensor, ElevasiDasar, sensorOffset);
Serial.printf("Interval Data: %lu ms | Periode Rata2: %lu m\n", intervalMs, periodeRata2);
Serial.println(F("===================================\n"));
}
void bukaWebPortal() {
Serial.println(F("\n[+] Mengaktifkan Pancaran AP Web Portal Mandiri (Tanpa Captive)."));
WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
WiFi.softAP("Low-Cost Water Level", "PMNPB2025-2026", 6, 0, 4);
if (MDNS.begin("lcwl")) { Serial.println(F("[+] mDNS aktif. Akses melalui: http://lcwl.local")); }
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.onNotFound([]() {
server.send(404, "text/plain", "404: Not Found. Silakan akses ke http://192.168.4.1 atau http://lcwl.local");
});
server.begin();
}
void ambilCuacaBMKG_ADM2() {
if (WiFi.status() != WL_CONNECTED) return;
String adm2Str = String(bmkgAdm2);
adm2Str.trim();
if (adm2Str.length() < 4) {
Serial.println(F("[BMKG] Gagal: ID ADM2 kosong atau tidak valid."));
return;
}
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(4000);
client.setHandshakeTimeout(4000);
HTTPClient http;
String url = "https://api.bmkg.go.id/publik/prakiraan-cuaca?adm2=" + adm2Str;
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
http.setTimeout(4000);
if (http.begin(client, url)) {
int httpCode = http.GET();
if (httpCode == 200) {
JsonDocument filter;
filter["data"][0]["prakiraan"]["cuaca"][0]["weather_desc"] = true;
filter["data"][0]["prakiraan"]["cuaca"][0]["t"] = true;
filter["data"][0]["prakiraan"]["cuaca"][0]["hu"] = true;
WiFiClient* stream = http.getStreamPtr();
JsonDocument doc;
DeserializationError err = deserializeJson(doc, *stream, DeserializationOption::Filter(filter));
if (!err) {
JsonArray dataArr = doc["data"];
if (dataArr.size() > 0) {
JsonObject prakiraanObj = dataArr[0]["prakiraan"];
JsonArray cuacaArr = prakiraanObj["cuaca"];
if (cuacaArr.size() > 0) {
JsonObject now = cuacaArr[0];
String kondisi = now["weather_desc"] | "-";
String temperatur = now["t"] | "-";
String kelembaban = now["hu"] | "-";
if (kondisi != "-") {
String hasil = kondisi + " | " + temperatur + "°C | " + kelembaban + "%";
hasil.toCharArray(cuacaSekarang, sizeof(cuacaSekarang));
Serial.printf("[BMKG] Sukses Update Wilayah %s: %s\n", adm2Str.c_str(), cuacaSekarang);
if (WiFi.status() == WL_CONNECTED && Blynk.connected()) {
Blynk.virtualWrite(V19, cuacaSekarang);
}
}
} else {
Serial.println(F("[BMKG] Array cuaca kosong pada jam ini."));
}
} else {
Serial.printf("[BMKG] ID Kabupaten %s tidak ditemukan di server.\n", adm2Str.c_str());
}
} else {
Serial.printf("[BMKG] Gagal parsing JSON. Error: %s\n", err.c_str());
}
} else {
Serial.printf("[BMKG] Gagal request HTTP. Code: %d\n", httpCode);
}
http.end();
} else {
Serial.println(F("[BMKG] Gagal inisialisasi http.begin ke server."));
}
}
void triggerLedBurst5x() {
ledBurstActive = true;
ledBurstStep = 10;
}
float bacaTinggiAir() {
std::vector<float> tempSamples;
tempSamples.reserve(5);
for(int i = 0; i < 5; i++) {
digitalWrite(triggerPin, LOW); delayMicroseconds(5);
digitalWrite(triggerPin, HIGH); delayMicroseconds(10);
digitalWrite(triggerPin, LOW);
unsigned long durasi = pulseIn(echoPin, HIGH, 30000);
if(durasi > 0) {
float hitungJarak = (durasi * 0.034 / 2.0) + sensorOffset;
tempSamples.push_back(hitungJarak);
}
delay(30);
if(WiFi.status() == WL_CONNECTED && Blynk.connected()) Blynk.run();
}
if(tempSamples.empty()) return TinggiAir;
float sumJarak = 0;
for(float s : tempSamples) sumJarak += s;
Jarak = sumJarak / tempSamples.size();
float hitungTinggi = TinggiSensor - Jarak;
if(hitungTinggi < 0) hitungTinggi = 0;
TinggiAir = hitungTinggi;
return TinggiAir;
}
float hitungEMA(float input) {
if (!emaInitialized) { emaValue = input; emaInitialized = true; }
else { emaValue = emaAlpha * input + (1.0f - emaAlpha) * emaValue; }
return emaValue;
}
float koreksiDurasiCuaca(const String& cuaca, float menit){
if(cuaca.indexOf("Lebat")>=0 || cuaca.indexOf("Hujan")>=0) return menit * 0.7f;
else if(cuaca.indexOf("Ringan")>=0) return menit * 0.85f;
else return menit;
}
void updateV16V17(float tinggiAir, float kecepatan, const String& cuaca, String &v16, String &v17){
v16 = "-"; v17 = "-";
if (dataCounter < 10) {
if (WiFi.status() == WL_CONNECTED && Blynk.connected()) {
Blynk.virtualWrite(V16, v16); Blynk.setProperty(V16, "color", "#808080");
Blynk.virtualWrite(V17, v17); Blynk.setProperty(V17, "color", "#808080");
}
return;
}
auto durasiMenit = [&](float from, float to, float kecep) -> float {
if(abs(kecep) < 0.5f) return -1.0f;
float menit = (to - from) / kecep * 60.0f;
menit = koreksiDurasiCuaca(cuaca, menit);
if(menit < 0) return -1.0f;
return (float)((int)round(menit));
};
if(tinggiAir <= levelAmanMax){
if(kecepatan > 0.5f){
float dWaspada = durasiMenit(tinggiAir, levelWaspadaMax, kecepatan);
float dAman = durasiMenit(tinggiAir, levelAmanMax, kecepatan);
v16 = (dWaspada >= 0) ? "⬆️ " + String(dWaspada, 0) + " min ❌" : "⬆️ ❌";
v17 = (dAman >= 0) ? "⬆️ " + String(dAman, 0) + " min ⚠️" : "⬆️ ⚠️";
}
else if(kecepatan < -0.5f){ v16="-"; v17="⬇️ ✅"; }
else { v16="-"; v17="➡️ ✅"; }
}
else if(tinggiAir <= levelWaspadaMax){
if(kecepatan > 0.5f){
float dWaspada = durasiMenit(tinggiAir, levelWaspadaMax, kecepatan);
v16 = (dWaspada >= 0) ? "⬆️ " + String(dWaspada, 0) + " min ❌" : "⬆️ ❌";
v17 = "⬆️ ⚠️";
}
else if(kecepatan < -0.5f){
float dAman = durasiMenit(tinggiAir, levelAmanMax, kecepatan);
v16 = "⬇️ ⚠️";
v17 = (dAman >= 0) ? "⬇️ " + String(dAman, 0) + " min ✅" : "⬇️ ✅";
}
else { v17="➡️ ⚠️"; }
}
else {
if(kecepatan > 0.5f){ v16="⬆️ ❌"; v17="-"; }
else if(kecepatan < -0.5f){
float dWaspada = durasiMenit(tinggiAir, levelWaspadaMax, kecepatan);
float dAman = durasiMenit(tinggiAir, levelAmanMax, kecepatan);
v16 = (dWaspada >= 0) ? "⬇️ " + String(dWaspada, 0) + " min ⚠️" : "⬇️ ⚠️";
v17 = (dAman >= 0) ? "⬇️ " + String(dAman, 0) + " min ✅" : "⬇️ ✅";
}
else { v16="-"; v17="➡️ ❌"; }
}
auto warna = [](const String& teks) -> const char* {
if(teks == "-" || teks.indexOf("Menunggu") >= 0) return "#808080";
if(teks.indexOf("✅") >= 0) return "#4AB765";
if(teks.indexOf("⚠️") >= 0) return "#F5B800";
return "#EC4751";
};
if (WiFi.status() == WL_CONNECTED && Blynk.connected()) {
Blynk.virtualWrite(V16, v16); Blynk.setProperty(V16, "color", warna(v16));
Blynk.virtualWrite(V17, v17); Blynk.setProperty(V17, "color", warna(v17));
}
}
String prediksiLevelV32Gabungan(float tinggiAir, float kecepatan, const String& cuaca){
String v16, v17;
updateV16V17(tinggiAir, kecepatan, cuaca, v16, v17);
return v16 + " | " + v17;
}
void kirimForecastV31V33(float tinggiAir, float kecepatan, const String& cuaca, const String& tren){
if (!(WiFi.status() == WL_CONNECTED && Blynk.connected())) return;
const char* status = "";
if(tinggiAir <= levelAmanMax){ status = "✅ Aman"; Blynk.setProperty(V31, "color", "#4AB765"); }
else if(tinggiAir <= levelWaspadaMax){ status = "⚠️ Waspada"; Blynk.setProperty(V31, "color", "#F5B800"); }
else { status = "❌ Bahaya"; Blynk.setProperty(V31, "color", "#EC4751"); }
String prediksi = prediksiLevelV32Gabungan(tinggiAir, kecepatan, cuaca);
Blynk.virtualWrite(V31, "Status : " + String(status));
Blynk.virtualWrite(V32, "Prediksi : " + prediksi);
Blynk.virtualWrite(V33, "Kondisi : " + tren + " " + cuaca);
}
void tampilkanSerialMonitor(float fJarak, float fTinggiAir, float fElevasi, float fKecepatan, const String& fTren, const String& fCuaca){
Serial.println(F("\n========== DATA AIR =========="));
Serial.printf("Jarak Sensor : %.2f cm\n", fJarak);
Serial.printf("Tinggi Air : %.2f cm\n", fTinggiAir);
Serial.printf("Elevasi Muka : %.2f m\n", fElevasi / 100.0f);
Serial.printf("Kecepatan : %.2f cm/jam\n", fKecepatan);
Serial.printf("Tren : %s\n", fTren.c_str());
Serial.println(F("=============================="));
}
float hitungKecepatanRegresi(const std::vector<float>& samples, float interval_ms){
int n = samples.size();
if(n < 2) return 0.0f;
float sumX=0, sumY=0, sumXY=0, sumXX=0;
float factor = interval_ms / 3600000.0f;
for(int i=0; i<n; i++){
float t = i * factor;
float h = samples[i];
sumX += t; sumY += h; sumXY += t*h; sumXX += t*t;
}
float denom = (n * sumXX - sumX * sumX);
if(abs(denom) < 1e-9) return 0.0f;
return (n * sumXY - sumX * sumY) / denom;
}
String ambilTimestamp() {
struct tm timeinfo;
if(!getLocalTime(&timeinfo)) return "Offline";
char buf[25];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(buf);
}
String urlEncode(const String& str) {
String encoded = "";
encoded.reserve(str.length() * 3 / 2);
for (unsigned int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (isalnum(c)) {
encoded += c;
} else {
char code0 = "0123456789ABCDEF"[(c >> 4) & 0x0F];
char code1 = "0123456789ABCDEF"[c & 0x0F];
encoded += '%'; encoded += code0; encoded += code1;
}
}
return encoded;
}
bool kirimDataKeServer(const DataBuffer& data) {
if (WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
http.setTimeout(10000);
http.setReuse(true);
String finalDeviceID = (data.device_id.length() > 0) ? data.device_id : "SWARFS-S3-01";
// PERBAIKAN STABILITAS PENULISAN KOORDINAT (DI-URLENCODE AGAR TANDA NEGATIF/MINUS DAN TITIK AMAN)
String postData = "";
postData.reserve(512);
postData += "device_id=" + urlEncode(finalDeviceID);
postData += "&nama_titik=" + urlEncode(data.nama_titik);
postData += "&psm=" + urlEncode(data.psm);
postData += "&estate=" + urlEncode(data.estate);
postData += "&divisi=" + urlEncode(data.divisi);
postData += "&blok=" + urlEncode(data.blok);
postData += "&status=" + urlEncode(data.status);
postData += "&latitude=" + urlEncode(data.log_lat); // Diproteksi dengan urlEncode
postData += "&longitude=" + urlEncode(data.log_long); // Diproteksi dengan urlEncode
postData += "&tinggi_muka_air=" + String(data.tinggi_muka_air, 2);
postData += "&elevasi_muka_air=" + String(data.elevasi_muka_air, 2);
postData += "&jarak=" + String(data.jarak, 2);
postData += "&kecepatan=" + String(data.kecepatan, 2);
postData += "&kategori_perubahan=" + urlEncode(data.kategori_perubahan);
postData += "&time_forecast=" + urlEncode(data.time_forecast);
postData += "&created_at=" + urlEncode(data.created_at);
bool successGS = false;
bool successAPI = false;
if(conf_gs_url.length() > 0) {
WiFiClientSecure clientSecure;
clientSecure.setInsecure();
clientSecure.setTimeout(10000);
clientSecure.setHandshakeTimeout(10000);
http.begin(clientSecure, conf_gs_url);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int code = http.POST(postData);
if(code == 200 || code == 302 || code == 201) {
successGS = true;
} else {
Serial.printf("[SERVER] Google Sheet Gagal HTTP Code: %d\n", code);
}
http.end();
}
if(WiFi.status() == WL_CONNECTED && Blynk.connected()) { Blynk.run(); }
if(conf_api_server_ip.length() > 0) {
WiFiClient clientNormal;
String targetUrl = conf_api_server_ip;
if(!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
targetUrl = "http://" + targetUrl;
}
http.begin(clientNormal, targetUrl);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int code = http.POST(postData);
if(code == 200) successAPI = true;
http.end();
}
if (conf_api_server_ip.length() > 0 && conf_gs_url.length() > 0) {
return (successAPI && successGS);
}
return (conf_api_server_ip.length() > 0) ? successAPI : successGS;
}
void simpanKeFlash(const DataBuffer& data) {
File file = LittleFS.open("/data_log.txt", FILE_APPEND);
if(!file) return;
String finalDeviceID = (data.device_id.length() > 0) ? data.device_id : "SWARFS-S3-01";
file.printf("%s|%s|%s|%s|%s|%s|%s|%s|%s|%.2f|%.2f|%.2f|%.2f|%s|%s|%s\n",
finalDeviceID.c_str(), data.nama_titik.c_str(), data.psm.c_str(),
data.estate.c_str(), data.divisi.c_str(), data.blok.c_str(),
data.status.c_str(), data.log_lat.c_str(), data.log_long.c_str(),
data.tinggi_muka_air, data.elevasi_muka_air, data.jarak,
data.kecepatan, data.kategori_perubahan.c_str(), data.time_forecast.c_str(),
data.created_at.c_str());
file.close();
}
void kirimBufferOffline() {
if(WiFi.status() != WL_CONNECTED) return;
if(!LittleFS.exists("/data_log.txt")) return;
File file = LittleFS.open("/data_log.txt", "r");
if(!file) return;
File tempFile = LittleFS.open("/temp_log.txt", "w");
if(!tempFile) { file.close(); return; }
bool dataTerkirimDiSiklusIni = false;
static char buf[512];
while(file.available()){
int len = file.readBytesUntil('\n', buf, sizeof(buf) - 1);
buf[len] = '\0';
String line(buf);
line.trim();
if(line.length() == 0) continue;
if(dataTerkirimDiSiklusIni) {
tempFile.println(line);
continue;
}
int p1 = line.indexOf('|');
int p2 = line.indexOf('|', p1 + 1);
int p3 = line.indexOf('|', p2 + 1);
int p4 = line.indexOf('|', p3 + 1);
int p5 = line.indexOf('|', p4 + 1);
int p6 = line.indexOf('|', p5 + 1);
int p7 = line.indexOf('|', p6 + 1);
int p8 = line.indexOf('|', p7 + 1);
int p9 = line.indexOf('|', p8 + 1);
int p10 = line.indexOf('|', p9 + 1);
int p11 = line.indexOf('|', p10 + 1);
int p12 = line.indexOf('|', p11 + 1);
int p13 = line.indexOf('|', p12 + 1);
int p14 = line.indexOf('|', p13 + 1);
int p15 = line.indexOf('|', p14 + 1);
if (p1 > 0 && p2 > 0 && p3 > 0 && p4 > 0 && p5 > 0 && p6 > 0 && p7 > 0 && p8 > 0 &&
p9 > 0 && p10 > 0 && p11 > 0 && p12 > 0 && p13 > 0 && p14 > 0 && p15 > 0) {
DataBuffer data;
data.device_id = line.substring(0, p1);
data.nama_titik = line.substring(p1 + 1, p2);
data.psm = line.substring(p2 + 1, p3);
data.estate = line.substring(p3 + 1, p4);
data.divisi = line.substring(p4 + 1, p5);
data.blok = line.substring(p5 + 1, p6);
data.status = line.substring(p6 + 1, p7);
data.log_lat = line.substring(p7 + 1, p8);
data.log_long = line.substring(p8 + 1, p9);
data.tinggi_muka_air = line.substring(p9 + 1, p10).toFloat();
data.elevasi_muka_air = line.substring(p10 + 1, p11).toFloat();
data.jarak = line.substring(p11 + 1, p12).toFloat();
data.kecepatan = line.substring(p12 + 1, p13).toFloat();
data.kategori_perubahan = line.substring(p13 + 1, p14);
data.time_forecast = line.substring(p14 + 1, p15);
data.created_at = line.substring(p15 + 1);
if(!kirimDataKeServer(data)) {
tempFile.println(line);
} else {
dataTerkirimDiSiklusIni = true;
Serial.println(F("[SERVER] Sukses mencicil 1 data offline ke Google Sheet."));
delay(1500);
}
}
if(WiFi.status() == WL_CONNECTED && Blynk.connected()) Blynk.run();
}
file.close(); tempFile.close();
LittleFS.remove("/data_log.txt");
File checkTemp = LittleFS.open("/temp_log.txt", "r");
if (checkTemp && checkTemp.size() > 0) {
checkTemp.close();
LittleFS.rename("/temp_log.txt", "/data_log.txt");
} else {
if(checkTemp) checkTemp.close();
LittleFS.remove("/temp_log.txt");
}
}
void setConfigUpdated() {
configUpdated = true;
lastConfigUpdate = millis();
}
// ===================== OPERASI PARAMETER BLYNK =====================
BLYNK_WRITE(V10){
intervalMs = param.asInt() * 60000;
preferences.begin("swarfs", false); preferences.putULong("interval", intervalMs); preferences.end();
setConfigUpdated();
}
BLYNK_WRITE(V11){
TinggiSensor = param.asFloat();
preferences.begin("swarfs", false); preferences.putFloat("t_sensor", TinggiSensor); preferences.end();
setConfigUpdated();
}
BLYNK_WRITE(V12){
ElevasiDasar = param.asFloat();
preferences.begin("swarfs", false); preferences.putFloat("el_dasar", ElevasiDasar); preferences.end();
setConfigUpdated();
}
BLYNK_WRITE(V13){
levelAmanMax = param.asFloat();
preferences.begin("swarfs", false); preferences.putFloat("lv_aman", levelAmanMax); preferences.end();
setConfigUpdated();
}
BLYNK_WRITE(V14){
levelWaspadaMax = param.asFloat();
preferences.begin("swarfs", false); preferences.putFloat("lv_waspada", levelWaspadaMax); preferences.end();
setConfigUpdated();
}
BLYNK_WRITE(V15){
periodeRata2 = param.asInt();
float approxSamples = max(1.0f, (float)periodeRata2 / ((float)intervalMs / 1000.0f / 60.0f));
emaAlpha = 2.0f / (approxSamples + 1.0f);
if(emaAlpha < 0.01f) emaAlpha = 0.01f; if(emaAlpha > 0.9f) emaAlpha = 0.9f;
preferences.begin("swarfs", false); preferences.putULong("p_rata", periodeRata2); preferences.end();
setConfigUpdated();
}
BLYNK_WRITE(V21) {
String val = param.asStr();
val.trim();
if(val.length() >= 4) {
val.toCharArray(bmkgAdm2, sizeof(bmkgAdm2));
preferences.begin("swarfs", false); preferences.putString("adm2", val); preferences.end();
perluUpdateBmkg = true;
}
}
BLYNK_WRITE(V22){
sensorOffset = param.asFloat();
preferences.begin("swarfs", false); preferences.putFloat("offset", sensorOffset); preferences.end();
setConfigUpdated();
}
void lakukanPengukuran() {
float tinggiRaw = bacaTinggiAir();
if(WiFi.status() == WL_CONNECTED && Blynk.connected()) Blynk.run();
float tinggiEma = hitungEMA(tinggiRaw);
Elevasi = ElevasiDasar + tinggiEma;
tinggiSamples.push_back(tinggiEma);
float pembagi = ((float)intervalMs / 1000.0f) / 60.0f;
if(pembagi <= 0) pembagi = 1.0f;
long kapasitas = round((float)periodeRata2 / pembagi);
if(kapasitas < 2) kapasitas = 2;
while((long)tinggiSamples.size() > kapasitas) {
tinggiSamples.erase(tinggiSamples.begin());
}
float kecepatan = hitungKecepatanRegresi(tinggiSamples, intervalMs);
if(dataCounter < 100) dataCounter++;
const char* tren;
const char* formatWarna = "#0000FF";
if(kecepatan > 5.0f){ tren="⬆️ Naik Cepat"; formatWarna="#EC4751"; }
else if(kecepatan > 1.0f){ tren="⬆️ Naik Sedang"; formatWarna="#F58D42"; }
else if(kecepatan > 0.5f){ tren="⬆️ Naik Lambat"; formatWarna="#F5B800"; }
else if(kecepatan < -5.0f){ tren="⬇️ Turun Cepat"; formatWarna="#4AB765"; }
else if(kecepatan < -1.0f){ tren="⬇️ Turun Sedang"; formatWarna="#6CCF8C"; }
else if(kecepatan < -0.5f){ tren="⬇️ Turun Lambat"; formatWarna="#A8E6A1"; }
else{ tren="➡️ Stabil"; formatWarna="#0000FF"; }
String statusLevel = (tinggiEma <= levelAmanMax) ? "Aman" : ((tinggiEma <= levelWaspadaMax) ? "Waspada" : "Bahaya");
String forecastText = prediksiLevelV32Gabungan(tinggiEma, kecepatan, String(cuacaSekarang));
tampilkanSerialMonitor(Jarak, tinggiEma, Elevasi, kecepatan, String(tren), String(cuacaSekarang));
DataBuffer data;
data.device_id = conf_device_id;
data.nama_titik = conf_nama_titik;
data.psm = conf_psm;
data.estate = conf_estate;
data.divisi = conf_divisi;
data.blok = conf_blok;
data.status = statusLevel;
// Amankan fallback koordinat tetap sebagai string
data.log_lat = (conf_latitude.length() > 0) ? conf_latitude : "0.0";
data.log_long = (conf_longitude.length() > 0) ? conf_longitude : "0.0";
data.tinggi_muka_air = tinggiEma;
data.elevasi_muka_air = Elevasi / 100.0f;
data.jarak = Jarak;
data.kecepatan = kecepatan;
data.kategori_perubahan = String(tren);
data.time_forecast = forecastText;
data.created_at = ambilTimestamp();
if (WiFi.status() == WL_CONNECTED) {
if(Blynk.connected()){
Blynk.virtualWrite(V0, Jarak);
Blynk.virtualWrite(V1, tinggiEma);
Blynk.virtualWrite(V2, Elevasi/100.0f);
if(tinggiEma <= levelAmanMax){
Blynk.setProperty(V1,"color","#4AB765"); Blynk.setProperty(V2,"color","#4AB765");
Blynk.setProperty(V5,"color","#4AB765"); Blynk.virtualWrite(V5,255); Blynk.virtualWrite(V6,0);
} else if(tinggiEma <= levelWaspadaMax){
Blynk.setProperty(V1,"color","#F5B800"); Blynk.setProperty(V2,"color","#F5B800");
Blynk.setProperty(V5,"color","#F5B800"); Blynk.virtualWrite(V5,255); Blynk.virtualWrite(V6,1);
} else {
Blynk.setProperty(V1,"color","#EC4751"); Blynk.setProperty(V2,"color","#EC4751");
Blynk.virtualWrite(V5,255); Blynk.virtualWrite(V6,2);
Blynk.logEvent("bahaya","Level air mencapai BAHAYA!");
}
if(dataCounter >= 10){
Blynk.virtualWrite(V8, kecepatan);
Blynk.virtualWrite(V9, tren);
Blynk.setProperty(V9, "color", formatWarna);
kirimForecastV31V33(tinggiEma, kecepatan, String(cuacaSekarang), String(tren));
} else {
Blynk.virtualWrite(V8, 0.0f); Blynk.virtualWrite(V9, "-"); Blynk.setProperty(V9, "color", "#808080");
Blynk.virtualWrite(V31, "Status : Menghitung...");
Blynk.virtualWrite(V32, "Prediksi : Menunggu Sampel (" + String(dataCounter) + "/10)");
Blynk.virtualWrite(V33, "Kondisi : Kalibrasi Tren");
}
triggerLedBurst5x();
}
if (!kirimDataKeServer(data)) { simpanKeFlash(data); }
kirimBufferOffline();
} else {
simpanKeFlash(data);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.setDebugOutput(false);
setenv("TZ", "WIB-7", 1); tzset();
pinMode(ledPin, OUTPUT);
pinMode(triggerPin, OUTPUT);
pinMode(echoPin, INPUT);
loadConfig();
if(!LittleFS.begin(true)){ Serial.println(F("Gagal mounting LittleFS")); }
WiFi.mode(WIFI_AP_STA);
delay(100);
WiFi.setAutoReconnect(true);
WiFi.setScanMethod(WIFI_FAST_SCAN);
bukaWebPortal();
if(conf_ssid != "") {
Serial.printf("\n[WiFi] Menyambung ke SSID target: %s \n", conf_ssid.c_str());
WiFi.begin(conf_ssid.c_str(), conf_pass.c_str(), 6);
unsigned long startWait = millis();
while(WiFi.status() != WL_CONNECTED && millis() - startWait < 10000) {
delay(250);
Serial.print(".");
}
}
if(WiFi.status() == WL_CONNECTED) {
Serial.println(F("\n[+] WiFi Sukses Terhubung!"));
configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org", "time.google.com");
struct tm timeinfo;
int retryNTP = 0;
while(!getLocalTime(&timeinfo) && retryNTP < 15) {
delay(300);
retryNTP++;
}
ambilCuacaBMKG_ADM2();
if(conf_blynk_token != "") {
Serial.println(F("[BLYNK] Menginisialisasi jabat tangan cloud..."));
Blynk.config(conf_blynk_token.c_str(), "blynk.cloud", 80);
unsigned long startBlynk = millis();
while(!Blynk.connected() && millis() - startBlynk < 6000) {
Blynk.connect(1000);
}
if(Blynk.connected()) {
Serial.println(F("[BLYNK] Berhasil Terhubung Secara Mandiri!"));
Blynk.syncVirtual(V10, V11, V12, V13, V14, V15, V21, V22);
}
}
} else {
Serial.println(F("\n[!] Gagal terhubung ke ruter saat booting. Alat berjalan mandiri dalam Mode AP."));
}
}
void loop() {
unsigned long currentMillis = millis();
if (perluRestart && (currentMillis - waktuRestart >= 2000)) {
ESP.restart();
}
if (configUpdated && (currentMillis - lastConfigUpdate > 1000)) {
tampilkanNilaiKonfigurasi();
configUpdated = false;
}
server.handleClient();
if (perluUpdateBmkg) {
perluUpdateBmkg = false;
ambilCuacaBMKG_ADM2();
}
if (WiFi.status() != WL_CONNECTED) {
if (currentMillis - lastWifiRetry >= wifiRetryInterval) {
lastWifiRetry = currentMillis;
if (conf_ssid != "") {
Serial.println(F("[WiFi] Status: Terputus. Auto-reconnect SDK sedang bekerja senyap di background..."));
}
}
}
else {
if (Blynk.connected()) {
Blynk.run();
} else if (conf_blynk_token != "" && (currentMillis - lastWifiRetry >= 15000)) {
lastWifiRetry = currentMillis;
Serial.println(F("[BLYNK] Mencoba jabat tangan ulang dengan cloud server..."));
Blynk.connect(1000);
}
static unsigned long lastNtpAttempt = 0;
struct tm timeinfo;
if (!getLocalTime(&timeinfo) || timeinfo.tm_year < (2024 - 1900)) {
if (currentMillis - lastNtpAttempt >= 30000) {
lastNtpAttempt = currentMillis;
setenv("TZ", "WIB-7", 1); tzset();
configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org", "time.google.com");
}
}
static bool bmkgSudahAmbil = false;
if (getLocalTime(&timeinfo) && !bmkgSudahAmbil) {
ambilCuacaBMKG_ADM2();
bmkgSudahAmbil = true;
}
if (currentMillis - lastBmkgSync >= 3600000) {
lastBmkgSync = currentMillis;
ambilCuacaBMKG_ADM2();
}
}
unsigned long currentInterval = (intervalMs > 0) ? intervalMs : 60000;
if (currentMillis - lastSensorRead >= currentInterval) {
lastSensorRead = currentMillis;
lakukanPengukuran();
}
if (ledBurstActive) {
if (currentMillis - ledTimer >= 100) {
ledTimer = currentMillis;
digitalWrite(ledPin, !digitalRead(ledPin));
ledBurstStep--;
if (ledBurstStep <= 0) { ledBurstActive = false; digitalWrite(ledPin, LOW); }
}
}
else {
if (WiFi.status() == WL_CONNECTED) {
if (currentMillis - ledTimer >= 1500) { ledTimer = currentMillis; digitalWrite(ledPin, !digitalRead(ledPin)); }
} else {
if (currentMillis - ledTimer >= 300) { ledTimer = currentMillis; digitalWrite(ledPin, !digitalRead(ledPin)); }
}
}
}